using System.Security.Claims; using FluentAssertions; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging.Abstractions; using Moq; using StellaOps.Gateway.WebService.Authorization; using StellaOps.Router.Common; using StellaOps.Router.Common.Models; using Xunit; namespace StellaOps.Gateway.WebService.Tests.Authorization; /// /// Tests for . /// public sealed class AuthorizationMiddlewareTests { private readonly Mock _claimsStore; private readonly Mock _next; private readonly AuthorizationMiddleware _middleware; public AuthorizationMiddlewareTests() { _claimsStore = new Mock(); _next = new Mock(); _middleware = new AuthorizationMiddleware( _next.Object, _claimsStore.Object, NullLogger.Instance); } [Fact] public async Task InvokeAsync_NoEndpointResolved_CallsNext() { // Arrange var context = CreateHttpContext(); // Act await _middleware.InvokeAsync(context); // Assert _next.Verify(n => n(context), Times.Once); } [Fact] public async Task InvokeAsync_NoClaims_CallsNext() { // Arrange var context = CreateHttpContextWithEndpoint(); _claimsStore .Setup(s => s.GetEffectiveClaims("test-service", "GET", "/api/test")) .Returns(Array.Empty()); // Act await _middleware.InvokeAsync(context); // Assert _next.Verify(n => n(context), Times.Once); context.Response.StatusCode.Should().NotBe(403); } [Fact] public async Task InvokeAsync_UserHasRequiredClaims_CallsNext() { // Arrange var context = CreateHttpContextWithEndpoint(new[] { new Claim("scope", "read"), new Claim("role", "user") }); _claimsStore .Setup(s => s.GetEffectiveClaims("test-service", "GET", "/api/test")) .Returns(new List { new() { Type = "scope", Value = "read" }, new() { Type = "role", Value = "user" } }); // Act await _middleware.InvokeAsync(context); // Assert _next.Verify(n => n(context), Times.Once); context.Response.StatusCode.Should().NotBe(403); } [Fact] public async Task InvokeAsync_UserMissingRequiredClaim_Returns403() { // Arrange var context = CreateHttpContextWithEndpoint(new[] { new Claim("scope", "read") }); _claimsStore .Setup(s => s.GetEffectiveClaims("test-service", "GET", "/api/test")) .Returns(new List { new() { Type = "scope", Value = "read" }, new() { Type = "role", Value = "admin" } // User doesn't have this }); // Act await _middleware.InvokeAsync(context); // Assert _next.Verify(n => n(It.IsAny()), Times.Never); context.Response.StatusCode.Should().Be(403); } [Fact] public async Task InvokeAsync_UserHasClaimTypeButWrongValue_Returns403() { // Arrange var context = CreateHttpContextWithEndpoint(new[] { new Claim("role", "user") }); _claimsStore .Setup(s => s.GetEffectiveClaims("test-service", "GET", "/api/test")) .Returns(new List { new() { Type = "role", Value = "admin" } }); // Act await _middleware.InvokeAsync(context); // Assert _next.Verify(n => n(It.IsAny()), Times.Never); context.Response.StatusCode.Should().Be(403); } [Fact] public async Task InvokeAsync_ClaimWithNullValue_MatchesAnyValue() { // Arrange - user has claim of type "authenticated" with some value var context = CreateHttpContextWithEndpoint(new[] { new Claim("authenticated", "true") }); // Requirement only checks that type exists, any value is ok _claimsStore .Setup(s => s.GetEffectiveClaims("test-service", "GET", "/api/test")) .Returns(new List { new() { Type = "authenticated", Value = null } }); // Act await _middleware.InvokeAsync(context); // Assert _next.Verify(n => n(context), Times.Once); } [Fact] public async Task InvokeAsync_MultipleClaims_AllMustMatch() { // Arrange - user has 2 of 3 required claims var context = CreateHttpContextWithEndpoint(new[] { new Claim("scope", "read"), new Claim("role", "user") }); _claimsStore .Setup(s => s.GetEffectiveClaims("test-service", "GET", "/api/test")) .Returns(new List { new() { Type = "scope", Value = "read" }, new() { Type = "role", Value = "user" }, new() { Type = "department", Value = "IT" } // Missing }); // Act await _middleware.InvokeAsync(context); // Assert _next.Verify(n => n(It.IsAny()), Times.Never); context.Response.StatusCode.Should().Be(403); } [Fact] public async Task InvokeAsync_UserHasExtraClaims_StillAuthorized() { // Arrange - user has more claims than required var context = CreateHttpContextWithEndpoint(new[] { new Claim("scope", "read"), new Claim("scope", "write"), new Claim("role", "admin"), new Claim("department", "IT") }); _claimsStore .Setup(s => s.GetEffectiveClaims("test-service", "GET", "/api/test")) .Returns(new List { new() { Type = "scope", Value = "read" } }); // Act await _middleware.InvokeAsync(context); // Assert _next.Verify(n => n(context), Times.Once); } [Fact] public async Task InvokeAsync_ForbiddenResponse_ContainsErrorDetails() { // Arrange var context = CreateHttpContextWithEndpoint(); context.Response.Body = new MemoryStream(); _claimsStore .Setup(s => s.GetEffectiveClaims("test-service", "GET", "/api/test")) .Returns(new List { new() { Type = "admin", Value = "true" } }); // Act await _middleware.InvokeAsync(context); // Assert context.Response.StatusCode.Should().Be(403); context.Response.ContentType.Should().Contain("application/json"); } private static HttpContext CreateHttpContext() { var context = new DefaultHttpContext(); return context; } private static HttpContext CreateHttpContextWithEndpoint(Claim[]? userClaims = null) { var context = new DefaultHttpContext(); // Set resolved endpoint var endpoint = new EndpointDescriptor { ServiceName = "test-service", Version = "1.0.0", Method = "GET", Path = "/api/test" }; context.Items[RouterHttpContextKeys.EndpointDescriptor] = endpoint; // Set user with claims if (userClaims != null) { var identity = new ClaimsIdentity(userClaims, "Test"); context.User = new ClaimsPrincipal(identity); } return context; } }