From d92456e141fc5cb5a3ea78796bce18467387d967 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 12 Sep 2026 16:49:36 +0100 Subject: [PATCH] fixe tests --- src/Yavsc.Org.Tests/Mandatory/BatchTests.cs | 6 +-- src/Yavsc.Org.Tests/Mandatory/Remoting.cs | 6 ++- src/Yavsc.Org.Tests/NonRegression/Database.cs | 17 +------ src/Yavsc.Org.Tests/Smoke/BlogSmokeTests.cs | 6 ++- src/Yavsc.Org.Tests/WebServerFixture.cs | 49 +++++++++++++++++-- src/Yavsc.Tests.Shared/WebHostFixture.cs | 49 +++++++++++++++---- 6 files changed, 96 insertions(+), 37 deletions(-) diff --git a/src/Yavsc.Org.Tests/Mandatory/BatchTests.cs b/src/Yavsc.Org.Tests/Mandatory/BatchTests.cs index daa2bf1fe..30f0535a9 100644 --- a/src/Yavsc.Org.Tests/Mandatory/BatchTests.cs +++ b/src/Yavsc.Org.Tests/Mandatory/BatchTests.cs @@ -10,10 +10,10 @@ namespace Yavsc.Org.Tests { [Collection("Yavsc Server")] [Trait("regression", "oui")] - public class BaseTestContext : IClassFixture, IDisposable + public abstract class BaseTestContext : IClassFixture, IDisposable { - public readonly WebServerFixture _serverFixture; - private readonly ITestOutputHelper _output; + protected readonly WebServerFixture _serverFixture; + protected readonly ITestOutputHelper _output; public BaseTestContext(ITestOutputHelper output, WebServerFixture fixture) { diff --git a/src/Yavsc.Org.Tests/Mandatory/Remoting.cs b/src/Yavsc.Org.Tests/Mandatory/Remoting.cs index 125d6f75e..bb9353cb4 100644 --- a/src/Yavsc.Org.Tests/Mandatory/Remoting.cs +++ b/src/Yavsc.Org.Tests/Mandatory/Remoting.cs @@ -84,9 +84,11 @@ namespace Yavsc.Org.Tests [Fact] public async Task GetOpenIdConfiguration_returns_ok() { - using var client = _serverFixture.CreateHttpClient(); + using var client = CreateHttpClient(); var response = await GetRaw(client, "/.well-known/openid-configuration"); - var payload = await response.Content.ReadAsStringAsync(); + var payload = await response.Content.ReadAsStringAsync( + TestContext.Current.CancellationToken + ); Assert.True( response.IsSuccessStatusCode, diff --git a/src/Yavsc.Org.Tests/NonRegression/Database.cs b/src/Yavsc.Org.Tests/NonRegression/Database.cs index 4633ce177..1baf7e3bc 100644 --- a/src/Yavsc.Org.Tests/NonRegression/Database.cs +++ b/src/Yavsc.Org.Tests/NonRegression/Database.cs @@ -1,14 +1,8 @@ - -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; -using Yavsc.Models; - namespace Yavsc.Org.Tests.Mandatory {[Collection("Database")] [Trait("regression", "II")] [Trait("dev", "wip")] - public class Database: IClassFixture, IDisposable + public class Database : IClassFixture { readonly ITestOutputHelper output; readonly WebServerFixture _serverFixture; @@ -24,15 +18,6 @@ namespace Yavsc.Org.Tests.Mandatory /// Install all our migrations in a fresh new database. /// - public void Dispose() - { - if (_serverFixture!=null) - { - _serverFixture.Dispose(); - } - - } - [Fact] public void TestDatabaseMigration() { diff --git a/src/Yavsc.Org.Tests/Smoke/BlogSmokeTests.cs b/src/Yavsc.Org.Tests/Smoke/BlogSmokeTests.cs index 96650ba80..3c383ad80 100644 --- a/src/Yavsc.Org.Tests/Smoke/BlogSmokeTests.cs +++ b/src/Yavsc.Org.Tests/Smoke/BlogSmokeTests.cs @@ -14,11 +14,13 @@ namespace Yavsc.Org.Tests.Smoke; /// doc/architecture/decoupage-organisation.md. The smoke /// here asserts the front-end side of the BC. /// -public class BlogSmokeTests : SmokeTestBase, IClassFixture +public class BlogSmokeTests : BaseTestContext, IClassFixture { private readonly TestWebApplicationFactory _factory; - public BlogSmokeTests(TestWebApplicationFactory factory) + public BlogSmokeTests(TestWebApplicationFactory factory, ITestOutputHelper output, + WebServerFixture webServerFixture) + : base(output, webServerFixture) { _factory = factory; } diff --git a/src/Yavsc.Org.Tests/WebServerFixture.cs b/src/Yavsc.Org.Tests/WebServerFixture.cs index 9115af234..f438fd88c 100644 --- a/src/Yavsc.Org.Tests/WebServerFixture.cs +++ b/src/Yavsc.Org.Tests/WebServerFixture.cs @@ -63,6 +63,7 @@ public sealed class WebServerFixture : WebHostFixture private static string? _sharedTestingUserName; private static string? _sharedTestingUserPassword; private static string? _sharedTestingUserEmail; + private static string? _sharedHttpsAuthority; private static RecordingSmtpClientFactory? _sharedSmtpClientFactory; public IConfiguration? Configuration { get; private set; } @@ -78,10 +79,19 @@ public sealed class WebServerFixture : WebHostFixture public RecordingSmtpClientFactory? SmtpClientFactory { get; private set; } public ILogger? Logger { get; internal set; } - public string? HttpsAuthority => Addresses.FirstOrDefault(u => u.StartsWith("https:")); + public string? HttpsAuthority { get; private set; } + + protected override WebApplicationOptions CreateBuilderOptions() + { + return new WebApplicationOptions + { + ApplicationName = typeof(Yavsc.Program).Assembly.GetName().Name + }; + } + protected override WebApplication BuildApp(WebApplicationBuilder builder) { - var authority = $"https://localhost:{_httpsPort}"; + HttpsAuthority = $"https://localhost:{HttpsPort}"; // WebApplication.CreateBuilder defaults WebRootPath to // {ContentRoot}/wwwroot. The test assembly runs from @@ -100,7 +110,7 @@ public sealed class WebServerFixture : WebHostFixture ["Smtp:Port"] = "465", ["Smtp:UserName"] = "test-user", ["Smtp:Password"] = "test-pass", - ["Site:Authority"] = authority + ["Site:Authority"] = HttpsAuthority }); Configuration = builder.Configuration; @@ -183,6 +193,7 @@ public sealed class WebServerFixture : WebHostFixture _sharedTestingUserName = TestingUserName; _sharedTestingUserPassword = TestingUserPassword; _sharedTestingUserEmail = TestingUserEmail; + _sharedHttpsAuthority = HttpsAuthority; _sharedLogger = app.Services.GetRequiredService().CreateLogger(); Logger = _sharedLogger; SmtpClientFactory = smtpFactory; @@ -195,13 +206,42 @@ public sealed class WebServerFixture : WebHostFixture using var scope = Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); db.Database.EnsureDeleted(); + db.Database.EnsureCreated(); if (db.Database.IsRelational()) { db.Database.Migrate(); + ReseedAuthTestData(scope); return; } + ReseedAuthTestData(scope); + } - db.Database.EnsureCreated(); + private void ReseedAuthTestData(IServiceScope scope) + { + TestingUserName ??= "Tester"; + TestingUserPassword ??= "Test123!"; + TestingUserEmail ??= "test@no-reply.com"; + TestClientId ??= "testClientId"; + TestClientSecret ??= Guid.CreateVersion7().ToString(); + + TestingUser = null; + EnsureUser(TestingUserName, TestingUserPassword, TestingUserEmail, scope); + + var db = scope.ServiceProvider.GetRequiredService(); + TestingUser = db.Users.FirstOrDefault(u => u.UserName == TestingUserName); + + var configDb = scope.ServiceProvider.GetRequiredService(); + var hasClient = configDb.Set().Any(c => c.ClientId == TestClientId); + if (!hasClient) + { + AddAuthorizedClient(scope, TestClientId, TestClientSecret); + } + + _sharedTestClientId = TestClientId; + _sharedTestClientSecret = TestClientSecret; + _sharedTestingUserName = TestingUserName; + _sharedTestingUserPassword = TestingUserPassword; + _sharedTestingUserEmail = TestingUserEmail; } protected override async Task ConfigurePipelineAsync(WebApplication app) @@ -226,6 +266,7 @@ public sealed class WebServerFixture : WebHostFixture TestingUserName = _sharedTestingUserName; TestingUserPassword = _sharedTestingUserPassword; TestingUserEmail = _sharedTestingUserEmail; + HttpsAuthority = _sharedHttpsAuthority; SmtpClientFactory = _sharedSmtpClientFactory; Configuration = _sharedConfiguration; SiteSettings = _sharedSiteSettings; diff --git a/src/Yavsc.Tests.Shared/WebHostFixture.cs b/src/Yavsc.Tests.Shared/WebHostFixture.cs index b92adc094..ac3356cce 100644 --- a/src/Yavsc.Tests.Shared/WebHostFixture.cs +++ b/src/Yavsc.Tests.Shared/WebHostFixture.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.Extensions.DependencyInjection; using System.Net; +using System.Runtime.Loader; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; @@ -37,6 +38,7 @@ public abstract class WebHostFixture : IBackendFixture private static readonly object _sync = new object(); private static WebApplication? _app; private static bool _isInitialized; + private static bool _shutdownHooksRegistered; private static int _instanceCount; private static readonly List _sharedAddresses = new(); private static IServiceProvider? _sharedServices; @@ -63,6 +65,8 @@ public abstract class WebHostFixture : IBackendFixture { lock (_sync) { + RegisterShutdownHooks(); + if (!_isInitialized) { InitializeAsync().GetAwaiter().GetResult(); @@ -114,11 +118,19 @@ public abstract class WebHostFixture : IBackendFixture /// listen port. protected virtual int HttpsPort => 5101; + /// Options used to create the WebApplicationBuilder. + /// Derived fixtures can override (for example, to set + /// ApplicationName for MVC controller discovery). + protected virtual WebApplicationOptions CreateBuilderOptions() + { + return new WebApplicationOptions(); + } + public WebApplication App { get; private set; } private async Task InitializeAsync() { - var builder = WebApplication.CreateBuilder(); + var builder = WebApplication.CreateBuilder(CreateBuilderOptions()); builder.WebHost.ConfigureKestrel(options => { @@ -158,23 +170,40 @@ public abstract class WebHostFixture : IBackendFixture _instanceCount--; } - IsInitialized = false; + IsInitialized = _isInitialized; - if (_instanceCount > 0) + // Keep the shared host alive for the whole test process. + // Disposing per class/collection can race with other test + // classes and intermittently drop the listener mid-run. + } + } + + private static void RegisterShutdownHooks() + { + if (_shutdownHooksRegistered) + { + return; + } + + AppDomain.CurrentDomain.ProcessExit += (_, __) => ShutdownSharedHost(); + AssemblyLoadContext.Default.Unloading += _ => ShutdownSharedHost(); + _shutdownHooksRegistered = true; + } + + private static void ShutdownSharedHost() + { + lock (_sync) + { + if (!_isInitialized || _app is null) { return; } - if (!_isInitialized) - { - return; - } - - _app?.StopAsync().GetAwaiter().GetResult(); + _app.StopAsync().GetAwaiter().GetResult(); _app = null; _isInitialized = false; - _sharedAddresses.Clear(); _sharedServices = null; + _sharedAddresses.Clear(); } }