using System.Security.Claims; using Microsoft.Extensions.Options; using MxGateway.Server.Configuration; using MxGateway.Server.Dashboard; using MxGateway.Server.Security.Authentication; using MxGateway.Server.Security.Authorization; namespace MxGateway.Tests.Gateway.Dashboard; public sealed class DashboardAuthenticatorTests { [Fact] public async Task AuthenticateAsync_AdminKey_ReturnsCookiePrincipal() { FakeApiKeyVerifier verifier = new(SuccessWithScopes(GatewayScopes.Admin)); DashboardAuthenticator authenticator = CreateAuthenticator(verifier); DashboardAuthenticationResult result = await authenticator.AuthenticateAsync( "mxgw_operator01_super-secret", CancellationToken.None); Assert.True(result.Succeeded); Assert.NotNull(result.Principal); Assert.Equal("operator01", result.Principal.FindFirst(ClaimTypes.NameIdentifier)?.Value); Assert.Equal("Operator Key", result.Principal.FindFirst(ClaimTypes.Name)?.Value); Assert.Contains(result.Principal.Claims, claim => claim.Type == DashboardAuthenticationDefaults.ScopeClaimType && claim.Value == GatewayScopes.Admin); Assert.Equal("Bearer mxgw_operator01_super-secret", verifier.LastAuthorizationHeader); } [Fact] public async Task AuthenticateAsync_NonAdminKey_ReturnsFailureWithoutRawApiKey() { DashboardAuthenticator authenticator = CreateAuthenticator(new FakeApiKeyVerifier( SuccessWithScopes(GatewayScopes.EventsRead))); DashboardAuthenticationResult result = await authenticator.AuthenticateAsync( "mxgw_operator01_super-secret", CancellationToken.None); Assert.False(result.Succeeded); Assert.Null(result.Principal); Assert.DoesNotContain("super-secret", result.FailureMessage, StringComparison.Ordinal); } [Fact] public async Task AuthenticateAsync_RequireAdminScopeFalse_AllowsAuthenticatedKey() { DashboardAuthenticator authenticator = CreateAuthenticator( new FakeApiKeyVerifier(SuccessWithScopes(GatewayScopes.EventsRead)), requireAdminScope: false); DashboardAuthenticationResult result = await authenticator.AuthenticateAsync( "mxgw_operator01_secret", CancellationToken.None); Assert.True(result.Succeeded); Assert.NotNull(result.Principal); } [Fact] public async Task AuthenticateAsync_InvalidKey_ReturnsGenericFailure() { DashboardAuthenticator authenticator = CreateAuthenticator(new FakeApiKeyVerifier( ApiKeyVerificationResult.Fail(ApiKeyVerificationFailure.SecretMismatch))); DashboardAuthenticationResult result = await authenticator.AuthenticateAsync( "mxgw_operator01_super-secret", CancellationToken.None); Assert.False(result.Succeeded); Assert.DoesNotContain("super-secret", result.FailureMessage, StringComparison.Ordinal); } private static DashboardAuthenticator CreateAuthenticator( IApiKeyVerifier verifier, bool requireAdminScope = true) { return new DashboardAuthenticator( verifier, Options.Create(new GatewayOptions { Dashboard = new DashboardOptions { RequireAdminScope = requireAdminScope } })); } private static ApiKeyVerificationResult SuccessWithScopes(params string[] scopes) { return ApiKeyVerificationResult.Success(new ApiKeyIdentity( KeyId: "operator01", KeyPrefix: "mxgw_operator01", DisplayName: "Operator Key", Scopes: new HashSet(scopes, StringComparer.Ordinal))); } private sealed class FakeApiKeyVerifier(ApiKeyVerificationResult result) : IApiKeyVerifier { public string? LastAuthorizationHeader { get; private set; } public Task VerifyAsync( string? authorizationHeader, CancellationToken cancellationToken) { LastAuthorizationHeader = authorizationHeader; return Task.FromResult(result); } } }