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
};
}
}