865c22a884
IntegrationTests-022 (Conventions): ResolveRepositoryRoot now throws InvalidOperationException when the walk exhausts without finding a root marker, with a message naming the start directory, the expected markers (src/, .git, *.sln, *.slnx), and the MXGATEWAY_LIVE_MXACCESS_WORKER_EXE escape hatch. Replaces the silent fallback to Directory.GetCurrentDirectory() that previously masked misconfiguration. New regression test ResolveRepositoryRoot_NoMarkers_ThrowsInvalidOperationExceptionNamingStartAndMarkers in IntegrationTestEnvironmentTests asserts the throw and the message contents. TDD red→green confirmed. IntegrationTests-023 (Testing coverage): DashboardLdapLiveTests's AuthenticateAsync_AdminInGwAdminGroup_Succeeds now asserts that the authenticated principal carries a ClaimTypes.Role claim with value DashboardRoles.Admin in addition to the existing LdapGroupClaimType assertion. A regression in MapGroupsToRoles (returning an empty list or missing the RDN fallback) would now surface here. Gated by MXGATEWAY_RUN_LIVE_LDAP_TESTS. IntegrationTests-024 (Conventions): Option (b) — extracted within IntegrationTests. New file TestSupport/NullDashboardEventBroadcaster.cs (public type, private ctor, singleton Instance). The inline class at the bottom of WorkerLiveMxAccessSmokeTests is gone; the file now imports the shared type. Matches the unit-test project's Tests-007 / Tests-021 / Tests-025 pattern while keeping the two test projects independently buildable (no shared test-helpers project crossing module boundaries). Verification: dotnet build src/ZB.MOM.WW.MxGateway.IntegrationTests clean; 19/19 integration tests passing (live MxAccess + LDAP + Galaxy). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
132 lines
5.0 KiB
C#
132 lines
5.0 KiB
C#
using System.Security.Claims;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using ZB.MOM.WW.MxGateway.Server.Configuration;
|
|
using ZB.MOM.WW.MxGateway.Server.Dashboard;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.IntegrationTests;
|
|
|
|
[Collection(LiveResourcesCollection.Name)]
|
|
[Trait("Category", "LiveLdap")]
|
|
public sealed class DashboardLdapLiveTests
|
|
{
|
|
[LiveLdapFact]
|
|
public async Task AuthenticateAsync_AdminInGwAdminGroup_Succeeds()
|
|
{
|
|
DashboardAuthenticator authenticator = CreateAuthenticator();
|
|
|
|
DashboardAuthenticationResult result = await authenticator.AuthenticateAsync(
|
|
"admin",
|
|
"admin123",
|
|
CancellationToken.None);
|
|
|
|
Assert.True(result.Succeeded);
|
|
Assert.NotNull(result.Principal);
|
|
Assert.Equal("admin", result.Principal.FindFirst(ClaimTypes.NameIdentifier)?.Value);
|
|
Assert.Contains(result.Principal.Claims, claim =>
|
|
claim.Type == DashboardAuthenticationDefaults.LdapGroupClaimType
|
|
&& claim.Value.Contains("GwAdmin", StringComparison.OrdinalIgnoreCase));
|
|
|
|
// IntegrationTests-023: DashboardAuthenticator.CreatePrincipal emits a
|
|
// ClaimTypes.Role claim derived from MapGroupsToRoles. The seeded
|
|
// GroupToRole map (GwAdmin -> Admin) means the admin principal must
|
|
// carry Role=Admin alongside the raw LDAP-group claim. A regression in
|
|
// MapGroupsToRoles (returning an empty list, missing the RDN fallback)
|
|
// would silently pass without this assertion.
|
|
Assert.Contains(result.Principal.Claims, claim =>
|
|
claim.Type == ClaimTypes.Role
|
|
&& claim.Value == DashboardRoles.Admin);
|
|
}
|
|
|
|
[LiveLdapFact]
|
|
public async Task AuthenticateAsync_ReadOnlyUserMissingGwAdminGroup_Fails()
|
|
{
|
|
DashboardAuthenticator authenticator = CreateAuthenticator();
|
|
|
|
DashboardAuthenticationResult result = await authenticator.AuthenticateAsync(
|
|
"readonly",
|
|
"readonly123",
|
|
CancellationToken.None);
|
|
|
|
Assert.False(result.Succeeded);
|
|
Assert.Null(result.Principal);
|
|
Assert.DoesNotContain("readonly123", result.FailureMessage, StringComparison.Ordinal);
|
|
}
|
|
|
|
[LiveLdapFact]
|
|
public async Task AuthenticateAsync_AdminWithWrongPassword_FailsWithoutLeakingPassword()
|
|
{
|
|
// Exercises the LdapException branch: the user exists and the service
|
|
// account search succeeds, but the candidate bind is rejected.
|
|
const string wrongPassword = "definitely-not-the-admin-password";
|
|
DashboardAuthenticator authenticator = CreateAuthenticator();
|
|
|
|
DashboardAuthenticationResult result = await authenticator.AuthenticateAsync(
|
|
"admin",
|
|
wrongPassword,
|
|
CancellationToken.None);
|
|
|
|
Assert.False(result.Succeeded);
|
|
Assert.Null(result.Principal);
|
|
Assert.DoesNotContain(wrongPassword, result.FailureMessage, StringComparison.Ordinal);
|
|
}
|
|
|
|
[LiveLdapFact]
|
|
public async Task AuthenticateAsync_UnknownUsername_Fails()
|
|
{
|
|
// Exercises the `candidate is null` branch: the service-account search
|
|
// returns no entry, so no candidate bind is attempted.
|
|
DashboardAuthenticator authenticator = CreateAuthenticator();
|
|
|
|
DashboardAuthenticationResult result = await authenticator.AuthenticateAsync(
|
|
"no-such-user-9f3c1",
|
|
"irrelevant-password",
|
|
CancellationToken.None);
|
|
|
|
Assert.False(result.Succeeded);
|
|
Assert.Null(result.Principal);
|
|
}
|
|
|
|
[LiveLdapFact]
|
|
public async Task AuthenticateAsync_ServerUnreachable_FailsWithoutThrowing()
|
|
{
|
|
// Exercises the connect-failure path: a closed loopback port produces a
|
|
// connection error that DashboardAuthenticator must absorb into a Fail
|
|
// result rather than propagating an exception to the dashboard.
|
|
DashboardAuthenticator authenticator = new(
|
|
Options.Create(new GatewayOptions
|
|
{
|
|
Ldap = new LdapOptions
|
|
{
|
|
// 1 is a reserved port number that no LDAP server listens on.
|
|
Port = 1,
|
|
},
|
|
}),
|
|
NullLogger<DashboardAuthenticator>.Instance);
|
|
|
|
DashboardAuthenticationResult result = await authenticator.AuthenticateAsync(
|
|
"admin",
|
|
"admin123",
|
|
CancellationToken.None);
|
|
|
|
Assert.False(result.Succeeded);
|
|
Assert.Null(result.Principal);
|
|
}
|
|
|
|
private static DashboardAuthenticator CreateAuthenticator()
|
|
{
|
|
return new DashboardAuthenticator(
|
|
Options.Create(new GatewayOptions
|
|
{
|
|
Dashboard = new DashboardOptions
|
|
{
|
|
GroupToRole = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
["GwAdmin"] = DashboardRoles.Admin,
|
|
},
|
|
},
|
|
}),
|
|
NullLogger<DashboardAuthenticator>.Instance);
|
|
}
|
|
}
|