d3ro-voice/apps/api-server.Tests/SttGatewayAuthorizationE2ETests.cs
2026-08-29 18:33:45 +09:00

100 lines
3.9 KiB
C#

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<AdminAuthorizationE2ETests.ApiFactory>
{
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);
}
}