Refactor code structure and optimize performance across multiple modules
This commit is contained in:
@@ -3,11 +3,13 @@ using System.Net;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Auth.Abstractions.Tests;
|
||||
|
||||
public class NetworkMaskMatcherTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_SingleAddress_YieldsHostMask()
|
||||
{
|
||||
var mask = NetworkMask.Parse("192.168.1.42");
|
||||
@@ -17,7 +19,8 @@ public class NetworkMaskMatcherTests
|
||||
Assert.False(mask.Contains(IPAddress.Parse("192.168.1.43")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_Cidr_NormalisesHostBits()
|
||||
{
|
||||
var mask = NetworkMask.Parse("10.0.15.9/20");
|
||||
@@ -27,7 +30,8 @@ public class NetworkMaskMatcherTests
|
||||
Assert.False(mask.Contains(IPAddress.Parse("10.0.32.1")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Contains_ReturnsFalse_ForMismatchedAddressFamily()
|
||||
{
|
||||
var mask = NetworkMask.Parse("192.168.0.0/16");
|
||||
@@ -35,7 +39,8 @@ public class NetworkMaskMatcherTests
|
||||
Assert.False(mask.Contains(IPAddress.IPv6Loopback));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Matcher_AllowsAll_WhenStarProvided()
|
||||
{
|
||||
var matcher = new NetworkMaskMatcher(new[] { "*" });
|
||||
@@ -45,7 +50,8 @@ public class NetworkMaskMatcherTests
|
||||
Assert.True(matcher.IsAllowed(IPAddress.IPv6Loopback));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Matcher_ReturnsFalse_WhenNoMasksConfigured()
|
||||
{
|
||||
var matcher = new NetworkMaskMatcher(Array.Empty<string>());
|
||||
@@ -55,7 +61,8 @@ public class NetworkMaskMatcherTests
|
||||
Assert.False(matcher.IsAllowed(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Matcher_SupportsIpv4AndIpv6Masks()
|
||||
{
|
||||
var matcher = new NetworkMaskMatcher(new[] { "192.168.0.0/24", "::1/128" });
|
||||
@@ -66,7 +73,8 @@ public class NetworkMaskMatcherTests
|
||||
Assert.False(matcher.IsAllowed(IPAddress.IPv6Any));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Matcher_Throws_ForInvalidEntries()
|
||||
{
|
||||
var exception = Assert.Throws<FormatException>(() => new NetworkMaskMatcher(new[] { "invalid-mask" }));
|
||||
|
||||
@@ -4,11 +4,13 @@ using System.Security.Claims;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Auth.Abstractions.Tests;
|
||||
|
||||
public class StellaOpsPrincipalBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizedScopes_AreSortedDeduplicatedLowerCased()
|
||||
{
|
||||
var builder = new StellaOpsPrincipalBuilder()
|
||||
@@ -24,7 +26,8 @@ public class StellaOpsPrincipalBuilderTests
|
||||
builder.Audiences);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_ConstructsClaimsPrincipalWithNormalisedValues()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
@@ -4,11 +4,13 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Auth.Abstractions.Tests;
|
||||
|
||||
public class StellaOpsProblemResultFactoryTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AuthenticationRequired_ReturnsCanonicalProblem()
|
||||
{
|
||||
var result = StellaOpsProblemResultFactory.AuthenticationRequired(instance: "/jobs");
|
||||
@@ -22,7 +24,8 @@ public class StellaOpsProblemResultFactoryTests
|
||||
Assert.Equal(details.Detail, details.Extensions["error_description"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void InvalidToken_UsesProvidedDetail()
|
||||
{
|
||||
var result = StellaOpsProblemResultFactory.InvalidToken("expired refresh token");
|
||||
@@ -33,7 +36,8 @@ public class StellaOpsProblemResultFactoryTests
|
||||
Assert.Equal("invalid_token", details.Extensions["error"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void InsufficientScope_AddsScopeExtensions()
|
||||
{
|
||||
var result = StellaOpsProblemResultFactory.InsufficientScope(
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Auth.Abstractions.Tests;
|
||||
|
||||
#pragma warning disable CS0618
|
||||
|
||||
public class StellaOpsScopesTests
|
||||
{
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(StellaOpsScopes.AdvisoryRead)]
|
||||
[InlineData(StellaOpsScopes.AdvisoryIngest)]
|
||||
[InlineData(StellaOpsScopes.AdvisoryAiView)]
|
||||
@@ -73,7 +75,8 @@ public class StellaOpsScopesTests
|
||||
Assert.Contains(scope, StellaOpsScopes.All);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("Advisory:Read", StellaOpsScopes.AdvisoryRead)]
|
||||
[InlineData(" VEX:Ingest ", StellaOpsScopes.VexIngest)]
|
||||
[InlineData("AOC:VERIFY", StellaOpsScopes.AocVerify)]
|
||||
|
||||
@@ -564,6 +564,11 @@ public static class StellaOpsScopes
|
||||
/// </summary>
|
||||
public const string ExceptionsWrite = "exceptions:write";
|
||||
|
||||
/// <summary>
|
||||
/// Scope granting permission to request exceptions (initiate approval workflow).
|
||||
/// </summary>
|
||||
public const string ExceptionsRequest = "exceptions:request";
|
||||
|
||||
/// <summary>
|
||||
/// Scope granting administrative control over Graph resources.
|
||||
/// </summary>
|
||||
@@ -684,6 +689,7 @@ public static class StellaOpsScopes
|
||||
ZastavaAdmin,
|
||||
ExceptionsRead,
|
||||
ExceptionsWrite,
|
||||
ExceptionsRequest,
|
||||
GraphAdmin
|
||||
};
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ namespace StellaOps.Auth.Client.Tests;
|
||||
|
||||
public class ServiceCollectionExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AddStellaOpsAuthClient_ConfiguresRetryPolicy()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
@@ -75,7 +76,8 @@ public class ServiceCollectionExtensionsTests
|
||||
Assert.Contains(recordedHandlers, handler => handler.GetType().Name.Contains("PolicyHttpMessageHandler", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureEgressAllowed_InvokesPolicyWhenAuthorityProvided()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
@@ -131,7 +133,8 @@ public class ServiceCollectionExtensionsTests
|
||||
=> responder(request, cancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AddStellaOpsApiAuthentication_AttachesPatAndTenantHeader()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
@@ -177,7 +180,8 @@ public class ServiceCollectionExtensionsTests
|
||||
Assert.Equal(0, tokenClient.RequestCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AddStellaOpsApiAuthentication_UsesClientCredentialsWithCaching()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
@@ -210,6 +214,7 @@ public class ServiceCollectionExtensionsTests
|
||||
});
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
using StellaOps.TestKit;
|
||||
var client = provider.GetRequiredService<IHttpClientFactory>().CreateClient("notify");
|
||||
|
||||
await client.GetAsync("https://notify.example/api");
|
||||
|
||||
@@ -2,11 +2,13 @@ using System;
|
||||
using StellaOps.Auth.Client;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Auth.Client.Tests;
|
||||
|
||||
public class StellaOpsAuthClientOptionsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_NormalizesScopes()
|
||||
{
|
||||
var options = new StellaOpsAuthClientOptions
|
||||
@@ -26,7 +28,8 @@ public class StellaOpsAuthClientOptionsTests
|
||||
Assert.Equal<TimeSpan>(options.RetryDelays, options.NormalizedRetryDelays);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Throws_When_AuthorityMissing()
|
||||
{
|
||||
var options = new StellaOpsAuthClientOptions();
|
||||
@@ -36,7 +39,8 @@ public class StellaOpsAuthClientOptionsTests
|
||||
Assert.Contains("Authority", exception.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_NormalizesRetryDelays()
|
||||
{
|
||||
var options = new StellaOpsAuthClientOptions
|
||||
@@ -54,7 +58,8 @@ public class StellaOpsAuthClientOptionsTests
|
||||
Assert.Equal<TimeSpan>(options.NormalizedRetryDelays, options.RetryDelays);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_DisabledRetries_ProducesEmptyDelays()
|
||||
{
|
||||
var options = new StellaOpsAuthClientOptions
|
||||
@@ -68,7 +73,8 @@ public class StellaOpsAuthClientOptionsTests
|
||||
Assert.Empty(options.NormalizedRetryDelays);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Throws_When_OfflineToleranceNegative()
|
||||
{
|
||||
var options = new StellaOpsAuthClientOptions
|
||||
|
||||
@@ -10,11 +10,13 @@ using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Auth.Client;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Auth.Client.Tests;
|
||||
|
||||
public class StellaOpsDiscoveryCacheTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetAsync_UsesOfflineFallbackWithinTolerance()
|
||||
{
|
||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-01-01T00:00:00Z"));
|
||||
|
||||
@@ -18,6 +18,7 @@ using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Auth.Client;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Auth.Client.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -31,7 +32,8 @@ public class StellaOpsTokenClientTests
|
||||
{
|
||||
#region Task 1: Token Issuance Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RequestPasswordToken_ReturnsResultAndCaches()
|
||||
{
|
||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-02-01T00:00:00Z"));
|
||||
@@ -76,7 +78,8 @@ public class StellaOpsTokenClientTests
|
||||
Assert.Empty(jwks.Keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RequestClientCredentialsToken_ReturnsTokenWithCorrectExpiry()
|
||||
{
|
||||
// Arrange
|
||||
@@ -121,7 +124,8 @@ public class StellaOpsTokenClientTests
|
||||
Assert.Equal(expectedExpiry, result.ExpiresAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RequestClientCredentialsToken_WithCustomScope_UsesCustomScope()
|
||||
{
|
||||
// Arrange
|
||||
@@ -160,7 +164,8 @@ public class StellaOpsTokenClientTests
|
||||
Assert.Contains("policy.evaluate", result.Scopes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RequestClientCredentialsToken_WithoutClientId_ThrowsInvalidOperation()
|
||||
{
|
||||
// Arrange
|
||||
@@ -186,7 +191,8 @@ public class StellaOpsTokenClientTests
|
||||
client.RequestClientCredentialsTokenAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RequestPasswordToken_WithAdditionalParameters_IncludesParameters()
|
||||
{
|
||||
// Arrange
|
||||
@@ -237,7 +243,8 @@ public class StellaOpsTokenClientTests
|
||||
|
||||
#region Task 2: Token Validation/Rejection Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RequestPasswordToken_WhenServerReturnsError_ThrowsInvalidOperation()
|
||||
{
|
||||
// Arrange
|
||||
@@ -278,7 +285,8 @@ public class StellaOpsTokenClientTests
|
||||
Assert.Contains("401", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RequestPasswordToken_WhenResponseMissingAccessToken_ThrowsInvalidOperation()
|
||||
{
|
||||
// Arrange
|
||||
@@ -313,7 +321,8 @@ public class StellaOpsTokenClientTests
|
||||
Assert.Contains("access_token", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CachedToken_WhenExpired_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -338,7 +347,8 @@ public class StellaOpsTokenClientTests
|
||||
// The cache may have already evicted it or it won't be returned
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RequestPasswordToken_DefaultsToBearer_WhenTokenTypeNotProvided()
|
||||
{
|
||||
// Arrange
|
||||
@@ -375,7 +385,8 @@ public class StellaOpsTokenClientTests
|
||||
Assert.Equal("Bearer", result.TokenType); // Defaults to Bearer
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RequestPasswordToken_DefaultsTo3600ExpiresIn_WhenNotProvided()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -6,11 +6,13 @@ using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Auth.Client;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Auth.Client.Tests;
|
||||
|
||||
public class TokenCacheTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task InMemoryTokenCache_ExpiresEntries()
|
||||
{
|
||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-01-01T00:00:00Z"));
|
||||
@@ -28,7 +30,8 @@ public class TokenCacheTests
|
||||
Assert.Null(retrieved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FileTokenCache_PersistsEntries()
|
||||
{
|
||||
var directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
|
||||
|
||||
@@ -12,7 +12,8 @@ namespace StellaOps.Auth.ServerIntegration.Tests;
|
||||
|
||||
public class ServiceCollectionExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddStellaOpsResourceServerAuthentication_ConfiguresJwtBearer()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
@@ -31,6 +32,7 @@ public class ServiceCollectionExtensionsTests
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
using StellaOps.TestKit;
|
||||
var resourceOptions = provider.GetRequiredService<IOptionsMonitor<StellaOpsResourceServerOptions>>().CurrentValue;
|
||||
var jwtOptions = provider.GetRequiredService<IOptionsMonitor<JwtBearerOptions>>().Get(StellaOpsAuthenticationDefaults.AuthenticationScheme);
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@ using System.Net;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Auth.ServerIntegration.Tests;
|
||||
|
||||
public class StellaOpsResourceServerOptionsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_NormalisesCollections()
|
||||
{
|
||||
var options = new StellaOpsResourceServerOptions
|
||||
@@ -43,7 +45,8 @@ public class StellaOpsResourceServerOptionsTests
|
||||
Assert.True(options.BypassMatcher.IsAllowed(IPAddress.IPv6Loopback));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Throws_When_AuthorityMissing()
|
||||
{
|
||||
var options = new StellaOpsResourceServerOptions();
|
||||
|
||||
@@ -4,11 +4,13 @@ using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Auth.ServerIntegration.Tests;
|
||||
|
||||
public class StellaOpsResourceServerPoliciesTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddObservabilityResourcePolicies_RegistersExpectedPolicies()
|
||||
{
|
||||
var options = new AuthorizationOptions();
|
||||
@@ -28,7 +30,8 @@ public class StellaOpsResourceServerPoliciesTests
|
||||
AssertPolicy(options, StellaOpsResourceServerPolicies.ExportAdmin, StellaOpsScopes.ExportAdmin);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddPacksResourcePolicies_RegistersExpectedPolicies()
|
||||
{
|
||||
var options = new AuthorizationOptions();
|
||||
|
||||
@@ -16,11 +16,13 @@ using StellaOps.Cryptography.Audit;
|
||||
using OpenIddict.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Auth.ServerIntegration.Tests;
|
||||
|
||||
public class StellaOpsScopeAuthorizationHandlerTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HandleRequirement_Succeeds_WhenScopePresent()
|
||||
{
|
||||
var optionsMonitor = CreateOptionsMonitor(options =>
|
||||
@@ -52,7 +54,8 @@ public class StellaOpsScopeAuthorizationHandlerTests
|
||||
Assert.False(string.IsNullOrWhiteSpace(record.CorrelationId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HandleRequirement_Fails_WhenTenantMismatch()
|
||||
{
|
||||
var optionsMonitor = CreateOptionsMonitor(options =>
|
||||
@@ -83,7 +86,8 @@ public class StellaOpsScopeAuthorizationHandlerTests
|
||||
Assert.Equal("true", GetPropertyValue(record, "resource.tenant.mismatch"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HandleRequirement_Succeeds_WhenBypassNetworkMatches()
|
||||
{
|
||||
var optionsMonitor = CreateOptionsMonitor(options =>
|
||||
@@ -107,7 +111,8 @@ public class StellaOpsScopeAuthorizationHandlerTests
|
||||
Assert.Equal("true", GetPropertyValue(record, "resource.authorization.bypass"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HandleRequirement_Fails_WhenScopeMissingAndNoBypass()
|
||||
{
|
||||
var optionsMonitor = CreateOptionsMonitor(options =>
|
||||
@@ -130,7 +135,8 @@ public class StellaOpsScopeAuthorizationHandlerTests
|
||||
Assert.Equal("false", GetPropertyValue(record, "principal.authenticated"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HandleRequirement_Fails_WhenDefaultScopeMissing()
|
||||
{
|
||||
var optionsMonitor = CreateOptionsMonitor(options =>
|
||||
@@ -159,7 +165,8 @@ public class StellaOpsScopeAuthorizationHandlerTests
|
||||
Assert.Equal(StellaOpsScopes.PolicyRun, GetPropertyValue(record, "resource.scopes.missing"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HandleRequirement_Succeeds_WhenDefaultScopePresent()
|
||||
{
|
||||
var optionsMonitor = CreateOptionsMonitor(options =>
|
||||
@@ -187,7 +194,8 @@ public class StellaOpsScopeAuthorizationHandlerTests
|
||||
Assert.Equal("true", GetPropertyValue(record, "principal.authenticated"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HandleRequirement_Fails_WhenIncidentAuthTimeMissing()
|
||||
{
|
||||
var optionsMonitor = CreateOptionsMonitor(options =>
|
||||
@@ -220,7 +228,8 @@ public class StellaOpsScopeAuthorizationHandlerTests
|
||||
Assert.Equal("Sev1 drill", GetPropertyValue(record, "incident.reason"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HandleRequirement_Fails_WhenIncidentAuthTimeStale()
|
||||
{
|
||||
var optionsMonitor = CreateOptionsMonitor(options =>
|
||||
@@ -256,7 +265,8 @@ public class StellaOpsScopeAuthorizationHandlerTests
|
||||
Assert.Equal("Sev1 drill", GetPropertyValue(record, "incident.reason"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HandleRequirement_Succeeds_WhenIncidentFreshAuthValid()
|
||||
{
|
||||
var optionsMonitor = CreateOptionsMonitor(options =>
|
||||
@@ -291,7 +301,8 @@ public class StellaOpsScopeAuthorizationHandlerTests
|
||||
Assert.Equal("Sev1 drill", GetPropertyValue(record, "incident.reason"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HandleRequirement_Fails_WhenBackfillMetadataMissing()
|
||||
{
|
||||
var optionsMonitor = CreateOptionsMonitor(options =>
|
||||
@@ -321,7 +332,8 @@ public class StellaOpsScopeAuthorizationHandlerTests
|
||||
Assert.Equal("false", GetPropertyValue(record, "backfill.metadata_satisfied"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HandleRequirement_Succeeds_WhenBackfillMetadataPresent()
|
||||
{
|
||||
var optionsMonitor = CreateOptionsMonitor(options =>
|
||||
@@ -354,7 +366,8 @@ public class StellaOpsScopeAuthorizationHandlerTests
|
||||
Assert.Equal("INC-741", GetPropertyValue(record, "backfill.ticket"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HandleRequirement_Fails_WhenPackApprovalMetadataMissing()
|
||||
{
|
||||
var optionsMonitor = CreateOptionsMonitor(options =>
|
||||
@@ -385,7 +398,8 @@ public class StellaOpsScopeAuthorizationHandlerTests
|
||||
Assert.Equal(StellaOpsScopes.PacksApprove, Assert.Single(record.Scopes));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HandleRequirement_Fails_WhenPackApprovalFreshAuthStale()
|
||||
{
|
||||
var optionsMonitor = CreateOptionsMonitor(options =>
|
||||
@@ -421,7 +435,8 @@ public class StellaOpsScopeAuthorizationHandlerTests
|
||||
Assert.Equal(StellaOpsScopes.PacksApprove, Assert.Single(record.Scopes));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HandleRequirement_Succeeds_WhenPackApprovalMetadataPresent()
|
||||
{
|
||||
var optionsMonitor = CreateOptionsMonitor(options =>
|
||||
|
||||
@@ -7,6 +7,7 @@ using Microsoft.Extensions.Options;
|
||||
using StellaOps.Authority.Plugins.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Authority.Plugin.Ldap.Tests;
|
||||
|
||||
public class LdapPluginOptionsTests : IDisposable
|
||||
@@ -19,7 +20,8 @@ public class LdapPluginOptionsTests : IDisposable
|
||||
Directory.CreateDirectory(tempRoot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Normalize_ResolvesRelativeClientCertificateAndBundlePaths()
|
||||
{
|
||||
var configPath = Path.Combine(tempRoot, "ldap.yaml");
|
||||
@@ -53,7 +55,8 @@ public class LdapPluginOptionsTests : IDisposable
|
||||
Assert.Equal(expectedBundle, options.Connection.TrustStore.BundlePath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Throws_WhenHostMissing()
|
||||
{
|
||||
var options = new LdapPluginOptions
|
||||
@@ -70,7 +73,8 @@ public class LdapPluginOptionsTests : IDisposable
|
||||
Assert.Contains("connection.host", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Throws_WhenBundleModeWithoutPath()
|
||||
{
|
||||
var options = new LdapPluginOptions
|
||||
@@ -95,7 +99,8 @@ public class LdapPluginOptionsTests : IDisposable
|
||||
Assert.Contains("connection.trustStore.bundlePath", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Throws_WhenClientCertificateIncomplete()
|
||||
{
|
||||
var options = new LdapPluginOptions
|
||||
@@ -119,7 +124,8 @@ public class LdapPluginOptionsTests : IDisposable
|
||||
Assert.Contains("clientCertificate.pfxPath", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Throws_WhenTlsDisabledWithoutEnvToggle()
|
||||
{
|
||||
var options = ValidOptions();
|
||||
@@ -132,7 +138,8 @@ public class LdapPluginOptionsTests : IDisposable
|
||||
Assert.Contains("allowInsecureWithEnvToggle", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Throws_WhenTlsDisabledWithoutEnvironmentVariable()
|
||||
{
|
||||
var options = ValidOptions();
|
||||
@@ -145,7 +152,8 @@ public class LdapPluginOptionsTests : IDisposable
|
||||
Assert.Contains(LdapSecurityOptions.AllowInsecureEnvironmentVariable, ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_AllowsTlsDisabledWhenEnvToggleSet()
|
||||
{
|
||||
const string envVar = "STELLAOPS_LDAP_ALLOW_INSECURE";
|
||||
@@ -167,7 +175,8 @@ public class LdapPluginOptionsTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Throws_WhenRequireTlsWithoutTlsConfiguration()
|
||||
{
|
||||
var options = ValidOptions();
|
||||
@@ -182,7 +191,8 @@ public class LdapPluginOptionsTests : IDisposable
|
||||
Assert.Contains("requires TLS", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_AllowsRequireTlsWithStartTls()
|
||||
{
|
||||
var options = ValidOptions();
|
||||
@@ -195,7 +205,8 @@ public class LdapPluginOptionsTests : IDisposable
|
||||
options.Validate("corp-ldap");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Throws_WhenRequireClientCertificateWithoutConfiguration()
|
||||
{
|
||||
var options = ValidOptions();
|
||||
@@ -207,7 +218,8 @@ public class LdapPluginOptionsTests : IDisposable
|
||||
Assert.Contains("requireClientCertificate", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Normalize_ParsesLdapsSchemeAndSetsPort()
|
||||
{
|
||||
var options = ValidOptions();
|
||||
@@ -220,7 +232,8 @@ public class LdapPluginOptionsTests : IDisposable
|
||||
Assert.Equal(1636, options.Connection.Port);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Normalize_DeduplicatesCipherSuites()
|
||||
{
|
||||
var options = ValidOptions();
|
||||
@@ -234,7 +247,8 @@ public class LdapPluginOptionsTests : IDisposable
|
||||
item => Assert.Equal("TLS_AES_128_GCM_SHA256", item));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Registrar_BindsOptionsAndAppliesNormalization()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
@@ -283,7 +297,8 @@ public class LdapPluginOptionsTests : IDisposable
|
||||
Assert.Equal("TLS_AES_256_GCM_SHA384", Assert.Single(options.Security.AllowedCipherSuites));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Normalize_TrimsClaimsConfiguration()
|
||||
{
|
||||
var options = ValidOptions();
|
||||
@@ -317,7 +332,8 @@ public class LdapPluginOptionsTests : IDisposable
|
||||
Assert.Equal(0, options.Claims.Cache.MaxEntries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_AllowsClaimsCacheWithoutExplicitCollection()
|
||||
{
|
||||
var options = ValidOptions();
|
||||
@@ -330,7 +346,8 @@ public class LdapPluginOptionsTests : IDisposable
|
||||
Assert.Equal("ldap_claims_cache_corp-ldap", options.Claims.Cache.ResolveCollectionName("corp-ldap"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Normalize_ClientProvisioningOptions()
|
||||
{
|
||||
var options = ValidOptions();
|
||||
@@ -346,7 +363,8 @@ public class LdapPluginOptionsTests : IDisposable
|
||||
Assert.Equal("audit_log", options.ClientProvisioning.AuditMirror.CollectionName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Throws_WhenClientProvisioningMissingContainer()
|
||||
{
|
||||
var options = ValidOptions();
|
||||
|
||||
@@ -10,11 +10,13 @@ using StellaOps.Authority.Storage.Documents;
|
||||
using StellaOps.Authority.Storage.InMemory.Stores;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Authority.Plugin.Standard.Tests;
|
||||
|
||||
public class StandardClientProvisioningStoreTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateOrUpdateAsync_HashesSecretAndPersistsDocument()
|
||||
{
|
||||
var store = new TrackingClientStore();
|
||||
@@ -45,7 +47,8 @@ public class StandardClientProvisioningStoreTests
|
||||
Assert.Contains("scopea", descriptor.AllowedScopes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateOrUpdateAsync_NormalisesTenant()
|
||||
{
|
||||
var store = new TrackingClientStore();
|
||||
@@ -71,7 +74,8 @@ public class StandardClientProvisioningStoreTests
|
||||
Assert.NotNull(descriptor);
|
||||
Assert.Equal("tenant-alpha", descriptor!.Tenant);
|
||||
}
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateOrUpdateAsync_StoresAudiences()
|
||||
{
|
||||
var store = new TrackingClientStore();
|
||||
@@ -99,7 +103,8 @@ public class StandardClientProvisioningStoreTests
|
||||
Assert.Equal(new[] { "attestor", "signer" }, descriptor!.AllowedAudiences.OrderBy(value => value, StringComparer.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateOrUpdateAsync_MapsCertificateBindings()
|
||||
{
|
||||
var store = new TrackingClientStore();
|
||||
|
||||
@@ -3,11 +3,13 @@ using System.IO;
|
||||
using StellaOps.Authority.Plugin.Standard;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Authority.Plugin.Standard.Tests;
|
||||
|
||||
public class StandardPluginOptionsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_AllowsBootstrapWhenCredentialsProvided()
|
||||
{
|
||||
var options = new StandardPluginOptions
|
||||
@@ -23,7 +25,8 @@ public class StandardPluginOptionsTests
|
||||
options.Validate("standard");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Throws_WhenBootstrapUserIncomplete()
|
||||
{
|
||||
var options = new StandardPluginOptions
|
||||
@@ -39,7 +42,8 @@ public class StandardPluginOptionsTests
|
||||
Assert.Contains("bootstrapUser", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Throws_WhenLockoutWindowMinutesInvalid()
|
||||
{
|
||||
var options = new StandardPluginOptions
|
||||
@@ -56,7 +60,8 @@ public class StandardPluginOptionsTests
|
||||
Assert.Contains("lockout.windowMinutes", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Normalize_ResolvesRelativeTokenSigningDirectory()
|
||||
{
|
||||
var configDir = Path.Combine(Path.GetTempPath(), "stellaops-standard-plugin", Guid.NewGuid().ToString("N"));
|
||||
@@ -84,7 +89,8 @@ public class StandardPluginOptionsTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Normalize_PreservesAbsoluteTokenSigningDirectory()
|
||||
{
|
||||
var absolute = Path.Combine(Path.GetTempPath(), "stellaops-standard-plugin", Guid.NewGuid().ToString("N"), "keys");
|
||||
@@ -98,7 +104,8 @@ public class StandardPluginOptionsTests
|
||||
Assert.Equal(Path.GetFullPath(absolute), options.TokenSigning.KeyDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Throws_WhenPasswordHashingMemoryInvalid()
|
||||
{
|
||||
var options = new StandardPluginOptions
|
||||
@@ -113,7 +120,8 @@ public class StandardPluginOptionsTests
|
||||
Assert.Contains("memory", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Throws_WhenPasswordHashingIterationsInvalid()
|
||||
{
|
||||
var options = new StandardPluginOptions
|
||||
@@ -128,7 +136,8 @@ public class StandardPluginOptionsTests
|
||||
Assert.Contains("iteration", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_Throws_WhenPasswordHashingParallelismInvalid()
|
||||
{
|
||||
var options = new StandardPluginOptions
|
||||
|
||||
@@ -21,7 +21,8 @@ namespace StellaOps.Authority.Plugin.Standard.Tests;
|
||||
|
||||
public class StandardPluginRegistrarTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Register_ConfiguresIdentityProviderAndSeedsBootstrapUser()
|
||||
{
|
||||
var client = new InMemoryClient();
|
||||
@@ -83,7 +84,8 @@ public class StandardPluginRegistrarTests
|
||||
Assert.True(verification.User?.RequiresPasswordReset);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Register_LogsWarning_WhenPasswordPolicyWeaker()
|
||||
{
|
||||
var client = new InMemoryClient();
|
||||
@@ -128,7 +130,8 @@ public class StandardPluginRegistrarTests
|
||||
entry.Message.Contains("weaker password policy", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Register_ForcesPasswordCapability_WhenManifestMissing()
|
||||
{
|
||||
var client = new InMemoryClient();
|
||||
@@ -160,7 +163,8 @@ public class StandardPluginRegistrarTests
|
||||
Assert.True(plugin.Capabilities.SupportsClientProvisioning);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Register_Throws_WhenBootstrapConfigurationIncomplete()
|
||||
{
|
||||
var client = new InMemoryClient();
|
||||
@@ -194,7 +198,8 @@ public class StandardPluginRegistrarTests
|
||||
Assert.Throws<InvalidOperationException>(() => scope.ServiceProvider.GetRequiredService<IIdentityProviderPlugin>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Register_NormalizesTokenSigningKeyDirectory()
|
||||
{
|
||||
var client = new InMemoryClient();
|
||||
@@ -231,6 +236,7 @@ public class StandardPluginRegistrarTests
|
||||
registrar.Register(new AuthorityPluginRegistrationContext(services, pluginContext, configuration));
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
using StellaOps.TestKit;
|
||||
var optionsMonitor = provider.GetRequiredService<IOptionsMonitor<StandardPluginOptions>>();
|
||||
var options = optionsMonitor.Get("standard");
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ using StellaOps.Authority.Plugin.Standard.Storage;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography.Audit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Authority.Plugin.Standard.Tests;
|
||||
|
||||
public class StandardUserCredentialStoreTests : IAsyncLifetime
|
||||
@@ -60,7 +61,8 @@ public class StandardUserCredentialStoreTests : IAsyncLifetime
|
||||
NullLogger<StandardUserCredentialStore>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyPasswordAsync_ReturnsSuccess_ForValidCredentials()
|
||||
{
|
||||
auditLogger.Reset();
|
||||
@@ -87,7 +89,8 @@ public class StandardUserCredentialStoreTests : IAsyncLifetime
|
||||
Assert.Null(auditEntry.FailureCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyPasswordAsync_EnforcesLockout_AfterRepeatedFailures()
|
||||
{
|
||||
auditLogger.Reset();
|
||||
@@ -135,7 +138,8 @@ public class StandardUserCredentialStoreTests : IAsyncLifetime
|
||||
Assert.Contains(lastAudit.Properties, property => property.Name == "plugin.retry_after_seconds");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyPasswordAsync_RehashesLegacyHashesToArgon2()
|
||||
{
|
||||
auditLogger.Reset();
|
||||
@@ -179,7 +183,8 @@ public class StandardUserCredentialStoreTests : IAsyncLifetime
|
||||
Assert.StartsWith("$argon2id$", updated!.PasswordHash, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyPasswordAsync_RecordsAudit_ForUnknownUser()
|
||||
{
|
||||
auditLogger.Reset();
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
using System;
|
||||
using StellaOps.Authority.Plugins.Abstractions;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Authority.Plugins.Abstractions.Tests;
|
||||
|
||||
public class AuthorityClientRegistrationTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_Throws_WhenClientIdMissing()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new AuthorityClientRegistration(string.Empty, false, null, null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_RequiresSecret_ForConfidentialClients()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new AuthorityClientRegistration("cli", true, null, null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void WithClientSecret_ReturnsCopy()
|
||||
{
|
||||
var registration = new AuthorityClientRegistration("cli", false, null, null, tenant: "Tenant-Alpha");
|
||||
|
||||
@@ -2,11 +2,13 @@ using System;
|
||||
using StellaOps.Authority.Plugins.Abstractions;
|
||||
using StellaOps.Cryptography.Audit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Authority.Plugins.Abstractions.Tests;
|
||||
|
||||
public class AuthorityCredentialVerificationResultTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Success_SetsUserAndClearsFailure()
|
||||
{
|
||||
var user = new AuthorityUserDescriptor("subject-1", "user", "User", false);
|
||||
@@ -25,13 +27,15 @@ public class AuthorityCredentialVerificationResultTests
|
||||
Assert.Collection(result.AuditProperties, property => Assert.Equal("test", property.Name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Success_Throws_WhenUserNull()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => AuthorityCredentialVerificationResult.Success(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Failure_SetsFailureCode()
|
||||
{
|
||||
var auditProperties = new[]
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using System;
|
||||
using StellaOps.Authority.Plugins.Abstractions;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Authority.Plugins.Abstractions.Tests;
|
||||
|
||||
public class AuthorityIdentityProviderCapabilitiesTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FromCapabilities_SetsFlags_WhenTokensPresent()
|
||||
{
|
||||
var capabilities = AuthorityIdentityProviderCapabilities.FromCapabilities(new[]
|
||||
@@ -22,7 +24,8 @@ public class AuthorityIdentityProviderCapabilitiesTests
|
||||
Assert.True(capabilities.SupportsBootstrap);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FromCapabilities_DefaultsToFalse_WhenEmpty()
|
||||
{
|
||||
var capabilities = AuthorityIdentityProviderCapabilities.FromCapabilities(Array.Empty<string>());
|
||||
@@ -33,7 +36,8 @@ public class AuthorityIdentityProviderCapabilitiesTests
|
||||
Assert.False(capabilities.SupportsBootstrap);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FromCapabilities_IgnoresNullSet()
|
||||
{
|
||||
var capabilities = AuthorityIdentityProviderCapabilities.FromCapabilities(null!);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using StellaOps.Authority.Plugins.Abstractions;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Authority.Plugins.Abstractions.Tests;
|
||||
|
||||
public class AuthorityPluginHealthResultTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Healthy_ReturnsHealthyStatus()
|
||||
{
|
||||
var result = AuthorityPluginHealthResult.Healthy("ready");
|
||||
@@ -14,7 +16,8 @@ public class AuthorityPluginHealthResultTests
|
||||
Assert.NotNull(result.Details);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Degraded_ReturnsDegradedStatus()
|
||||
{
|
||||
var result = AuthorityPluginHealthResult.Degraded("slow");
|
||||
@@ -22,7 +25,8 @@ public class AuthorityPluginHealthResultTests
|
||||
Assert.Equal(AuthorityPluginHealthStatus.Degraded, result.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Unavailable_ReturnsUnavailableStatus()
|
||||
{
|
||||
var result = AuthorityPluginHealthResult.Unavailable("down");
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using System;
|
||||
using StellaOps.Authority.Plugins.Abstractions;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Authority.Plugins.Abstractions.Tests;
|
||||
|
||||
public class AuthorityPluginOperationResultTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Success_ReturnsSucceededResult()
|
||||
{
|
||||
var result = AuthorityPluginOperationResult.Success("ok");
|
||||
@@ -15,7 +17,8 @@ public class AuthorityPluginOperationResultTests
|
||||
Assert.Equal("ok", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Failure_PopulatesErrorCode()
|
||||
{
|
||||
var result = AuthorityPluginOperationResult.Failure("ERR_CODE", "failure");
|
||||
@@ -25,13 +28,15 @@ public class AuthorityPluginOperationResultTests
|
||||
Assert.Equal("failure", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Failure_Throws_WhenErrorCodeMissing()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => AuthorityPluginOperationResult.Failure(string.Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GenericSuccess_ReturnsValue()
|
||||
{
|
||||
var result = AuthorityPluginOperationResult<string>.Success("value", "created");
|
||||
@@ -41,7 +46,8 @@ public class AuthorityPluginOperationResultTests
|
||||
Assert.Equal("created", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GenericFailure_PopulatesErrorCode()
|
||||
{
|
||||
var result = AuthorityPluginOperationResult<int>.Failure("CONFLICT", "duplicate");
|
||||
@@ -52,7 +58,8 @@ public class AuthorityPluginOperationResultTests
|
||||
Assert.Equal("duplicate", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GenericFailure_Throws_WhenErrorCodeMissing()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => AuthorityPluginOperationResult<string>.Failure(" "));
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
using System;
|
||||
using StellaOps.Authority.Plugins.Abstractions;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Authority.Plugins.Abstractions.Tests;
|
||||
|
||||
public class AuthorityUserDescriptorTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_Throws_WhenSubjectMissing()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new AuthorityUserDescriptor(string.Empty, "user", null, false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_Throws_WhenUsernameMissing()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new AuthorityUserDescriptor("subject", " ", null, false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_MaterialisesCollections()
|
||||
{
|
||||
var descriptor = new AuthorityUserDescriptor("subject", "user", null, false);
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
using System;
|
||||
using StellaOps.Authority.Plugins.Abstractions;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Authority.Plugins.Abstractions.Tests;
|
||||
|
||||
public class AuthorityUserRegistrationTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_Throws_WhenUsernameMissing()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new AuthorityUserRegistration(string.Empty, null, null, null, false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void WithPassword_ReturnsCopyWithPassword()
|
||||
{
|
||||
var registration = new AuthorityUserRegistration("alice", null, "Alice", null, true);
|
||||
|
||||
@@ -59,7 +59,8 @@ public sealed class ApiKeyConcurrencyTests : IAsyncLifetime
|
||||
await _npgsqlDataSource.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ParallelCreates_DifferentIds_All_Succeed()
|
||||
{
|
||||
// Arrange
|
||||
@@ -77,7 +78,8 @@ public sealed class ApiKeyConcurrencyTests : IAsyncLifetime
|
||||
allKeys.Should().HaveCount(parallelCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ConcurrentReads_SameKey_All_Succeed()
|
||||
{
|
||||
// Arrange
|
||||
@@ -97,7 +99,8 @@ public sealed class ApiKeyConcurrencyTests : IAsyncLifetime
|
||||
"all concurrent reads should return same key");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ParallelReadsDuringWrite_ReturnsConsistentState()
|
||||
{
|
||||
// Arrange
|
||||
@@ -124,7 +127,8 @@ public sealed class ApiKeyConcurrencyTests : IAsyncLifetime
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ConcurrentUpdateLastUsed_SameKey_NoConflict()
|
||||
{
|
||||
// Arrange
|
||||
@@ -146,7 +150,8 @@ public sealed class ApiKeyConcurrencyTests : IAsyncLifetime
|
||||
result!.LastUsedAt.Should().NotBeNull("at least one update should have succeeded");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ParallelListOperations_NoDeadlock()
|
||||
{
|
||||
// Arrange - Create some keys first
|
||||
@@ -166,7 +171,8 @@ public sealed class ApiKeyConcurrencyTests : IAsyncLifetime
|
||||
completedInTime.Should().BeTrue("parallel list operations should not deadlock");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MixedOperations_NoDeadlock()
|
||||
{
|
||||
// Arrange
|
||||
@@ -200,7 +206,8 @@ public sealed class ApiKeyConcurrencyTests : IAsyncLifetime
|
||||
completedInTime.Should().BeTrue("mixed operations should not deadlock");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RapidSuccessiveWrites_AllSucceed()
|
||||
{
|
||||
// Arrange
|
||||
@@ -217,7 +224,8 @@ public sealed class ApiKeyConcurrencyTests : IAsyncLifetime
|
||||
allKeys.Should().HaveCount(iterations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ConcurrentDeleteAndRead_ReturnsConsistentState()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -59,7 +59,8 @@ public sealed class ApiKeyIdempotencyTests : IAsyncLifetime
|
||||
await _npgsqlDataSource.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateAsync_SameId_Twice_Should_Not_Duplicate()
|
||||
{
|
||||
// Arrange
|
||||
@@ -88,7 +89,8 @@ public sealed class ApiKeyIdempotencyTests : IAsyncLifetime
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateAsync_DifferentIds_SamePrefix_Should_Not_Duplicate()
|
||||
{
|
||||
// Arrange
|
||||
@@ -116,7 +118,8 @@ public sealed class ApiKeyIdempotencyTests : IAsyncLifetime
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpdateLastUsedAsync_Twice_Should_Be_Idempotent()
|
||||
{
|
||||
// Arrange
|
||||
@@ -138,7 +141,8 @@ public sealed class ApiKeyIdempotencyTests : IAsyncLifetime
|
||||
after2!.Id.Should().Be(key.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RevokeAsync_Twice_Should_Be_Idempotent()
|
||||
{
|
||||
// Arrange
|
||||
@@ -160,7 +164,8 @@ public sealed class ApiKeyIdempotencyTests : IAsyncLifetime
|
||||
after2!.Status.Should().Be(ApiKeyStatus.Revoked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DeleteAsync_Twice_Should_Be_Idempotent()
|
||||
{
|
||||
// Arrange
|
||||
@@ -182,7 +187,8 @@ public sealed class ApiKeyIdempotencyTests : IAsyncLifetime
|
||||
afterSecond.Should().BeNull("second delete should also succeed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateAsync_Multiple_Keys_For_Same_User_Allowed()
|
||||
{
|
||||
// Arrange - Create 5 keys for same user
|
||||
|
||||
@@ -5,6 +5,7 @@ using StellaOps.Authority.Storage.Postgres.Models;
|
||||
using StellaOps.Authority.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Authority.Storage.Postgres.Tests;
|
||||
|
||||
[Collection(AuthorityPostgresCollection.Name)]
|
||||
@@ -32,7 +33,8 @@ public sealed class ApiKeyRepositoryTests : IAsyncLifetime
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateAndGetByPrefix_RoundTripsApiKey()
|
||||
{
|
||||
var keyPrefix = "sk_live_" + Guid.NewGuid().ToString("N")[..8];
|
||||
@@ -59,7 +61,8 @@ public sealed class ApiKeyRepositoryTests : IAsyncLifetime
|
||||
fetched.Scopes.Should().BeEquivalentTo(["scan:read", "scan:write"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetById_ReturnsApiKey()
|
||||
{
|
||||
var apiKey = CreateApiKey(Guid.NewGuid(), "Test Key");
|
||||
@@ -72,7 +75,8 @@ public sealed class ApiKeyRepositoryTests : IAsyncLifetime
|
||||
fetched!.Name.Should().Be("Test Key");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByUserId_ReturnsUserApiKeys()
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
@@ -87,7 +91,8 @@ public sealed class ApiKeyRepositoryTests : IAsyncLifetime
|
||||
keys.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task List_ReturnsAllKeysForTenant()
|
||||
{
|
||||
var key1 = CreateApiKey(Guid.NewGuid(), "Key A");
|
||||
@@ -101,7 +106,8 @@ public sealed class ApiKeyRepositoryTests : IAsyncLifetime
|
||||
keys.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Revoke_UpdatesStatusAndRevokedFields()
|
||||
{
|
||||
var apiKey = CreateApiKey(Guid.NewGuid(), "ToRevoke");
|
||||
@@ -116,7 +122,8 @@ public sealed class ApiKeyRepositoryTests : IAsyncLifetime
|
||||
fetched.RevokedBy.Should().Be("security@test.com");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Delete_RemovesApiKey()
|
||||
{
|
||||
var apiKey = CreateApiKey(Guid.NewGuid(), "DeleteKey");
|
||||
|
||||
@@ -5,6 +5,7 @@ using StellaOps.Authority.Storage.Postgres.Models;
|
||||
using StellaOps.Authority.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Authority.Storage.Postgres.Tests;
|
||||
|
||||
[Collection(AuthorityPostgresCollection.Name)]
|
||||
@@ -27,7 +28,8 @@ public sealed class AuditRepositoryTests : IAsyncLifetime
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Create_ReturnsGeneratedId()
|
||||
{
|
||||
// Arrange
|
||||
@@ -50,7 +52,8 @@ public sealed class AuditRepositoryTests : IAsyncLifetime
|
||||
id.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task List_ReturnsAuditEntriesOrderedByCreatedAtDesc()
|
||||
{
|
||||
// Arrange
|
||||
@@ -68,7 +71,8 @@ public sealed class AuditRepositoryTests : IAsyncLifetime
|
||||
audits[0].Action.Should().Be("action2"); // Most recent first
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByUserId_ReturnsUserAudits()
|
||||
{
|
||||
// Arrange
|
||||
@@ -90,7 +94,8 @@ public sealed class AuditRepositoryTests : IAsyncLifetime
|
||||
audits[0].UserId.Should().Be(userId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByResource_ReturnsResourceAudits()
|
||||
{
|
||||
// Arrange
|
||||
@@ -112,7 +117,8 @@ public sealed class AuditRepositoryTests : IAsyncLifetime
|
||||
audits[0].ResourceId.Should().Be(resourceId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByCorrelationId_ReturnsCorrelatedAudits()
|
||||
{
|
||||
// Arrange
|
||||
@@ -142,7 +148,8 @@ public sealed class AuditRepositoryTests : IAsyncLifetime
|
||||
audits.Should().AllSatisfy(a => a.CorrelationId.Should().Be(correlationId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByAction_ReturnsMatchingAudits()
|
||||
{
|
||||
// Arrange
|
||||
@@ -158,7 +165,8 @@ public sealed class AuditRepositoryTests : IAsyncLifetime
|
||||
audits.Should().AllSatisfy(a => a.Action.Should().Be("user.login"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Create_StoresJsonbValues()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -17,7 +17,8 @@ public sealed class AuthorityMigrationTests
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MigrationsApplied_SchemaHasTables()
|
||||
{
|
||||
// Arrange
|
||||
@@ -47,11 +48,13 @@ public sealed class AuthorityMigrationTests
|
||||
// Add more specific table assertions based on Authority migrations
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MigrationsApplied_SchemaVersionRecorded()
|
||||
{
|
||||
// Arrange
|
||||
await using var connection = new NpgsqlConnection(_fixture.ConnectionString);
|
||||
using StellaOps.TestKit;
|
||||
await connection.OpenAsync();
|
||||
|
||||
// Act - Check schema_migrations table
|
||||
|
||||
@@ -5,6 +5,7 @@ using StellaOps.Authority.Storage.Postgres.Models;
|
||||
using StellaOps.Authority.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Authority.Storage.Postgres.Tests;
|
||||
|
||||
[Collection(AuthorityPostgresCollection.Name)]
|
||||
@@ -26,7 +27,8 @@ public sealed class OfflineKitAuditRepositoryTests : IAsyncLifetime
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Insert_ThenList_ReturnsRecord()
|
||||
{
|
||||
var tenantId = Guid.NewGuid().ToString("N");
|
||||
@@ -52,7 +54,8 @@ public sealed class OfflineKitAuditRepositoryTests : IAsyncLifetime
|
||||
listed[0].Details.Should().Contain("kitFilename");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task List_WithFilters_ReturnsMatchingRows()
|
||||
{
|
||||
var tenantId = Guid.NewGuid().ToString("N");
|
||||
@@ -88,7 +91,8 @@ public sealed class OfflineKitAuditRepositoryTests : IAsyncLifetime
|
||||
validated[0].EventType.Should().Be("IMPORT_VALIDATED");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task List_IsTenantIsolated()
|
||||
{
|
||||
var tenantA = Guid.NewGuid().ToString("N");
|
||||
|
||||
@@ -5,6 +5,7 @@ using StellaOps.Authority.Storage.Postgres.Models;
|
||||
using StellaOps.Authority.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Authority.Storage.Postgres.Tests;
|
||||
|
||||
[Collection(AuthorityPostgresCollection.Name)]
|
||||
@@ -32,7 +33,8 @@ public sealed class PermissionRepositoryTests : IAsyncLifetime
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateAndGet_RoundTripsPermission()
|
||||
{
|
||||
var permission = new PermissionEntity
|
||||
@@ -54,7 +56,8 @@ public sealed class PermissionRepositoryTests : IAsyncLifetime
|
||||
fetched.Action.Should().Be("read");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByName_ReturnsCorrectPermission()
|
||||
{
|
||||
var permission = BuildPermission("tokens:revoke", "tokens", "revoke", "Revoke tokens");
|
||||
@@ -66,7 +69,8 @@ public sealed class PermissionRepositoryTests : IAsyncLifetime
|
||||
fetched!.Action.Should().Be("revoke");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByResource_ReturnsResourcePermissions()
|
||||
{
|
||||
var p1 = BuildPermission("users:read", "users", "read", "Read");
|
||||
@@ -79,7 +83,8 @@ public sealed class PermissionRepositoryTests : IAsyncLifetime
|
||||
perms.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task List_ReturnsAllPermissionsForTenant()
|
||||
{
|
||||
var p1 = BuildPermission("orch:read", "orch", "read", "Read orch");
|
||||
@@ -92,7 +97,8 @@ public sealed class PermissionRepositoryTests : IAsyncLifetime
|
||||
perms.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Delete_RemovesPermission()
|
||||
{
|
||||
var permission = BuildPermission("tokens:revoke", "tokens", "revoke", "Revoke tokens");
|
||||
|
||||
@@ -6,6 +6,7 @@ using StellaOps.Authority.Storage.Postgres.Models;
|
||||
using StellaOps.Authority.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Authority.Storage.Postgres.Tests;
|
||||
|
||||
[Collection(AuthorityPostgresCollection.Name)]
|
||||
@@ -33,7 +34,8 @@ public sealed class RefreshTokenRepositoryTests : IAsyncLifetime
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateAndGetByHash_RoundTripsRefreshToken()
|
||||
{
|
||||
var refresh = BuildToken(Guid.NewGuid());
|
||||
@@ -47,7 +49,8 @@ public sealed class RefreshTokenRepositoryTests : IAsyncLifetime
|
||||
fetched!.Id.Should().Be(refresh.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetById_ReturnsToken()
|
||||
{
|
||||
var refresh = BuildToken(Guid.NewGuid());
|
||||
@@ -61,7 +64,8 @@ public sealed class RefreshTokenRepositoryTests : IAsyncLifetime
|
||||
fetched!.UserId.Should().Be(refresh.UserId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByUserId_ReturnsUserTokens()
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
@@ -77,7 +81,8 @@ public sealed class RefreshTokenRepositoryTests : IAsyncLifetime
|
||||
tokens.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Revoke_SetsRevokedFields()
|
||||
{
|
||||
var refresh = BuildToken(Guid.NewGuid());
|
||||
@@ -92,7 +97,8 @@ public sealed class RefreshTokenRepositoryTests : IAsyncLifetime
|
||||
fetched.RevokedBy.Should().Be("tester");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RevokeByUserId_RevokesAllUserTokens()
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
@@ -111,7 +117,8 @@ public sealed class RefreshTokenRepositoryTests : IAsyncLifetime
|
||||
revoked2!.RevokedAt.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Revoke_WithReplacedBy_SetsReplacedByField()
|
||||
{
|
||||
var refresh = BuildToken(Guid.NewGuid());
|
||||
@@ -126,7 +133,8 @@ public sealed class RefreshTokenRepositoryTests : IAsyncLifetime
|
||||
fetched!.ReplacedBy.Should().Be(newTokenId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByUserId_IsDeterministic_WhenIssuedAtTies()
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
|
||||
@@ -13,6 +13,7 @@ using StellaOps.Authority.Storage.Postgres.Models;
|
||||
using StellaOps.Authority.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Authority.Storage.Postgres.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -56,7 +57,8 @@ public sealed class RoleBasedAccessTests : IAsyncLifetime
|
||||
|
||||
#region User-Role Assignment Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UserWithRole_GetsRolePermissions()
|
||||
{
|
||||
// Arrange
|
||||
@@ -81,7 +83,8 @@ public sealed class RoleBasedAccessTests : IAsyncLifetime
|
||||
userPermissions.Should().Contain(p => p.Resource == "scanner" && p.Action == "view");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UserWithoutRole_HasNoPermissions_DenyByDefault()
|
||||
{
|
||||
// Arrange
|
||||
@@ -101,7 +104,8 @@ public sealed class RoleBasedAccessTests : IAsyncLifetime
|
||||
userPermissions.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UserWithExpiredRole_HasNoPermissions()
|
||||
{
|
||||
// Arrange
|
||||
@@ -124,7 +128,8 @@ public sealed class RoleBasedAccessTests : IAsyncLifetime
|
||||
userPermissions.Should().BeEmpty("expired role should not grant permissions");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UserWithFutureExpiryRole_HasPermissions()
|
||||
{
|
||||
// Arrange
|
||||
@@ -148,7 +153,8 @@ public sealed class RoleBasedAccessTests : IAsyncLifetime
|
||||
userPermissions.Should().Contain(p => p.Resource == "policy" && p.Action == "read");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UserWithNoExpiryRole_HasPermissions()
|
||||
{
|
||||
// Arrange
|
||||
@@ -174,7 +180,8 @@ public sealed class RoleBasedAccessTests : IAsyncLifetime
|
||||
|
||||
#region Multiple Roles Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UserWithMultipleRoles_AccumulatesPermissions()
|
||||
{
|
||||
// Arrange
|
||||
@@ -209,7 +216,8 @@ public sealed class RoleBasedAccessTests : IAsyncLifetime
|
||||
userPermissions.Should().Contain(p => p.Action == "delete");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UserWithOverlappingRolePermissions_GetsDistinctPermissions()
|
||||
{
|
||||
// Arrange
|
||||
@@ -239,7 +247,8 @@ public sealed class RoleBasedAccessTests : IAsyncLifetime
|
||||
userPermissions.Select(p => p.Id).Should().OnlyHaveUniqueItems();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UserWithOneExpiredRole_StillHasOtherRolePermissions()
|
||||
{
|
||||
// Arrange
|
||||
@@ -277,7 +286,8 @@ public sealed class RoleBasedAccessTests : IAsyncLifetime
|
||||
|
||||
#region Role Removal Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RemovingRole_RemovesPermissions()
|
||||
{
|
||||
// Arrange
|
||||
@@ -303,7 +313,8 @@ public sealed class RoleBasedAccessTests : IAsyncLifetime
|
||||
afterRemoval.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RemovingPermissionFromRole_AffectsAllUsersWithRole()
|
||||
{
|
||||
// Arrange
|
||||
@@ -337,7 +348,8 @@ public sealed class RoleBasedAccessTests : IAsyncLifetime
|
||||
|
||||
#region Role Permission Enforcement Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetRolePermissions_ReturnsOnlyAssignedPermissions()
|
||||
{
|
||||
// Arrange
|
||||
@@ -357,7 +369,8 @@ public sealed class RoleBasedAccessTests : IAsyncLifetime
|
||||
rolePermissions.Should().NotContain(p => p.Resource == "notallowed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SystemRole_CanHaveSpecialPermissions()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -5,6 +5,7 @@ using StellaOps.Authority.Storage.Postgres.Models;
|
||||
using StellaOps.Authority.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Authority.Storage.Postgres.Tests;
|
||||
|
||||
[Collection(AuthorityPostgresCollection.Name)]
|
||||
@@ -32,7 +33,8 @@ public sealed class RoleRepositoryTests : IAsyncLifetime
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateAndGet_RoundTripsRole()
|
||||
{
|
||||
var role = BuildRole("Admin");
|
||||
@@ -44,7 +46,8 @@ public sealed class RoleRepositoryTests : IAsyncLifetime
|
||||
fetched!.Name.Should().Be("Admin");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByName_ReturnsCorrectRole()
|
||||
{
|
||||
var role = BuildRole("Reader");
|
||||
@@ -56,7 +59,8 @@ public sealed class RoleRepositoryTests : IAsyncLifetime
|
||||
fetched!.Description.Should().Be("Reader role");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task List_ReturnsAllRolesForTenant()
|
||||
{
|
||||
await _repository.CreateAsync(_tenantId, BuildRole("Reader"));
|
||||
@@ -67,7 +71,8 @@ public sealed class RoleRepositoryTests : IAsyncLifetime
|
||||
roles.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Update_ModifiesRole()
|
||||
{
|
||||
var role = BuildRole("Updater");
|
||||
@@ -92,7 +97,8 @@ public sealed class RoleRepositoryTests : IAsyncLifetime
|
||||
fetched!.Description.Should().Be("Updated description");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Delete_RemovesRole()
|
||||
{
|
||||
var role = BuildRole("Deleter");
|
||||
|
||||
@@ -5,6 +5,7 @@ using StellaOps.Authority.Storage.Postgres.Models;
|
||||
using StellaOps.Authority.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Authority.Storage.Postgres.Tests;
|
||||
|
||||
[Collection(AuthorityPostgresCollection.Name)]
|
||||
@@ -32,7 +33,8 @@ public sealed class SessionRepositoryTests : IAsyncLifetime
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateAndGet_RoundTripsSession()
|
||||
{
|
||||
var session = BuildSession();
|
||||
@@ -45,7 +47,8 @@ public sealed class SessionRepositoryTests : IAsyncLifetime
|
||||
fetched!.Id.Should().Be(session.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByTokenHash_ReturnsSession()
|
||||
{
|
||||
var session = BuildSession();
|
||||
@@ -58,7 +61,8 @@ public sealed class SessionRepositoryTests : IAsyncLifetime
|
||||
fetched!.UserId.Should().Be(session.UserId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EndByUserId_EndsAllUserSessions()
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
|
||||
@@ -6,6 +6,7 @@ using StellaOps.Authority.Storage.Postgres.Models;
|
||||
using StellaOps.Authority.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Authority.Storage.Postgres.Tests;
|
||||
|
||||
[Collection(AuthorityPostgresCollection.Name)]
|
||||
@@ -32,7 +33,8 @@ public sealed class TokenRepositoryTests : IAsyncLifetime
|
||||
}
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateAndGetByHash_RoundTripsToken()
|
||||
{
|
||||
// Arrange
|
||||
@@ -61,7 +63,8 @@ public sealed class TokenRepositoryTests : IAsyncLifetime
|
||||
fetched.Scopes.Should().BeEquivalentTo(["read", "write"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetById_ReturnsToken()
|
||||
{
|
||||
// Arrange
|
||||
@@ -77,7 +80,8 @@ public sealed class TokenRepositoryTests : IAsyncLifetime
|
||||
fetched!.Id.Should().Be(token.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByUserId_ReturnsUserTokens()
|
||||
{
|
||||
// Arrange
|
||||
@@ -95,7 +99,8 @@ public sealed class TokenRepositoryTests : IAsyncLifetime
|
||||
tokens.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Revoke_SetsRevokedFields()
|
||||
{
|
||||
// Arrange
|
||||
@@ -112,7 +117,8 @@ public sealed class TokenRepositoryTests : IAsyncLifetime
|
||||
fetched.RevokedBy.Should().Be("admin@test.com");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RevokeByUserId_RevokesAllUserTokens()
|
||||
{
|
||||
// Arrange
|
||||
@@ -133,7 +139,8 @@ public sealed class TokenRepositoryTests : IAsyncLifetime
|
||||
revoked2!.RevokedAt.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByUserId_IsDeterministic_WhenIssuedAtTies()
|
||||
{
|
||||
// Arrange: same IssuedAt, fixed IDs to validate ordering
|
||||
|
||||
Reference in New Issue
Block a user