using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using StellaOps.Gateway.WebService.Authorization; using StellaOps.Router.Common.Models; using Xunit; namespace StellaOps.Gateway.WebService.Tests.Authorization; /// /// Tests for . /// public sealed class EffectiveClaimsStoreTests { private readonly EffectiveClaimsStore _store; public EffectiveClaimsStoreTests() { _store = new EffectiveClaimsStore(NullLogger.Instance); } [Fact] public void GetEffectiveClaims_NoClaimsRegistered_ReturnsEmpty() { // Act var claims = _store.GetEffectiveClaims("test-service", "GET", "/api/test"); // Assert claims.Should().BeEmpty(); } [Fact] public void GetEffectiveClaims_MicroserviceClaimsOnly_ReturnsMicroserviceClaims() { // Arrange var endpoint = CreateEndpoint("GET", "/api/test", [ new ClaimRequirement { Type = "scope", Value = "read" } ]); _store.UpdateFromMicroservice("test-service", [endpoint]); // Act var claims = _store.GetEffectiveClaims("test-service", "GET", "/api/test"); // Assert claims.Should().HaveCount(1); claims[0].Type.Should().Be("scope"); claims[0].Value.Should().Be("read"); } [Fact] public void GetEffectiveClaims_AuthorityOverrideExists_ReturnsAuthorityClaims() { // Arrange var endpoint = CreateEndpoint("GET", "/api/test", [ new ClaimRequirement { Type = "scope", Value = "read" } ]); _store.UpdateFromMicroservice("test-service", [endpoint]); var authorityOverrides = new Dictionary> { [EndpointKey.Create("test-service", "GET", "/api/test")] = [ new ClaimRequirement { Type = "role", Value = "admin" } ] }; _store.UpdateFromAuthority(authorityOverrides); // Act var claims = _store.GetEffectiveClaims("test-service", "GET", "/api/test"); // Assert claims.Should().HaveCount(1); claims[0].Type.Should().Be("role"); claims[0].Value.Should().Be("admin"); } [Fact] public void GetEffectiveClaims_AuthorityTakesPrecedence_OverMicroservice() { // Arrange - microservice claims with different requirements var endpoint = CreateEndpoint("POST", "/api/users", [ new ClaimRequirement { Type = "scope", Value = "users:read" }, new ClaimRequirement { Type = "role", Value = "user" } ]); _store.UpdateFromMicroservice("user-service", [endpoint]); // Authority overrides with stricter requirements var authorityOverrides = new Dictionary> { [EndpointKey.Create("user-service", "POST", "/api/users")] = [ new ClaimRequirement { Type = "scope", Value = "users:write" }, new ClaimRequirement { Type = "role", Value = "admin" }, new ClaimRequirement { Type = "department", Value = "IT" } ] }; _store.UpdateFromAuthority(authorityOverrides); // Act var claims = _store.GetEffectiveClaims("user-service", "POST", "/api/users"); // Assert - Authority claims completely replace microservice claims claims.Should().HaveCount(3); claims.Should().Contain(c => c.Type == "scope" && c.Value == "users:write"); claims.Should().Contain(c => c.Type == "role" && c.Value == "admin"); claims.Should().Contain(c => c.Type == "department" && c.Value == "IT"); claims.Should().NotContain(c => c.Value == "users:read"); claims.Should().NotContain(c => c.Value == "user"); } [Fact] public void GetEffectiveClaims_EndpointWithoutAuthority_FallsBackToMicroservice() { // Arrange var endpoints = new[] { CreateEndpoint("GET", "/api/public", [ new ClaimRequirement { Type = "scope", Value = "public" } ]), CreateEndpoint("GET", "/api/private", [ new ClaimRequirement { Type = "scope", Value = "private" } ]) }; _store.UpdateFromMicroservice("test-service", endpoints); // Authority only overrides /api/private var authorityOverrides = new Dictionary> { [EndpointKey.Create("test-service", "GET", "/api/private")] = [ new ClaimRequirement { Type = "role", Value = "admin" } ] }; _store.UpdateFromAuthority(authorityOverrides); // Act var publicClaims = _store.GetEffectiveClaims("test-service", "GET", "/api/public"); var privateClaims = _store.GetEffectiveClaims("test-service", "GET", "/api/private"); // Assert publicClaims.Should().HaveCount(1); publicClaims[0].Type.Should().Be("scope"); publicClaims[0].Value.Should().Be("public"); privateClaims.Should().HaveCount(1); privateClaims[0].Type.Should().Be("role"); privateClaims[0].Value.Should().Be("admin"); } [Fact] public void UpdateFromAuthority_ClearsPreviousAuthorityOverrides() { // Arrange - first Authority update var firstOverrides = new Dictionary> { [EndpointKey.Create("svc", "GET", "/first")] = [ new ClaimRequirement { Type = "claim1", Value = "value1" } ] }; _store.UpdateFromAuthority(firstOverrides); // Second Authority update (different endpoint) var secondOverrides = new Dictionary> { [EndpointKey.Create("svc", "GET", "/second")] = [ new ClaimRequirement { Type = "claim2", Value = "value2" } ] }; _store.UpdateFromAuthority(secondOverrides); // Act var firstClaims = _store.GetEffectiveClaims("svc", "GET", "/first"); var secondClaims = _store.GetEffectiveClaims("svc", "GET", "/second"); // Assert - first override should be gone firstClaims.Should().BeEmpty(); secondClaims.Should().HaveCount(1); secondClaims[0].Type.Should().Be("claim2"); } [Fact] public void UpdateFromMicroservice_EmptyClaims_RemovesFromStore() { // Arrange - first register claims var endpoint = CreateEndpoint("GET", "/api/test", [ new ClaimRequirement { Type = "scope", Value = "read" } ]); _store.UpdateFromMicroservice("test-service", [endpoint]); // Then update with empty claims var emptyEndpoint = CreateEndpoint("GET", "/api/test", []); _store.UpdateFromMicroservice("test-service", [emptyEndpoint]); // Act var claims = _store.GetEffectiveClaims("test-service", "GET", "/api/test"); // Assert claims.Should().BeEmpty(); } [Fact] public void RemoveService_RemovesAllMicroserviceClaimsForService() { // Arrange var endpoints = new[] { CreateEndpoint("GET", "/api/a", [new ClaimRequirement { Type = "scope", Value = "a" }]), CreateEndpoint("GET", "/api/b", [new ClaimRequirement { Type = "scope", Value = "b" }]) }; _store.UpdateFromMicroservice("service-to-remove", endpoints); var otherEndpoint = CreateEndpoint("GET", "/api/other", [ new ClaimRequirement { Type = "scope", Value = "other" } ]); _store.UpdateFromMicroservice("other-service", [otherEndpoint]); // Act _store.RemoveService("service-to-remove"); // Assert _store.GetEffectiveClaims("service-to-remove", "GET", "/api/a").Should().BeEmpty(); _store.GetEffectiveClaims("service-to-remove", "GET", "/api/b").Should().BeEmpty(); _store.GetEffectiveClaims("other-service", "GET", "/api/other").Should().HaveCount(1); } [Fact] public void GetEffectiveClaims_CaseInsensitiveServiceAndPath() { // Arrange var endpoint = CreateEndpoint("GET", "/API/Test", [ new ClaimRequirement { Type = "scope", Value = "read" } ]); _store.UpdateFromMicroservice("Test-Service", [endpoint]); // Act - query with different case var claims = _store.GetEffectiveClaims("TEST-SERVICE", "get", "/api/test"); // Assert claims.Should().HaveCount(1); claims[0].Type.Should().Be("scope"); } [Fact] public void GetEffectiveClaims_ClaimWithNullValue_Matches() { // Arrange - claim that only requires type, any value var endpoint = CreateEndpoint("GET", "/api/test", [ new ClaimRequirement { Type = "authenticated", Value = null } ]); _store.UpdateFromMicroservice("test-service", [endpoint]); // Act var claims = _store.GetEffectiveClaims("test-service", "GET", "/api/test"); // Assert claims.Should().HaveCount(1); claims[0].Type.Should().Be("authenticated"); claims[0].Value.Should().BeNull(); } private static EndpointDescriptor CreateEndpoint( string method, string path, List claims) { return new EndpointDescriptor { ServiceName = "test-service", Version = "1.0.0", Method = method, Path = path, RequiringClaims = claims }; } }