[AZ-PENDING-1] [AZ-PENDING-2] Testability fixes: JWKS gate + RMQ DNS

C01 (JWKS HTTPS env gate, src/Auth/JwtExtensions.cs)
  Gate HttpDocumentRetriever.RequireHttps on
  ASPNETCORE_ENVIRONMENT != "E2ETest" (case-insensitive). HTTPS is
  still enforced for Development, Staging, Production, and any
  unset value. Test harness can now serve JWKS over plain HTTP via
  the mock issuer documented in _docs/02_document/tests/environment.md.

C02 (RabbitMQ host DNS resolution, src/Services/FailsafeProducer.cs)
  Resolve RABBITMQ_HOST via DNS when the value is not a literal IP.
  Adds ResolveHostAddress(host, ct) helper that uses
  IPAddress.TryParse first, then Dns.GetHostAddressesAsync. Fixes
  a latent production bug (operators using a DNS hostname like
  "rabbitmq" or "broker.internal" got a FormatException at startup)
  and unblocks the e2e Docker test harness where the broker is
  reachable only via service-name DNS.

Review report: _docs/03_implementation/reviews/batch_01_review.md
  Verdict PASS_WITH_WARNINGS (1 Low/Maintainability finding,
  documented as deferred to Step 8 hardening).

Tracker IDs are placeholders — Jira MCP unavailable. Real IDs to be
assigned per _docs/_process_leftovers/2026-05-14_testability-tracker.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-14 20:24:30 +03:00
parent 13e9731a8f
commit 90d48cf3c0
4 changed files with 155 additions and 5 deletions
+11 -1
View File
@@ -27,10 +27,20 @@ public static class JwtExtensions
// document; admin only exposes JWKS, so we wire a JWKS-only retriever.
// The manager caches the document and refreshes on the default schedule
// (matches admin's Cache-Control: public, max-age=3600 on /.well-known/jwks.json).
//
// RequireHttps is relaxed only when ASPNETCORE_ENVIRONMENT=E2ETest so the
// blackbox harness can serve its mock JWKS over the test-net HTTP issuer
// (architecture.md Open Risks Section 6). Any other environment — including
// unset, Development, Staging, Production — keeps the HTTPS enforcement.
var requireHttpsForJwks = !string.Equals(
Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"),
"E2ETest",
StringComparison.OrdinalIgnoreCase);
var jwksConfigManager = new ConfigurationManager<JsonWebKeySet>(
jwksUrl,
new JwksRetriever(),
new HttpDocumentRetriever { RequireHttps = true });
new HttpDocumentRetriever { RequireHttps = requireHttpsForJwks });
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
+21 -1
View File
@@ -51,9 +51,16 @@ public class FailsafeProducer(
private async Task ProcessQueue(CancellationToken ct)
{
// RABBITMQ_HOST is documented as accepting either a literal IP or a DNS
// hostname (container service name, k8s service, etc.). IPEndPoint takes
// an IPAddress, so non-IP values must be resolved before construction —
// a bare IPAddress.Parse here throws FormatException on every drain cycle
// for any hostname value.
var brokerAddress = await ResolveHostAddress(config.Host, ct);
var streamSystem = await StreamSystem.Create(new StreamSystemConfig
{
Endpoints = [new IPEndPoint(IPAddress.Parse(config.Host), config.Port)],
Endpoints = [new IPEndPoint(brokerAddress, config.Port)],
UserName = config.Username,
Password = config.Password
});
@@ -192,6 +199,19 @@ public class FailsafeProducer(
}
}
private static async Task<IPAddress> ResolveHostAddress(string host, CancellationToken ct)
{
if (IPAddress.TryParse(host, out var literal))
return literal;
var addresses = await Dns.GetHostAddressesAsync(host, ct);
if (addresses.Length == 0)
throw new InvalidOperationException(
$"DNS resolution for RABBITMQ_HOST '{host}' returned no addresses.");
return addresses[0];
}
public static async Task EnqueueAsync(AppDataConnection db, string annotationId, QueueOperation operation)
{
var ids = JsonSerializer.Serialize(new[] { annotationId });