using System.IdentityModel.Tokens.Jwt; using System.Net; using System.Net.Http.Headers; using System.Security.Claims; using System.Text; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Data.Sqlite; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; using Xunit; namespace D3ROVoice.Api.Tests; [CollectionDefinition("Api server integration", DisableParallelization = true)] public sealed class ApiServerIntegrationCollection; [Collection("Api server integration")] public sealed class AdminAuthorizationE2ETests : 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 readonly ApiFactory _factory; public AdminAuthorizationE2ETests(ApiFactory factory) { _factory = factory; } [Fact] public async Task AdminReadEndpointsRejectAnonymousAndNonManager() { using var client = _factory.CreateClient(); Assert.Equal(HttpStatusCode.Unauthorized, (await client.GetAsync("/api/admin/stats")).StatusCode); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token("User")); Assert.Equal(HttpStatusCode.Forbidden, (await client.GetAsync("/api/admin/stats")).StatusCode); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token("manager")); Assert.Equal(HttpStatusCode.OK, (await client.GetAsync("/api/admin/stats")).StatusCode); } [Fact] public async Task AdminMutationRejectsManagerAndAcceptsNormalizedSuperAdmin() { using var client = _factory.CreateClient(); using var emptyPayload = new StringContent("{}", Encoding.UTF8, "application/json"); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token("Manager")); Assert.Equal(HttpStatusCode.Forbidden, (await client.PostAsync("/api/admin/endpoints", emptyPayload)).StatusCode); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token("super_admin")); using var secondPayload = new StringContent("{}", Encoding.UTF8, "application/json"); Assert.Equal(HttpStatusCode.BadRequest, (await client.PostAsync("/api/admin/endpoints", secondPayload)).StatusCode); } private static string Token(string role) { 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, "admin@example.com"), new Claim(ClaimTypes.Role, role) ], expires: DateTime.UtcNow.AddMinutes(5), signingCredentials: credentials); return new JwtSecurityTokenHandler().WriteToken(token); } public sealed class ApiFactory : WebApplicationFactory { private readonly string _databasePath = Path.Combine( Path.GetTempPath(), $"d3ro-admin-auth-e2e-{Guid.NewGuid():N}.db"); private readonly Dictionary _previousEnvironment = new(); public ApiFactory() { SetEnvironment("JWT_SECRET", Secret); SetEnvironment("JWT_ISSUER", Issuer); SetEnvironment("JWT_AUDIENCE", Audience); SetEnvironment("D3RO_API_TOKEN", "fixture-internal-gateway-token-32-bytes-minimum"); SetEnvironment("DB_PATH", _databasePath); SetEnvironment("CORS_ALLOWED_ORIGINS", "http://localhost:3001"); SetEnvironment("ALLOWED_HOSTS", "localhost;127.0.0.1"); } protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.UseEnvironment("Development"); builder.ConfigureLogging(logging => logging.ClearProviders()); } protected override void Dispose(bool disposing) { base.Dispose(disposing); SqliteConnection.ClearAllPools(); if (File.Exists(_databasePath)) File.Delete(_databasePath); foreach (var (name, value) in _previousEnvironment) { Environment.SetEnvironmentVariable(name, value); } } private void SetEnvironment(string name, string value) { _previousEnvironment[name] = Environment.GetEnvironmentVariable(name); Environment.SetEnvironmentVariable(name, value); } } }