using System.IdentityModel.Tokens.Jwt; using System.Net; using System.Net.Http.Headers; using System.Security.Claims; using System.Text; using Microsoft.IdentityModel.Tokens; using Xunit; namespace D3ROVoice.Api.Tests; [Collection("Api server integration")] public sealed class SttGatewayAuthorizationE2ETests : IClassFixture { private const string Secret = "admin-e2e-jwt-secret-0123456789-abcdef"; private const string Issuer = "https://admin-e2e.test"; private const string Audience = "d3ro-admin-e2e"; private const string GatewayToken = "fixture-internal-gateway-token-32-bytes-minimum"; private readonly AdminAuthorizationE2ETests.ApiFactory _factory; public SttGatewayAuthorizationE2ETests(AdminAuthorizationE2ETests.ApiFactory factory) { _factory = factory; } [Fact] public async Task UserEndpointRequiresJwtThenStillRefusesQuotaBypass() { using var client = _factory.CreateClient(); using var anonymousBody = JsonBody(); Assert.Equal(HttpStatusCode.Unauthorized, (await client.PostAsync("/api/stt/transcribe", anonymousBody)).StatusCode); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", UserToken()); using var authenticatedBody = JsonBody(); var response = await client.PostAsync("/api/stt/transcribe", authenticatedBody); Assert.Equal(HttpStatusCode.Gone, response.StatusCode); Assert.Contains("stt_edge_gateway_required", await response.Content.ReadAsStringAsync(), StringComparison.Ordinal); } [Theory] [InlineData(null)] [InlineData("one-character-off")] public async Task InternalEndpointRejectsMissingOrWrongGatewaySecret(string? suppliedToken) { using var client = _factory.CreateClient(); if (suppliedToken != null) client.DefaultRequestHeaders.Add("X-D3RO-STT-Gateway-Token", suppliedToken); using var body = AudioBody(); var response = await client.PostAsync("/api/stt/internal/transcribe", body); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); Assert.Contains("stt_gateway_unauthorized", await response.Content.ReadAsStringAsync(), StringComparison.Ordinal); } [Fact] public async Task InternalEndpointAcceptsExactSecretButFailsClosedWithoutProvider() { using var client = _factory.CreateClient(); client.DefaultRequestHeaders.Add("X-D3RO-STT-Gateway-Token", GatewayToken); using var body = AudioBody(); var response = await client.PostAsync("/api/stt/internal/transcribe", body); Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode); Assert.Contains("stt_provider_unavailable", await response.Content.ReadAsStringAsync(), StringComparison.Ordinal); } private static StringContent JsonBody() => new("{\"audioBase64\":\"AQID\"}", Encoding.UTF8, "application/json"); private static MultipartFormDataContent AudioBody() { var body = new MultipartFormDataContent(); var audio = new ByteArrayContent([1, 2, 3, 4]); audio.Headers.ContentType = new MediaTypeHeaderValue("audio/wav"); body.Add(audio, "file", "fixture.wav"); return body; } private static string UserToken() { var credentials = new SigningCredentials( new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Secret)), SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: Issuer, audience: Audience, claims: [ new Claim(ClaimTypes.NameIdentifier, "1"), new Claim(ClaimTypes.Email, "user@example.com"), new Claim(ClaimTypes.Role, "user") ], expires: DateTime.UtcNow.AddMinutes(5), signingCredentials: credentials); return new JwtSecurityTokenHandler().WriteToken(token); } }