Resolve IntegrationTests-022..024
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>
This commit is contained in:
@@ -26,6 +26,16 @@ public sealed class DashboardLdapLiveTests
|
||||
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]
|
||||
|
||||
@@ -97,9 +97,22 @@ public static class IntegrationTestEnvironment
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>Resolves the root directory of the repository by walking parents for a src/ directory next to either a .git marker or a .sln/.slnx file.</summary>
|
||||
/// <summary>
|
||||
/// Resolves the root directory of the repository by walking parents for a
|
||||
/// <c>src/</c> directory next to either a <c>.git</c> marker or a
|
||||
/// <c>.sln</c>/<c>.slnx</c> file. Throws <see cref="InvalidOperationException"/>
|
||||
/// when no root is found so a misconfigured run fails fast with an actionable
|
||||
/// message rather than silently falling back to the current working directory
|
||||
/// (which previously produced a misleading "worker exe not found" pointing at
|
||||
/// a fabricated path — see IntegrationTests-022). The
|
||||
/// <see cref="LiveMxAccessWorkerExecutableVariableName"/> environment variable
|
||||
/// remains the escape hatch for unusual deployments.
|
||||
/// </summary>
|
||||
/// <param name="startDirectory">Starting directory to search from.</param>
|
||||
/// <returns>The repository root path, or the start directory if not found.</returns>
|
||||
/// <returns>The repository root path.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when the parent walk exhausts without finding a repository root.
|
||||
/// </exception>
|
||||
internal static string ResolveRepositoryRoot(string startDirectory)
|
||||
{
|
||||
DirectoryInfo? directory = new(startDirectory);
|
||||
@@ -113,7 +126,11 @@ public static class IntegrationTestEnvironment
|
||||
directory = directory.Parent;
|
||||
}
|
||||
|
||||
return Directory.GetCurrentDirectory();
|
||||
throw new InvalidOperationException(
|
||||
$"Could not resolve repository root by walking parents of '{startDirectory}'. "
|
||||
+ "Expected to find a directory containing a 'src' subdirectory next to either a '.git' marker "
|
||||
+ "or a '*.sln' / '*.slnx' file in 'src'. "
|
||||
+ $"Set the '{LiveMxAccessWorkerExecutableVariableName}' environment variable to bypass repository-root resolution.");
|
||||
}
|
||||
|
||||
private static bool IsRepositoryRoot(DirectoryInfo directory)
|
||||
|
||||
@@ -45,4 +45,41 @@ public sealed class IntegrationTestEnvironmentTests
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that <see cref="IntegrationTestEnvironment.ResolveRepositoryRoot"/>
|
||||
/// throws <see cref="InvalidOperationException"/> with a diagnostic message when
|
||||
/// the walk exhausts without finding a repository root. The previous silent
|
||||
/// fallback to <c>Directory.GetCurrentDirectory()</c> masked misconfiguration
|
||||
/// (IntegrationTests-022); operators get a clear, actionable failure instead.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ResolveRepositoryRoot_NoMarkers_ThrowsInvalidOperationExceptionNamingStartAndMarkers()
|
||||
{
|
||||
string isolatedRoot = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||
string isolatedStart = Path.Combine(isolatedRoot, "nested");
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(isolatedStart);
|
||||
|
||||
InvalidOperationException ex = Assert.Throws<InvalidOperationException>(
|
||||
() => IntegrationTestEnvironment.ResolveRepositoryRoot(isolatedStart));
|
||||
|
||||
Assert.Contains(isolatedStart, ex.Message, StringComparison.Ordinal);
|
||||
Assert.Contains(".git", ex.Message, StringComparison.Ordinal);
|
||||
Assert.Contains(".sln", ex.Message, StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
IntegrationTestEnvironment.LiveMxAccessWorkerExecutableVariableName,
|
||||
ex.Message,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(isolatedRoot))
|
||||
{
|
||||
Directory.Delete(isolatedRoot, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||
using ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.IntegrationTests.TestSupport;
|
||||
|
||||
/// <summary>
|
||||
/// No-op <see cref="IDashboardEventBroadcaster"/> for integration tests that
|
||||
/// construct an <see cref="ZB.MOM.WW.MxGateway.Server.Grpc.EventStreamService"/>
|
||||
/// without exercising the dashboard SignalR fan-out. Singleton because the
|
||||
/// type holds no state — every consumer can share <see cref="Instance"/>.
|
||||
/// The unit-test project owns a parallel copy under
|
||||
/// <c>ZB.MOM.WW.MxGateway.Tests/TestSupport/</c>; IntegrationTests keeps its
|
||||
/// own copy here so the two test projects stay independently buildable
|
||||
/// without a shared test-helpers project (IntegrationTests-024).
|
||||
/// </summary>
|
||||
public sealed class NullDashboardEventBroadcaster : IDashboardEventBroadcaster
|
||||
{
|
||||
/// <summary>Shared no-op instance.</summary>
|
||||
public static readonly NullDashboardEventBroadcaster Instance = new();
|
||||
|
||||
private NullDashboardEventBroadcaster()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Publish(string sessionId, MxEvent mxEvent)
|
||||
{
|
||||
// Intentionally empty — integration tests assert directly on the gRPC
|
||||
// stream writer, not on dashboard fan-out.
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ using ZB.MOM.WW.MxGateway.Server.Security.Authentication;
|
||||
using ZB.MOM.WW.MxGateway.Server.Security.Authorization;
|
||||
using ZB.MOM.WW.MxGateway.Server.Sessions;
|
||||
using ZB.MOM.WW.MxGateway.Server.Workers;
|
||||
using ZB.MOM.WW.MxGateway.IntegrationTests.TestSupport;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.IntegrationTests;
|
||||
@@ -1595,9 +1596,4 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
|
||||
CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
private sealed class NullDashboardEventBroadcaster : ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs.IDashboardEventBroadcaster
|
||||
{
|
||||
public static readonly NullDashboardEventBroadcaster Instance = new();
|
||||
public void Publish(string sessionId, ZB.MOM.WW.MxGateway.Contracts.Proto.MxEvent mxEvent) { }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user