Add unit tests for Router configuration and transport layers
- Implemented tests for RouterConfig, RoutingOptions, StaticInstanceConfig, and RouterConfigOptions to ensure default values are set correctly. - Added tests for RouterConfigProvider to validate configurations and ensure defaults are returned when no file is specified. - Created tests for ConfigValidationResult to check success and error scenarios. - Developed tests for ServiceCollectionExtensions to verify service registration for RouterConfig. - Introduced UdpTransportTests to validate serialization, connection, request-response, and error handling in UDP transport. - Added scripts for signing authority gaps and hashing DevPortal SDK snippets.
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="AuthorizationMiddleware"/>.
|
||||
/// </summary>
|
||||
public sealed class AuthorizationMiddlewareTests
|
||||
{
|
||||
private readonly Mock<IEffectiveClaimsStore> _claimsStore;
|
||||
private readonly Mock<RequestDelegate> _next;
|
||||
private readonly AuthorizationMiddleware _middleware;
|
||||
|
||||
public AuthorizationMiddlewareTests()
|
||||
{
|
||||
_claimsStore = new Mock<IEffectiveClaimsStore>();
|
||||
_next = new Mock<RequestDelegate>();
|
||||
_middleware = new AuthorizationMiddleware(
|
||||
_next.Object,
|
||||
_claimsStore.Object,
|
||||
NullLogger<AuthorizationMiddleware>.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<ClaimRequirement>());
|
||||
|
||||
// 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<ClaimRequirement>
|
||||
{
|
||||
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<ClaimRequirement>
|
||||
{
|
||||
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<HttpContext>()), 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<ClaimRequirement>
|
||||
{
|
||||
new() { Type = "role", Value = "admin" }
|
||||
});
|
||||
|
||||
// Act
|
||||
await _middleware.InvokeAsync(context);
|
||||
|
||||
// Assert
|
||||
_next.Verify(n => n(It.IsAny<HttpContext>()), 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<ClaimRequirement>
|
||||
{
|
||||
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<ClaimRequirement>
|
||||
{
|
||||
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<HttpContext>()), 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<ClaimRequirement>
|
||||
{
|
||||
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<ClaimRequirement>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="EffectiveClaimsStore"/>.
|
||||
/// </summary>
|
||||
public sealed class EffectiveClaimsStoreTests
|
||||
{
|
||||
private readonly EffectiveClaimsStore _store;
|
||||
|
||||
public EffectiveClaimsStoreTests()
|
||||
{
|
||||
_store = new EffectiveClaimsStore(NullLogger<EffectiveClaimsStore>.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, IReadOnlyList<ClaimRequirement>>
|
||||
{
|
||||
[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, IReadOnlyList<ClaimRequirement>>
|
||||
{
|
||||
[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, IReadOnlyList<ClaimRequirement>>
|
||||
{
|
||||
[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, IReadOnlyList<ClaimRequirement>>
|
||||
{
|
||||
[EndpointKey.Create("svc", "GET", "/first")] = [
|
||||
new ClaimRequirement { Type = "claim1", Value = "value1" }
|
||||
]
|
||||
};
|
||||
_store.UpdateFromAuthority(firstOverrides);
|
||||
|
||||
// Second Authority update (different endpoint)
|
||||
var secondOverrides = new Dictionary<EndpointKey, IReadOnlyList<ClaimRequirement>>
|
||||
{
|
||||
[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<ClaimRequirement> claims)
|
||||
{
|
||||
return new EndpointDescriptor
|
||||
{
|
||||
ServiceName = "test-service",
|
||||
Version = "1.0.0",
|
||||
Method = method,
|
||||
Path = path,
|
||||
RequiringClaims = claims
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,12 @@
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
Reference in New Issue
Block a user