using System.Net.Http.Headers; using System.Security.Claims; using System.Text; using SatelliteProvider.TestSupport; namespace SatelliteProvider.IntegrationTests; public static class JwtTestHelpers { public const string JwtSecretEnvVar = "JWT_SECRET"; public const string JwtIssuerEnvVar = "JWT_ISSUER"; public const string JwtAudienceEnvVar = "JWT_AUDIENCE"; public const string DefaultSubject = "integration-tests"; public static string ResolveSecretOrThrow() { var secret = Environment.GetEnvironmentVariable(JwtSecretEnvVar); if (string.IsNullOrWhiteSpace(secret)) { throw new InvalidOperationException( $"{JwtSecretEnvVar} is not set in the integration test environment. " + "It must match the JWT_SECRET configured for the API container."); } var byteLength = Encoding.UTF8.GetByteCount(secret); if (byteLength < 32) { throw new InvalidOperationException( $"{JwtSecretEnvVar} is {byteLength} bytes; the test runner requires at least 32 bytes to match API validation."); } return secret; } // AZ-494: runner-side resolvers for the iss / aud values the API enforces. // Both MUST be present and match the API container's values, or the API // will reject every minted token at validation time. public static string ResolveIssuerOrThrow() { var issuer = Environment.GetEnvironmentVariable(JwtIssuerEnvVar); if (string.IsNullOrWhiteSpace(issuer)) { throw new InvalidOperationException( $"{JwtIssuerEnvVar} is not set in the integration test environment. " + "It must match the JWT_ISSUER configured for the API container (AZ-494)."); } return issuer; } public static string ResolveAudienceOrThrow() { var audience = Environment.GetEnvironmentVariable(JwtAudienceEnvVar); if (string.IsNullOrWhiteSpace(audience)) { throw new InvalidOperationException( $"{JwtAudienceEnvVar} is not set in the integration test environment. " + "It must match the JWT_AUDIENCE configured for the API container (AZ-494)."); } return audience; } public static void AttachDefaultAuthorization(HttpClient httpClient, string token) { httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); } // AZ-494: convenience wrapper. Reads JWT_ISSUER / JWT_AUDIENCE from env // (cached for the runner's lifetime via resolve calls) and delegates to // the canonical JwtTokenFactory.Create. Test scenarios that need WRONG // iss/aud (e.g. negative AC tests) pass overrides explicitly. public static string MintAuthenticated( string secret, string? subject = null, IEnumerable? extraClaims = null, TimeSpan? lifetime = null, string? overrideIssuer = null, string? overrideAudience = null) { return JwtTokenFactory.Create( secret, subject ?? DefaultSubject, lifetime, extraClaims, issuer: overrideIssuer ?? ResolveIssuerOrThrow(), audience: overrideAudience ?? ResolveAudienceOrThrow()); } public static string MintExpired(string secret, string? subject = null) { return JwtTokenFactory.CreateExpired( secret, subject ?? DefaultSubject, issuer: ResolveIssuerOrThrow(), audience: ResolveAudienceOrThrow()); } }