using System.IdentityModel.Tokens.Jwt; using System.Net; using System.Net.Http.Json; using System.Net.Http.Headers; using System.Security.Claims; using System.Text; using Campaign_Tracker.Server.Authentication; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; namespace Campaign_Tracker.Server.Tests; public class AuthEndpointTests : IClassFixture> { private const string Issuer = "http://kci-app01.ntp.kentcommunications.com:8180/realms/KCI"; private const string ClientId = "canopy-web"; private const string SigningKey = "test-signing-key-with-at-least-32-characters"; private readonly WebApplicationFactory _factory; public AuthEndpointTests(WebApplicationFactory factory) { Environment.SetEnvironmentVariable("Keycloak__Authority", Issuer); Environment.SetEnvironmentVariable("Keycloak__ValidIssuer", Issuer); Environment.SetEnvironmentVariable("Keycloak__PublicAuthority", Issuer); Environment.SetEnvironmentVariable("Keycloak__ClientId", ClientId); Environment.SetEnvironmentVariable("Keycloak__DisableHttpsMetadata", "true"); Environment.SetEnvironmentVariable("Keycloak__TestSigningKey", SigningKey); _factory = factory; } [Fact] public async Task SessionEndpoint_WithoutToken_ReturnsUnauthorized() { using var client = _factory.CreateClient(); var response = await client.GetAsync("/api/auth/session"); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } [Fact] public async Task SessionEndpoint_WithValidToken_ReturnsRoleWorkspaceAndAuditsSuccess() { using var client = _factory.CreateClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", CreateToken("daniel@example.test", "client-services")); var httpResponse = await client.GetAsync("/api/auth/session"); var auditStore = _factory.Services.GetRequiredService(); Assert.True(httpResponse.IsSuccessStatusCode, string.Join("; ", auditStore.Events.Select(audit => audit.Reason))); var response = await httpResponse.Content.ReadFromJsonAsync(); Assert.NotNull(response); Assert.Equal("daniel@example.test", response.UserName); Assert.Contains("client-services", response.Roles); Assert.Equal("/workspace/client-services", response.WorkspacePath); Assert.Contains(auditStore.Events, audit => audit.EventType == AuthenticationAuditEventType.Success && audit.Subject == "daniel@example.test"); } [Fact] public async Task SessionEndpoint_WithInvalidToken_ReturnsUnauthorizedAndAuditsFailure() { using var client = _factory.CreateClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "invalid.jwt.value"); var response = await client.GetAsync("/api/auth/session"); var auditStore = _factory.Services.GetRequiredService(); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); Assert.Contains(auditStore.Events, audit => audit.EventType == AuthenticationAuditEventType.Failure && audit.Reason.Contains("invalid", StringComparison.OrdinalIgnoreCase)); } private static string CreateToken(string subject, string role) { var credentials = new SigningCredentials( new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SigningKey)), SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: Issuer, audience: ClientId, claims: [ new Claim(JwtRegisteredClaimNames.Sub, subject), new Claim(ClaimTypes.Name, subject), new Claim(ClaimTypes.Role, role), ], expires: DateTime.UtcNow.AddMinutes(10), signingCredentials: credentials); return new JwtSecurityTokenHandler().WriteToken(token); } private sealed class AuthSessionResponse { public string UserName { get; init; } = string.Empty; public string[] Roles { get; init; } = []; public string WorkspacePath { get; init; } = string.Empty; } }