using System.IdentityModel.Tokens.Jwt; using System.Net; using System.Net.Http.Headers; using System.Security.Claims; using System.Text; using System.Text.Json; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using Xunit; namespace WorkClub.Tests.Integration.Middleware; public class TenantValidationTests : IClassFixture { private readonly CustomWebApplicationFactory _factory; private readonly HttpClient _client; public TenantValidationTests(CustomWebApplicationFactory factory) { _factory = factory; _client = factory.CreateClient(); } [Fact] public async Task Request_WithValidTenantId_Returns200() { // Arrange: Create JWT with clubs claim containing club-1 var clubs = new Dictionary { { "club-1", "admin" } }; var token = CreateTestJwt(clubs); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); _client.DefaultRequestHeaders.Add("X-Tenant-Id", "club-1"); // Act: Make request to test endpoint var response = await _client.GetAsync("/api/test"); // Assert: Request should succeed Assert.Equal(HttpStatusCode.OK, response.StatusCode); } [Fact] public async Task Request_WithNonMemberTenantId_Returns403() { // Arrange: Create JWT with clubs claim (only club-1, not club-2) var clubs = new Dictionary { { "club-1", "admin" } }; var token = CreateTestJwt(clubs); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); _client.DefaultRequestHeaders.Add("X-Tenant-Id", "club-2"); // User not member of club-2 // Act var response = await _client.GetAsync("/api/test"); // Assert: Cross-tenant access should be denied Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); } [Fact] public async Task Request_WithoutTenantIdHeader_Returns400() { // Arrange: Create valid JWT but no X-Tenant-Id header var clubs = new Dictionary { { "club-1", "admin" } }; var token = CreateTestJwt(clubs); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); // No X-Tenant-Id header // Act var response = await _client.GetAsync("/api/test"); // Assert: Missing header should return bad request Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task Request_WithoutAuthentication_Returns401() { // Arrange: No authorization header _client.DefaultRequestHeaders.Add("X-Tenant-Id", "club-1"); // Act var response = await _client.GetAsync("/api/test"); // Assert: Unauthenticated request should be denied Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } private static string CreateTestJwt(Dictionary clubs) { var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("test-secret-key-for-jwt-signing-must-be-at-least-32-chars")); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var claims = new List { new Claim(ClaimTypes.NameIdentifier, "test-user-id"), new Claim(ClaimTypes.Name, "test@test.com"), new Claim("clubs", JsonSerializer.Serialize(clubs)) // JSON object claim }; var token = new JwtSecurityToken( issuer: "test-issuer", audience: "test-audience", claims: claims, expires: DateTime.UtcNow.AddHours(1), signingCredentials: creds ); return new JwtSecurityTokenHandler().WriteToken(token); } } /// /// Custom WebApplicationFactory for integration testing with test authentication. /// public class CustomWebApplicationFactory : WebApplicationFactory { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureTestServices(services => { services.AddAuthentication("TestScheme") .AddScheme("TestScheme", options => { }); services.AddAuthorization(); }); builder.UseEnvironment("Testing"); } } /// /// Test authentication handler that validates JWT tokens without Keycloak. /// public class TestAuthHandler : AuthenticationHandler { public TestAuthHandler( Microsoft.Extensions.Options.IOptionsMonitor options, Microsoft.Extensions.Logging.ILoggerFactory logger, System.Text.Encodings.Web.UrlEncoder encoder) : base(options, logger, encoder) { } protected override Task HandleAuthenticateAsync() { var authHeader = Request.Headers.Authorization.ToString(); if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer ")) { return Task.FromResult(AuthenticateResult.NoResult()); } var token = authHeader.Substring("Bearer ".Length).Trim(); try { var handler = new JwtSecurityTokenHandler(); var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("test-secret-key-for-jwt-signing-must-be-at-least-32-chars")); var validationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = "test-issuer", ValidAudience = "test-audience", IssuerSigningKey = key }; var principal = handler.ValidateToken(token, validationParameters, out _); var ticket = new AuthenticationTicket(principal, "TestScheme"); return Task.FromResult(AuthenticateResult.Success(ticket)); } catch (Exception ex) { return Task.FromResult(AuthenticateResult.Fail(ex.Message)); } } }