11a716a07f
Full-host tests passed in isolation but failed 2-4/run under parallel execution on
Windows with 'the process cannot access the file ... because it is being used by
another process' (macOS never sees it — Unix deletes open files). Two shared-state
parallel collisions, discovered while verifying TST-08:
1. Self-signed cert generation. GatewayTlsBootstrapTests sets process-global
Kestrel/TLS env vars that GatewayApplication.Build reads; a parallel host-building
test inherits them mid-run and both generate a cert at the same path, racing on the
fixed-name '<path>.tmp'. Fixes:
- SelfSignedCertificateProvider: stage the PFX in a unique '<path>.<guid>.tmp' instead
of a fixed name, so concurrent/interrupted writers never collide on the temp file
(real robustness: two instances or a restart-during-write no longer clash). The
final atomic Move (last-writer-wins) still yields an equivalent cert.
- TestHostEnvironmentInitializer: default MxGateway__Tls__SelfSignedCertPath to a
per-process temp path so the suite never writes the shared ProgramData default or
fights the deployed service; first host-building test generates, the rest load it.
- GatewayTlsBootstrapTests: [Collection] with DisableParallelization so its global
env-var mutation cannot bleed into parallel tests (new GlobalEnvironmentCollection).
2. AuthStoreHealthCheckTests opened a real SQLite file; Microsoft.Data.Sqlite's
connection pool keeps the .db handle open after 'await using', so the finally
File.Delete threw. Reuse the existing TempDatabaseDirectory helper, which calls
SqliteConnection.ClearAllPools() before deleting.
Server build clean; affected classes 17/17 on macOS. Windows full-suite verified separately.
78 lines
3.6 KiB
C#
78 lines
3.6 KiB
C#
using System.Net.Security;
|
|
using System.Net.Sockets;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Hosting.Server;
|
|
using Microsoft.AspNetCore.Hosting.Server.Features;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using ZB.MOM.WW.MxGateway.Server;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Tests.Gateway;
|
|
|
|
// Sets process-global Kestrel/TLS environment variables that GatewayApplication.Build reads;
|
|
// serialized against all other collections so a parallel host-building test cannot inherit them
|
|
// mid-run and race on the generated-certificate file. See GlobalEnvironmentCollection.
|
|
[Collection(TestSupport.GlobalEnvironmentCollection.Name)]
|
|
public sealed class GatewayTlsBootstrapTests
|
|
{
|
|
/// <summary>
|
|
/// Verifies that when a Kestrel HTTPS endpoint is configured without its own certificate,
|
|
/// the gateway supplies the generated self-signed certificate as the Kestrel HTTPS default.
|
|
/// The host must start and bind, and the certificate served on the TLS handshake must be the
|
|
/// gateway's generated cert (subject <c>CN=MxAccessGateway Self-Signed</c>) — not an ambient
|
|
/// ASP.NET Core development certificate. On a host with no dev cert installed, starting a
|
|
/// cert-less HTTPS endpoint throws "No server certificate was specified"; on a host that has a
|
|
/// trusted dev cert, Kestrel would otherwise serve that dev cert (<c>CN=localhost</c>), so the
|
|
/// subject assertion is what makes this test fail without the wiring on either kind of host.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task Host_ServesGeneratedCertificate_WhenHttpsEndpointHasNoCertificate()
|
|
{
|
|
string certDir = Directory.CreateTempSubdirectory().FullName;
|
|
try
|
|
{
|
|
Environment.SetEnvironmentVariable("Kestrel__Endpoints__Test__Url", "https://127.0.0.1:0");
|
|
Environment.SetEnvironmentVariable(
|
|
"MxGateway__Tls__SelfSignedCertPath", Path.Combine(certDir, "gw.pfx"));
|
|
|
|
WebApplication app = GatewayApplication.Build([]);
|
|
await app.StartAsync();
|
|
try
|
|
{
|
|
string servedSubject = await ReadServedCertificateSubjectAsync(app);
|
|
Assert.Contains("MxAccessGateway Self-Signed", servedSubject, StringComparison.Ordinal);
|
|
}
|
|
finally
|
|
{
|
|
await app.StopAsync();
|
|
await app.DisposeAsync();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
Environment.SetEnvironmentVariable("Kestrel__Endpoints__Test__Url", null);
|
|
Environment.SetEnvironmentVariable("MxGateway__Tls__SelfSignedCertPath", null);
|
|
Directory.Delete(certDir, recursive: true);
|
|
}
|
|
}
|
|
|
|
private static async Task<string> ReadServedCertificateSubjectAsync(WebApplication app)
|
|
{
|
|
IServerAddressesFeature addresses =
|
|
app.Services.GetRequiredService<IServer>().Features.Get<IServerAddressesFeature>()
|
|
?? throw new InvalidOperationException("Server addresses feature was not available.");
|
|
Uri endpoint = new(addresses.Addresses.First());
|
|
|
|
using TcpClient client = new();
|
|
await client.ConnectAsync(endpoint.Host, endpoint.Port);
|
|
using SslStream ssl = new(
|
|
client.GetStream(),
|
|
leaveInnerStreamOpen: false,
|
|
userCertificateValidationCallback: (_, _, _, _) => true);
|
|
await ssl.AuthenticateAsClientAsync("127.0.0.1");
|
|
|
|
return ssl.RemoteCertificate?.Subject ?? "(none)";
|
|
}
|
|
}
|