rename: prefix gateway projects/namespaces with ZB.MOM.WW + sln→slnx
Apply the ZB.MOM.WW. prefix to all gateway-side projects, folders,
.csproj/.sln contents, C# namespaces, using directives, generated proto
C# (csharp_namespace + checked-in generated files), InternalsVisibleTo
attributes, project-name string literals (LoadProject, .sln lookups,
worker exe paths, staticwebassets manifest), and the install/script/doc
references that point at any of the above. Migrate the solution from
.sln to .slnx via `dotnet sln migrate` and delete the old file.
External-runtime identifiers are intentionally NOT prefixed so external
configuration keeps working:
- GatewayMetrics.cs MeterName ("MxGateway.Server")
- DashboardAuthenticationDefaults Scheme/Policy ("MxGateway.Dashboard")
- GatewayRequestLoggingMiddleware logger category ("MxGateway.Request")
- StaRuntime thread name ("MxGateway.Worker.STA")
- appsettings.json root section "MxGateway" + env-var prefix
MxGateway__... and secret-name MxGateway:ApiKeyPepper
- C:\ProgramData\MxGateway\ data dir paths
Also fixes two tests that were not rename-related but became visible
while validating the rename:
- WorkerLiveMxAccessSmokeTests.ShutDownAsync: cancellation that the
gateway service correctly maps to RpcException(Cancelled) per gRPC
convention was being misclassified as a stream fault. Added a sibling
catch on RpcException with StatusCode.Cancelled.
- IntegrationTestEnvironment.ResolveRepositoryRoot: extracted IsRepositoryRoot
and made it accept either a .git marker OR a .sln/.slnx next to src/
so the worker-exe walker works in non-git working copies.
clients/proto/proto-inputs.json's protoRoot updated to point at
src/ZB.MOM.WW.MxGateway.Contracts/Protos.
Verified by `dotnet build` and a full `dotnet test` of the .slnx with
MXGATEWAY_RUN_LIVE_{MXACCESS,LDAP,GALAXY}_TESTS=1:
Tests: 472/472 pass
Worker.Tests: 280/280 pass (4 dev-rig [Fact(Skip=...)] skipped)
IntegrationTests: 18/18 pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.MxGateway.Server;
|
||||
using ZB.MOM.WW.MxGateway.Server.Dashboard;
|
||||
using ZB.MOM.WW.MxGateway.Server.Metrics;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Gateway;
|
||||
|
||||
public sealed class GatewayApplicationTests
|
||||
{
|
||||
/// <summary>Verifies that Build maps the live health check endpoint.</summary>
|
||||
[Fact]
|
||||
public async Task Build_MapsLiveHealthEndpoint()
|
||||
{
|
||||
await using WebApplication app = GatewayApplication.Build([]);
|
||||
|
||||
RouteEndpoint endpoint = Assert.Single(
|
||||
((IEndpointRouteBuilder)app).DataSources
|
||||
.SelectMany(dataSource => dataSource.Endpoints)
|
||||
.OfType<RouteEndpoint>(),
|
||||
candidate => candidate.RoutePattern.RawText == "/health/live");
|
||||
|
||||
Assert.Equal("LiveHealth", endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Build registers the gateway metrics service.</summary>
|
||||
[Fact]
|
||||
public async Task Build_RegistersGatewayMetrics()
|
||||
{
|
||||
await using WebApplication app = GatewayApplication.Build([]);
|
||||
|
||||
GatewayMetrics metrics = app.Services.GetRequiredService<GatewayMetrics>();
|
||||
|
||||
Assert.NotNull(metrics);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Build maps dashboard and authentication endpoints when the dashboard is enabled.</summary>
|
||||
[Fact]
|
||||
public async Task Build_WhenDashboardEnabled_MapsBlazorDashboardAndAuthEndpoints()
|
||||
{
|
||||
await using WebApplication app = GatewayApplication.Build([]);
|
||||
IReadOnlyList<RouteEndpoint> endpoints = GetRouteEndpoints(app);
|
||||
|
||||
Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/dashboard/");
|
||||
Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/dashboard/sessions");
|
||||
Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/dashboard/workers");
|
||||
Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/dashboard/events");
|
||||
Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/dashboard/settings");
|
||||
Assert.Contains(endpoints, endpoint =>
|
||||
endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName == "DashboardLogin");
|
||||
Assert.Contains(endpoints, endpoint =>
|
||||
endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName == "DashboardLogout");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the dashboard login, logout, and denied endpoints allow anonymous access.</summary>
|
||||
[Fact]
|
||||
public async Task Build_WhenDashboardEnabled_AuthEndpointsAllowAnonymousAccess()
|
||||
{
|
||||
await using WebApplication app = GatewayApplication.Build([]);
|
||||
IReadOnlyList<RouteEndpoint> endpoints = GetRouteEndpoints(app);
|
||||
|
||||
string[] anonymousEndpointNames =
|
||||
["DashboardLogin", "DashboardLoginPost", "DashboardLogout", "DashboardAccessDenied"];
|
||||
foreach (string endpointName in anonymousEndpointNames)
|
||||
{
|
||||
RouteEndpoint endpoint = Assert.Single(
|
||||
endpoints,
|
||||
candidate => candidate.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName == endpointName);
|
||||
|
||||
Assert.NotNull(endpoint.Metadata.GetMetadata<IAllowAnonymous>());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that dashboard Razor component routes require the dashboard authorization policy.</summary>
|
||||
[Fact]
|
||||
public async Task Build_WhenDashboardEnabled_ComponentRoutesRequireAuthorization()
|
||||
{
|
||||
await using WebApplication app = GatewayApplication.Build([]);
|
||||
IReadOnlyList<RouteEndpoint> endpoints = GetRouteEndpoints(app);
|
||||
|
||||
string[] componentRoutes =
|
||||
["/dashboard/", "/dashboard/sessions", "/dashboard/workers", "/dashboard/events", "/dashboard/settings"];
|
||||
foreach (string route in componentRoutes)
|
||||
{
|
||||
RouteEndpoint[] matches = endpoints
|
||||
.Where(endpoint => endpoint.RoutePattern.RawText == route)
|
||||
.ToArray();
|
||||
|
||||
Assert.NotEmpty(matches);
|
||||
Assert.All(matches, endpoint =>
|
||||
{
|
||||
IAuthorizeData? authorize = endpoint.Metadata.GetMetadata<IAuthorizeData>();
|
||||
Assert.NotNull(authorize);
|
||||
Assert.Equal(DashboardAuthenticationDefaults.AuthorizationPolicy, authorize.Policy);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-020 reversal regression guard. The original Server-020 finding
|
||||
/// incorrectly concluded that the duplicate <c>@page "/dashboard/X"</c>
|
||||
/// directives were redundant because <c>MapGroup("/dashboard")</c>
|
||||
/// would prepend the prefix to all dashboard Razor pages. In practice
|
||||
/// Blazor SSR's <c>RouteTableFactory</c> matches against the raw
|
||||
/// <c>@page</c> template values (not against the endpoint-route
|
||||
/// prefix), so removing <c>@page "/dashboard/X"</c> left the dashboard
|
||||
/// unreachable at runtime (every page returned HTTP 500 with "Unable
|
||||
/// to find the provided template '/dashboard/'"). The duplicate
|
||||
/// <c>@page</c> directives are restored, and as a side effect the
|
||||
/// endpoint route table DOES carry the doubled <c>/dashboard/dashboard/X</c>
|
||||
/// shape (because <c>MapGroup("/dashboard")</c> prefixes the already-prefixed
|
||||
/// <c>@page "/dashboard/X"</c>). Those doubled endpoints are harmless —
|
||||
/// no client requests <c>/dashboard/dashboard/X</c> — and removing them
|
||||
/// requires either dropping <c>MapGroup</c> or the <c>@page</c>
|
||||
/// prefix. This test asserts only the positive contract: every
|
||||
/// dashboard page IS reachable under the canonical <c>/dashboard/X</c>
|
||||
/// route, which is what the Blazor router actually serves.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Build_WhenDashboardEnabled_RegistersCanonicalDashboardRoutes()
|
||||
{
|
||||
await using WebApplication app = GatewayApplication.Build([]);
|
||||
IReadOnlyList<RouteEndpoint> endpoints = GetRouteEndpoints(app);
|
||||
|
||||
string[] canonicalRoutes =
|
||||
[
|
||||
"/dashboard/",
|
||||
"/dashboard/sessions",
|
||||
"/dashboard/workers",
|
||||
"/dashboard/events",
|
||||
"/dashboard/settings",
|
||||
"/dashboard/galaxy",
|
||||
"/dashboard/apikeys",
|
||||
"/dashboard/sessions/{SessionId}",
|
||||
];
|
||||
foreach (string canonical in canonicalRoutes)
|
||||
{
|
||||
Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == canonical);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Build_WhenDashboardDisabled_DoesNotMapDashboardRoutes()
|
||||
{
|
||||
await using WebApplication app = GatewayApplication.Build(["--MxGateway:Dashboard:Enabled=false"]);
|
||||
IReadOnlyList<RouteEndpoint> endpoints = GetRouteEndpoints(app);
|
||||
|
||||
Assert.DoesNotContain(endpoints, endpoint =>
|
||||
endpoint.RoutePattern.RawText?.StartsWith("/dashboard", StringComparison.Ordinal) == true);
|
||||
Assert.DoesNotContain(endpoints, endpoint =>
|
||||
endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName?.StartsWith(
|
||||
"Dashboard",
|
||||
StringComparison.Ordinal) == true);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that StartAsync fails when gateway configuration is invalid.</summary>
|
||||
/// <param name="key">Configuration key to override.</param>
|
||||
/// <param name="value">Invalid configuration value.</param>
|
||||
/// <param name="expectedFailure">Expected validation error message.</param>
|
||||
[Theory]
|
||||
[InlineData(
|
||||
"MxGateway:Worker:ExecutablePath",
|
||||
"worker.dll",
|
||||
"MxGateway:Worker:ExecutablePath must point to a .exe file.")]
|
||||
[InlineData(
|
||||
"MxGateway:Events:QueueCapacity",
|
||||
"0",
|
||||
"MxGateway:Events:QueueCapacity must be greater than zero.")]
|
||||
[InlineData(
|
||||
"MxGateway:Authentication:PepperSecretName",
|
||||
"",
|
||||
"MxGateway:Authentication:PepperSecretName is required")]
|
||||
[InlineData(
|
||||
"MxGateway:Dashboard:PathBase",
|
||||
"dashboard",
|
||||
"MxGateway:Dashboard:PathBase must start with '/'.")]
|
||||
[InlineData(
|
||||
"MxGateway:Ldap:RequiredGroup",
|
||||
"",
|
||||
"MxGateway:Ldap:RequiredGroup is required when LDAP login is enabled.")]
|
||||
[InlineData(
|
||||
"MxGateway:Ldap:AllowInsecureLdap",
|
||||
"false",
|
||||
"MxGateway:Ldap:AllowInsecureLdap must be true when UseTls is false.")]
|
||||
public async Task StartAsync_InvalidGatewayConfiguration_FailsStartup(
|
||||
string key,
|
||||
string value,
|
||||
string expectedFailure)
|
||||
{
|
||||
// Bind an ephemeral port (:0) — xUnit runs test collections in parallel, so any
|
||||
// WebApplication-building test must avoid a fixed port to prevent a bind collision.
|
||||
await using WebApplication app = GatewayApplication.Build(
|
||||
[$"--{key}={value}", "--urls=http://127.0.0.1:0"]);
|
||||
|
||||
OptionsValidationException exception = await Assert.ThrowsAsync<OptionsValidationException>(
|
||||
() => app.StartAsync());
|
||||
|
||||
Assert.Contains(
|
||||
exception.Failures,
|
||||
failure => failure.Contains(expectedFailure, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
private static IReadOnlyList<RouteEndpoint> GetRouteEndpoints(WebApplication app)
|
||||
{
|
||||
return ((IEndpointRouteBuilder)app).DataSources
|
||||
.SelectMany(dataSource => dataSource.Endpoints)
|
||||
.OfType<RouteEndpoint>()
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user