feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
191
apps/api-server.Tests/SttControllerSecurityTests.cs
Normal file
191
apps/api-server.Tests/SttControllerSecurityTests.cs
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using D3ROVoice.Api.Controllers;
|
||||
using D3ROVoice.Api.Dtos;
|
||||
using D3ROVoice.Api.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Xunit;
|
||||
|
||||
namespace D3ROVoice.Api.Tests;
|
||||
|
||||
public sealed class SttControllerSecurityTests
|
||||
{
|
||||
private const string GatewayToken = "fixture-internal-gateway-token-32-bytes-minimum";
|
||||
|
||||
[Fact]
|
||||
public void Controller_RequiresJwtAuthorization()
|
||||
{
|
||||
Assert.NotNull(Attribute.GetCustomAttribute(typeof(SttController), typeof(AuthorizeAttribute)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, false)]
|
||||
[InlineData("42", true)]
|
||||
[InlineData("not-an-integer", true)]
|
||||
public async Task Transcribe_AlwaysRequiresQuotaOwningEdgeGatewayAndNeverCallsProvider(
|
||||
string? userId,
|
||||
bool authenticated)
|
||||
{
|
||||
var service = new RecordingSttService();
|
||||
var controller = CreateController(service, userId, isAuthenticated: authenticated);
|
||||
|
||||
var result = Assert.IsType<ObjectResult>(await controller.Transcribe());
|
||||
|
||||
Assert.Equal(StatusCodes.Status410Gone, result.StatusCode);
|
||||
Assert.Contains("stt_edge_gateway_required", JsonSerializer.Serialize(result.Value), StringComparison.Ordinal);
|
||||
Assert.Equal(0, service.TranscribeCalls);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("wrong-token")]
|
||||
public async Task InternalGateway_RejectsMissingOrWrongTokenWithoutProviderCall(string? suppliedToken)
|
||||
{
|
||||
var service = new RecordingSttService();
|
||||
var controller = CreateController(service, null, isAuthenticated: false, suppliedGatewayToken: suppliedToken);
|
||||
|
||||
var result = Assert.IsType<UnauthorizedObjectResult>(await controller.TranscribeFromQuotaGateway());
|
||||
|
||||
Assert.Equal(StatusCodes.Status401Unauthorized, result.StatusCode);
|
||||
Assert.Equal(0, service.TranscribeCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InternalGateway_FailsClosedWhenServerSecretIsTooShort()
|
||||
{
|
||||
var service = new RecordingSttService();
|
||||
var controller = CreateController(
|
||||
service,
|
||||
null,
|
||||
isAuthenticated: false,
|
||||
configuredGatewayToken: "short",
|
||||
suppliedGatewayToken: "short");
|
||||
|
||||
var result = Assert.IsType<ObjectResult>(await controller.TranscribeFromQuotaGateway());
|
||||
|
||||
Assert.Equal(StatusCodes.Status503ServiceUnavailable, result.StatusCode);
|
||||
Assert.Equal(0, service.TranscribeCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InternalGateway_ValidTokenInvokesProviderWithoutLegacyUsageWrite()
|
||||
{
|
||||
var service = new RecordingSttService();
|
||||
var controller = CreateController(
|
||||
service,
|
||||
null,
|
||||
isAuthenticated: false,
|
||||
suppliedGatewayToken: GatewayToken,
|
||||
includeAudioForm: true);
|
||||
|
||||
var result = Assert.IsType<OkObjectResult>(await controller.TranscribeFromQuotaGateway());
|
||||
|
||||
Assert.Equal(StatusCodes.Status200OK, result.StatusCode);
|
||||
Assert.Equal(1, service.TranscribeCalls);
|
||||
Assert.False(service.LastRecordUsage);
|
||||
Assert.Equal(0, service.LastUserId);
|
||||
Assert.Equal("edge-internal", service.LastUserEmail);
|
||||
}
|
||||
|
||||
private static SttController CreateController(
|
||||
RecordingSttService service,
|
||||
string? userId,
|
||||
bool isAuthenticated,
|
||||
string? email = null,
|
||||
string configuredGatewayToken = GatewayToken,
|
||||
string? suppliedGatewayToken = null,
|
||||
bool includeAudioForm = false)
|
||||
{
|
||||
var claims = new List<Claim>();
|
||||
if (userId != null) claims.Add(new Claim(ClaimTypes.NameIdentifier, userId));
|
||||
if (email != null) claims.Add(new Claim(ClaimTypes.Email, email));
|
||||
var identity = new ClaimsIdentity(claims, isAuthenticated ? "test-auth" : null);
|
||||
var context = new DefaultHttpContext
|
||||
{
|
||||
User = new ClaimsPrincipal(identity)
|
||||
};
|
||||
if (includeAudioForm)
|
||||
{
|
||||
context.Request.ContentType = "multipart/form-data; boundary=fixture";
|
||||
var audioStream = new MemoryStream(new byte[] { 1, 2, 3, 4 });
|
||||
var files = new FormFileCollection
|
||||
{
|
||||
new FormFile(audioStream, 0, audioStream.Length, "file", "fixture.wav")
|
||||
{
|
||||
Headers = new HeaderDictionary(),
|
||||
ContentType = "audio/wav",
|
||||
}
|
||||
};
|
||||
context.Request.Form = new FormCollection(
|
||||
new Dictionary<string, Microsoft.Extensions.Primitives.StringValues>(),
|
||||
files);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Request.ContentType = "application/json";
|
||||
context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{\"audioBase64\":\"AQID\"}"));
|
||||
}
|
||||
if (suppliedGatewayToken != null)
|
||||
context.Request.Headers["X-D3RO-STT-Gateway-Token"] = suppliedGatewayToken;
|
||||
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["D3RO_API_TOKEN"] = configuredGatewayToken,
|
||||
})
|
||||
.Build();
|
||||
|
||||
return new SttController(service, configuration)
|
||||
{
|
||||
ControllerContext = new ControllerContext { HttpContext = context }
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class RecordingSttService : ISttProxyService
|
||||
{
|
||||
public SttProviderUnavailableException? Exception { get; init; }
|
||||
public int TranscribeCalls { get; private set; }
|
||||
public int LastUserId { get; private set; }
|
||||
public string? LastUserEmail { get; private set; }
|
||||
public bool LastRecordUsage { get; private set; } = true;
|
||||
|
||||
public Task<SttTranscribeResponse> TranscribeAsync(
|
||||
int userId,
|
||||
string userEmail,
|
||||
SttTranscribeRequest request,
|
||||
byte[]? audioBytes = null,
|
||||
string? contentType = null,
|
||||
string? fileName = null,
|
||||
bool recordUsage = true)
|
||||
{
|
||||
TranscribeCalls++;
|
||||
LastUserId = userId;
|
||||
LastUserEmail = userEmail;
|
||||
LastRecordUsage = recordUsage;
|
||||
if (Exception != null) throw Exception;
|
||||
|
||||
return Task.FromResult(new SttTranscribeResponse(
|
||||
"real transcript", 0.99, "ko", 1, "test", "test-model", 1, 0));
|
||||
}
|
||||
|
||||
public Task<SttTestResultDto> TestEndpointAsync(int endpointId, string? testApiKey = null, string? testEndpointUrl = null) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<List<SttProviderEndpointDto>> GetAllEndpointsAsync() =>
|
||||
Task.FromResult(new List<SttProviderEndpointDto>());
|
||||
|
||||
public Task<SttProviderEndpointDto> CreateEndpointAsync(CreateSttEndpointDto dto) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<SttProviderEndpointDto> UpdateEndpointAsync(int id, UpdateSttEndpointDto dto) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<bool> DeleteEndpointAsync(int id) => throw new NotSupportedException();
|
||||
public Task<bool> SetDefaultEndpointAsync(int id) => throw new NotSupportedException();
|
||||
public Task<SttUsageReportDto> GetUsageReportAsync() => throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue