diff --git a/.gitignore b/.gitignore index b7813f60..a94475e3 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,3 @@ DataDir/ *.tests.trx *.tests.html - -*.log - diff --git a/Makefile b/Makefile index e8d7f3cf..fa9d4ecf 100644 --- a/Makefile +++ b/Makefile @@ -121,161 +121,4 @@ release: git push -u origin "$$BRANCH"; \ echo "==> Terminé. Branche $$BRANCH live sur origin." -# Cibles pour installer PostIt.Android en Debug sur l'AVD qemu. -# -# Usage typique : -# make qemu # lance l'AVD, attend le boot, build l'APK, l'installe -# make qemu-install # (re)build l'APK et l'installe (AVD doit tourner) -# make qemu-build # build l'APK seul (sans install) -# make qemu-run # démarre l'AVD en background -# make qemu-stop # arrête l'émulateur -# make qemu-wait-boot # attend que l'AVD ait fini de booter -# -# Variables surchargeables (make VAR=valeur) : -# AVD_NAME default: postit_test_avd -# (l'AVD doit être listé par `avdmanager list avd`) -# ADB_SERIAL default: emulator-5554 -# (port standard du premier émulateur lancé) -# ANDROID_HOME default: /opt/android-sdk -# (le SDK Android local; doit contenir -# emulator/emulator et platform-tools/adb) -# POSTIT_RID default: android-x64 -# (doit matcher l'ABI de l'AVD; `avdmanager list avd` -# affiche la ligne Tag/ABI) -# EMU_HEADLESS default: 0 -# (1 = lancer l'émulateur sans fenêtre, pour scripter) -# CONFIG surcharge la variable CONFIG globale (Debug par -# défaut dans ce Makefile). Passer à Release pour -# un APK optimisé et signé release. -# LOGCAT_LINES default: 200 -# (nombre de lignes dumpées par `make qemu-logcat`) -# LOGCAT_FOLLOW default: 0 -# (1 = stream live via `make qemu-logcat`, -# sinon dump one-shot des N dernières lignes) -# LOGCAT_BOOT_WAIT default: 5 -# (secondes d'attente entre le clear du buffer, -# le `am start`, et le dump final dans -# `make qemu-logcat-boot`) -AVD_NAME ?= postit_test_avd -ADB_SERIAL ?= emulator-5554 -ANDROID_HOME ?= /opt/android-sdk -POSTIT_RID ?= android-x64 -EMU_HEADLESS ?= 0 -LOGCAT_LINES ?= 200 -LOGCAT_FOLLOW ?= 0 -LOGCAT_BOOT_WAIT ?= 5 - -POSTIT_ANDROID_CSPROJ := src/PostIt/PostIt.Android/PostIt.Android.csproj -POSTIT_APK_DIR := src/PostIt/PostIt.Android/bin/$(CONFIG)/net10.0-android/$(POSTIT_RID) -POSTIT_APK := $(POSTIT_APK_DIR)/com.CompanyName.PostIt-Signed.apk - -qemu-run: - @echo " Starting AVD $(AVD_NAME) on $(ADB_SERIAL)..." - @mkdir -p /tmp/yavsc-emu - @EMU_ARGS=""; \ - if [ "$(EMU_HEADLESS)" = "1" ]; then EMU_ARGS="-no-window -no-audio"; fi; \ - $(ANDROID_HOME)/emulator/emulator -avd $(AVD_NAME) $$EMU_ARGS \ - >/tmp/yavsc-emu/$(AVD_NAME).log 2>&1 & \ - echo " emulator PID: $$!" - -qemu-stop: - adb -s $(ADB_SERIAL) emu kill - -qemu-wait-boot: - @echo " Waiting for $(ADB_SERIAL) to finish booting..." - adb -s $(ADB_SERIAL) wait-for-device - @for i in $$(seq 1 180); do \ - BOOTED=$$(adb -s $(ADB_SERIAL) shell getprop sys.boot_completed 2>/dev/null | tr -d '\r\n'); \ - if [ "$$BOOTED" = "1" ]; then \ - echo " ✓ booted in $${i}s"; \ - exit 0; \ - fi; \ - sleep 1; \ - done; \ - echo " ERROR: device did not boot within 180s." >&2; \ - echo " Logs: /tmp/yavsc-emu/$(AVD_NAME).log" >&2; \ - exit 1 - -qemu-build: - # EmbedAssembliesIntoApk=true: without this, the Debug APK ships - # without the managed assemblies in it (they are pushed at runtime - # via `adb push`, "Fast Deployment"). On the qemu emulator, the - # runtime cannot find them in `files/.__override__//` and - # aborts at startup with "No assemblies found in '.__override__'" - # (monodroid-glue.cc:757, SIGABRT). Forcing this property on - # packages the .dlls into the APK as `assemblies//` so the - # runtime reads them directly. - # - # The Xamarin.Android SDK property is `EmbedAssembliesIntoApk`, - # not `AndroidEnableFastDeployment` (which exists in older - # templates but is a no-op in the .NET 10 SDK). - dotnet build $(POSTIT_ANDROID_CSPROJ) \ - -c $(CONFIG) \ - -p:RuntimeIdentifier=$(POSTIT_RID) \ - -p:EmbedAssembliesIntoApk=true \ - --nologo - -qemu-install: qemu-build - @if [ ! -f "$(POSTIT_APK)" ]; then \ - echo " APK not found at $(POSTIT_APK)." >&2; \ - echo " Files in $(POSTIT_APK_DIR):" >&2; \ - ls -la "$(POSTIT_APK_DIR)" 2>/dev/null || echo " (directory does not exist)" >&2; \ - exit 1; \ - fi - @echo " Installing $(POSTIT_APK) on $(ADB_SERIAL)..." - adb -s $(ADB_SERIAL) install -r "$(POSTIT_APK)" - -# Dump recent logcat output for the running PostIt.Android process. -# By default, prints the last $(LOGCAT_LINES) lines (one-shot, with -# `-d`). Set LOGCAT_FOLLOW=1 to follow the stream live instead. -# Filtering is by PID (pidof com.CompanyName.PostIt), not by tag, -# because Mono/Xamarin can emit logs under several tags -# (mono, PostIt.Android, Avalonia.Android) and tag-based filtering -# would miss the ones not matching. PID-based filtering is exact. -# If the app is not running, pidof returns empty and logcat exits -# silently with no output; that is the expected behaviour for -# "no logs yet". -qemu-logcat: - @PID=$$(adb -s $(ADB_SERIAL) shell pidof com.CompanyName.PostIt 2>/dev/null | tr -d '\r\n'); \ - if [ -z "$$PID" ]; then \ - echo " com.CompanyName.PostIt is not running on $(ADB_SERIAL)."; \ - echo " Start the app first (am start -n com.CompanyName.PostIt/PostIt.Android.PostItMainActivity)"; \ - exit 1; \ - fi; \ - echo " Following PID $$PID (LOGCAT_FOLLOW=$(LOGCAT_FOLLOW), LOGCAT_LINES=$(LOGCAT_LINES))"; \ - if [ "$(LOGCAT_FOLLOW)" = "1" ]; then \ - adb -s $(ADB_SERIAL) logcat -v time --pid=$$PID; \ - else \ - adb -s $(ADB_SERIAL) logcat -d -v time -t $(LOGCAT_LINES) --pid=$$PID; \ - fi - -# Clear logcat, launch PostIt.Android, then dump everything that was -# emitted during the startup window. Targets the "démarrage KO" case -# where the process starts but Avalonia never renders a frame — the -# logcat trace from process start to first frame is what diagnoses it. -# -# Override LOGCAT_BOOT_WAIT to extend the post-launch wait -# (default 15s; raise to 30+ if the device is slow to boot Avalonia). -LOGCAT_BOOT_WAIT ?= 15 -qemu-logcat-boot: - @echo " Clearing logcat buffer..." - adb -s $(ADB_SERIAL) logcat -c - @echo " Launching com.CompanyName.PostIt..." - adb -s $(ADB_SERIAL) shell am start \ - -n com.CompanyName.PostIt/PostIt.Android.PostItMainActivity - @echo " Waiting $(LOGCAT_BOOT_WAIT)s for the app to start rendering..." - @sleep $(LOGCAT_BOOT_WAIT) - @echo " Dumping logcat (PostIt PID + system buffer):" - @PID=$$(adb -s $(ADB_SERIAL) shell pidof com.CompanyName.PostIt 2>/dev/null | tr -d '\r\n'); \ - if [ -n "$$PID" ]; then \ - echo " (PID $$PID at dump time)"; \ - adb -s $(ADB_SERIAL) logcat -d -v time --pid=$$PID; \ - else \ - echo " (PostIt process not running at dump time — dumping last $(LOGCAT_LINES) lines unfiltered)"; \ - adb -s $(ADB_SERIAL) logcat -d -v time -t $(LOGCAT_LINES); \ - fi - -qemu: qemu-run qemu-wait-boot qemu-install - @echo " ✓ PostIt.Android installed on $(ADB_SERIAL)" - -.PHONY: test release qemu qemu-run qemu-stop qemu-wait-boot qemu-build qemu-install qemu-logcat qemu-logcat-boot +.PHONY: test release diff --git a/src/Yavsc.Blogs.Tests/BlogApiMappedClaimsTests.cs b/src/Yavsc.Blogs.Tests/BlogApiMappedClaimsTests.cs index f4878860..f02a1b99 100644 --- a/src/Yavsc.Blogs.Tests/BlogApiMappedClaimsTests.cs +++ b/src/Yavsc.Blogs.Tests/BlogApiMappedClaimsTests.cs @@ -79,10 +79,10 @@ public sealed class BlogApiMappedClaimsTests : IClassFixture(TestContext.Current.CancellationToken); + var created = await response.Content.ReadFromJsonAsync(); Assert.NotNull(created); Assert.Equal("mapped-user", created!.AuthorId); } @@ -101,10 +101,10 @@ public sealed class BlogApiMappedClaimsTests : IClassFixture(TestContext.Current.CancellationToken); + var created = await createdResponse.Content.ReadFromJsonAsync(); Assert.NotNull(created); var updateResponse = await http.PutAsJsonAsync($"/api/v1/blog/{created!.Id}", new BlogPost @@ -115,7 +115,7 @@ public sealed class BlogApiMappedClaimsTests : IClassFixture(TestContext.Current.CancellationToken); + var created = await createdResponse.Content.ReadFromJsonAsync(); Assert.NotNull(created); using var otherHttp = NewClient(subject: "mapped-other"); @@ -149,7 +149,7 @@ public sealed class BlogApiMappedClaimsTests : IClassFixture var circleId = SeedCircle("alice", "Famille"); using var http = NewClient("alice"); - var response = await http.GetAsync(MembersUrl(circleId), TestContext.Current.CancellationToken); + var response = await http.GetAsync(MembersUrl(circleId)); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)); + using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); Assert.Equal(JsonValueKind.Array, doc.RootElement.ValueKind); Assert.Equal(0, doc.RootElement.GetArrayLength()); } @@ -130,14 +130,14 @@ public sealed class CircleMembersApiTests : IClassFixture var postResponse = await http.PostAsJsonAsync( MembersUrl(circleId), - new { userId = "bob" }, TestContext.Current.CancellationToken); + new { userId = "bob" }); Assert.Equal(HttpStatusCode.Created, postResponse.StatusCode); - var getResponse = await http.GetAsync(MembersUrl(circleId), TestContext.Current.CancellationToken); + var getResponse = await http.GetAsync(MembersUrl(circleId)); Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode); - using var doc = JsonDocument.Parse(await getResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)); + using var doc = JsonDocument.Parse(await getResponse.Content.ReadAsStringAsync()); Assert.Equal(JsonValueKind.Array, doc.RootElement.ValueKind); Assert.Equal(1, doc.RootElement.GetArrayLength()); var member = doc.RootElement[0]; @@ -155,12 +155,12 @@ public sealed class CircleMembersApiTests : IClassFixture var first = await http.PostAsJsonAsync( MembersUrl(circleId), - new { userId = "bob" }, TestContext.Current.CancellationToken); + new { userId = "bob" }); Assert.Equal(HttpStatusCode.Created, first.StatusCode); var second = await http.PostAsJsonAsync( MembersUrl(circleId), - new { userId = "bob" }, TestContext.Current.CancellationToken); + new { userId = "bob" }); Assert.Equal(HttpStatusCode.Conflict, second.StatusCode); } @@ -171,14 +171,14 @@ public sealed class CircleMembersApiTests : IClassFixture var circleId = SeedCircle("alice", "Famille"); using var http = NewClient("alice"); - await http.PostAsJsonAsync(MembersUrl(circleId), new { userId = "bob" }, TestContext.Current.CancellationToken); + await http.PostAsJsonAsync(MembersUrl(circleId), new { userId = "bob" }); var deleteResponse = await http.DeleteAsync( - $"{MembersUrl(circleId)}/bob", TestContext.Current.CancellationToken); + $"{MembersUrl(circleId)}/bob"); Assert.Equal(HttpStatusCode.OK, deleteResponse.StatusCode); - var getResponse = await http.GetAsync(MembersUrl(circleId), TestContext.Current.CancellationToken); - using var doc = JsonDocument.Parse(await getResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)); + var getResponse = await http.GetAsync(MembersUrl(circleId)); + using var doc = JsonDocument.Parse(await getResponse.Content.ReadAsStringAsync()); Assert.Equal(0, doc.RootElement.GetArrayLength()); } @@ -190,7 +190,7 @@ public sealed class CircleMembersApiTests : IClassFixture var circleId = SeedCircle("alice", "Famille"); using var http = NewClient("bob"); - var response = await http.GetAsync(MembersUrl(circleId), TestContext.Current.CancellationToken); + var response = await http.GetAsync(MembersUrl(circleId)); // 404, not 403 — the controller deliberately avoids leaking // the existence of someone else's circle. diff --git a/src/Yavsc.Blogs.Tests/PublishEndpointTests.cs b/src/Yavsc.Blogs.Tests/PublishEndpointTests.cs index 7d5a02a9..8a564262 100644 --- a/src/Yavsc.Blogs.Tests/PublishEndpointTests.cs +++ b/src/Yavsc.Blogs.Tests/PublishEndpointTests.cs @@ -100,12 +100,12 @@ public sealed class PublishEndpointTests : IClassFixture var postId = SeedPost("alice"); using var http = NewClient("alice"); - var put = await http.PutAsJsonAsync(PublishUrl(postId), new { publish = true }, TestContext.Current.CancellationToken); + var put = await http.PutAsJsonAsync(PublishUrl(postId), new { publish = true }); Assert.Equal(HttpStatusCode.NoContent, put.StatusCode); - var get = await http.GetAsync($"{BlogsUrl}/{postId}", TestContext.Current.CancellationToken); + var get = await http.GetAsync($"{BlogsUrl}/{postId}"); Assert.Equal(HttpStatusCode.OK, get.StatusCode); - using var doc = JsonDocument.Parse(await get.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)); + using var doc = JsonDocument.Parse(await get.Content.ReadAsStringAsync()); Assert.True(doc.RootElement.GetProperty("isPublished").GetBoolean()); } @@ -116,12 +116,12 @@ public sealed class PublishEndpointTests : IClassFixture var postId = SeedPost("alice"); using var http = NewClient("alice"); - await http.PutAsJsonAsync(PublishUrl(postId), new { publish = true }, TestContext.Current.CancellationToken); - var put = await http.PutAsJsonAsync(PublishUrl(postId), new { publish = false }, TestContext.Current.CancellationToken); + await http.PutAsJsonAsync(PublishUrl(postId), new { publish = true }); + var put = await http.PutAsJsonAsync(PublishUrl(postId), new { publish = false }); Assert.Equal(HttpStatusCode.NoContent, put.StatusCode); - var get = await http.GetAsync($"{BlogsUrl}/{postId}", TestContext.Current.CancellationToken); - using var doc = JsonDocument.Parse(await get.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)); + var get = await http.GetAsync($"{BlogsUrl}/{postId}"); + using var doc = JsonDocument.Parse(await get.Content.ReadAsStringAsync()); Assert.False(doc.RootElement.GetProperty("isPublished").GetBoolean()); } @@ -130,7 +130,7 @@ public sealed class PublishEndpointTests : IClassFixture { ResetDatabase(); using var http = NewClient("alice"); - var put = await http.PutAsJsonAsync(PublishUrl(99999L), new { publish = true }, TestContext.Current.CancellationToken); + var put = await http.PutAsJsonAsync(PublishUrl(99999L), new { publish = true }); Assert.Equal(HttpStatusCode.NotFound, put.StatusCode); } @@ -141,7 +141,7 @@ public sealed class PublishEndpointTests : IClassFixture var postId = SeedPost("alice"); using var http = NewClient("bob"); - var put = await http.PutAsJsonAsync(PublishUrl(postId), new { publish = true }, TestContext.Current.CancellationToken); + var put = await http.PutAsJsonAsync(PublishUrl(postId), new { publish = true }); // 401 Challenge (the controller returns Challenge() // for AuthorizationFailureException). The exact code // is framework-dependent; what matters is "not 204". diff --git a/src/Yavsc.Org.Tests/Controllers/TestWebApplicationFactoryIsolationTests.cs b/src/Yavsc.Org.Tests/Controllers/TestWebApplicationFactoryIsolationTests.cs deleted file mode 100644 index ec86ffe8..00000000 --- a/src/Yavsc.Org.Tests/Controllers/TestWebApplicationFactoryIsolationTests.cs +++ /dev/null @@ -1,68 +0,0 @@ -using IdentityServer8.EntityFramework.DbContexts; -using IdentityServer8.EntityFramework.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace Yavsc.Org.Tests.Controllers; - -/// -/// Regression sentinel: two -/// instances must not see each other's clients. -/// -/// EF Core's UseInMemoryDatabase(name) returns the same -/// backing store to every DbContext that asks for it under -/// the same name, in the same process. Before the per-fixture GUID -/// fix, both and -/// used the bare "InMemory" -/// connection string, so every fixture shared one store and tests -/// were silently order-dependent. -/// -/// We assert against directly -/// rather than via IClientStore: the validating wrapper around -/// IClientStore raises events through IEventService, -/// which is not registered in the test host and crashes with a -/// NullReferenceException before it can return a result. Going -/// straight to the DbContext is the same code path the production -/// code uses, so it is the right surface to assert against. -/// -public class TestWebApplicationFactoryIsolationTests -{ - [Fact] - public async Task Second_factory_does_not_see_clients_seeded_into_first() - { - var marker = $"marker-A-{Guid.NewGuid():N}"; - - // First factory: seed a distinctive client. - using (var first = new TestWebApplicationFactory()) - { - await using var scope = first.Services.CreateAsyncScope(); - var configDb = scope.ServiceProvider.GetRequiredService(); - var firstCs = scope.ServiceProvider.GetRequiredService() - .GetConnectionString("YavscConnection"); - Assert.StartsWith("InMemory-", firstCs); - configDb.Clients.Add(new Client { ClientId = marker, ClientName = "marker-A" }); - await configDb.SaveChangesAsync(TestContext.Current.CancellationToken); - - // Sanity: the first factory can see its own seed. - var seenByFirst = await configDb.Clients - .AsNoTracking() - .AnyAsync(c => c.ClientId == marker, TestContext.Current.CancellationToken); - Assert.True(seenByFirst); - } - - // Second factory: must start from a clean slate. If the - // in-memory store leaked from the first factory, this - // assertion fails. - using var second = new TestWebApplicationFactory(); - await using var secondScope = second.Services.CreateAsyncScope(); - var secondCs = secondScope.ServiceProvider.GetRequiredService() - .GetConnectionString("YavscConnection"); - Assert.StartsWith("InMemory-", secondCs); - var secondDb = secondScope.ServiceProvider.GetRequiredService(); - var seenBySecond = await secondDb.Clients - .AsNoTracking() - .AnyAsync(c => c.ClientId == marker, TestContext.Current.CancellationToken); - Assert.False(seenBySecond); - } -} diff --git a/src/Yavsc.Org.Tests/EstimateSignatureFileHelperTests.cs b/src/Yavsc.Org.Tests/EstimateSignatureFileHelperTests.cs index 69009432..d4e52cc4 100644 --- a/src/Yavsc.Org.Tests/EstimateSignatureFileHelperTests.cs +++ b/src/Yavsc.Org.Tests/EstimateSignatureFileHelperTests.cs @@ -91,13 +91,9 @@ public class EstimateSignatureFileHelperTests : IDisposable public async Task ReceiveEstimateSignatureAsync_rejects_null_payload() { var user = MakeUser("bob"); - // Capture TestContext.Current.CancellationToken outside the - // lambda so xUnit1051 sees a real CancellationToken argument - // (the lambda body runs on a different stack frame). - var ct = TestContext.Current.CancellationToken; await Assert.ThrowsAsync(() => EstimateSignatureFileHelper.ReceiveEstimateSignatureAsync( - user, 1L, SignatureType.Pro, payload: null!, token: ct)); + user, 1L, SignatureType.Pro, payload: null!)); } [Fact] @@ -105,10 +101,9 @@ public class EstimateSignatureFileHelperTests : IDisposable { var user = MakeUser("bob"); var payload = new SignaturePadPayload { Strokes = new[] { 1, 100, 100 } }; - var ct = TestContext.Current.CancellationToken; await Assert.ThrowsAsync(() => EstimateSignatureFileHelper.ReceiveEstimateSignatureAsync( - user, 0L, SignatureType.Pro, payload, token: ct)); + user, 0L, SignatureType.Pro, payload)); } // --- helpers ---------------------------------------------------- diff --git a/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs b/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs index b85cba11..dfd6edea 100644 --- a/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs +++ b/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs @@ -21,54 +21,9 @@ namespace Yavsc.Org.Tests; /// so that User.GetUserId() /// in user code sees a logged-in identity derived from the same /// header. -/// -/// Each instance gets its own in-memory database, identified by a -/// GUID generated in the constructor. The connection string -/// (ConnectionStrings:YavscConnection) is set as an -/// environment variable (ConnectionStrings__YavscConnection) -/// in the constructor and unset in , so the -/// production AddIdentityDBAndStores registers DbContext -/// instances against this fixture's own store. Without this, the -/// "InMemory" connection string from -/// appsettings-org.Testing.json would route every -/// instance — and any -/// running in the same process — to -/// the same backing store, leaking state between fixtures. -/// -/// Env vars are used (rather than ConfigureAppConfiguration or -/// UseSetting) because WebApplicationFactory applies -/// those too late: Program.Main has already captured the -/// connection string in AddIdentityDBAndStores by the time -/// the test host's overrides take effect. Env vars are the last -/// provider added in AddConfiguration (see -/// Yavsc.Server/Helpers/ConfigHelpers.cs), so they win. /// public class TestWebApplicationFactory : WebApplicationFactory { - private readonly string _fixtureId = Guid.NewGuid().ToString("N"); - - // ASP.NET Core's environment-variable configuration provider uses - // the key ConnectionStrings__YavscConnection (double underscore - // for the section separator). Set it before the host starts so - // the per-fixture connection string wins over - // appsettings-org.Testing.json. We do NOT touch the appsettings - // file; env vars take precedence in the configuration pipeline - // (see AddConfiguration in Yavsc.Server/Helpers/ConfigHelpers.cs, - // which adds AddEnvironmentVariables last). - private static readonly object _envLock = new(); - private bool _envSet; - - public TestWebApplicationFactory() - { - lock (_envLock) - { - Environment.SetEnvironmentVariable( - "ConnectionStrings__YavscConnection", - InMemoryDatabaseName.For(_fixtureId)); - _envSet = true; - } - } - protected override void ConfigureWebHost(IWebHostBuilder builder) { // UseEnvironment("Testing") puts the host in a dedicated @@ -95,18 +50,4 @@ public class TestWebApplicationFactory : WebApplicationFactory services.AddTransient(); }); } - - protected override void Dispose(bool disposing) - { - if (disposing && _envSet) - { - lock (_envLock) - { - Environment.SetEnvironmentVariable( - "ConnectionStrings__YavscConnection", null); - _envSet = false; - } - } - base.Dispose(disposing); - } } diff --git a/src/Yavsc.Org.Tests/WebServerFixture.cs b/src/Yavsc.Org.Tests/WebServerFixture.cs index 4f736ecf..e580438b 100644 --- a/src/Yavsc.Org.Tests/WebServerFixture.cs +++ b/src/Yavsc.Org.Tests/WebServerFixture.cs @@ -42,17 +42,6 @@ public sealed class WebServerFixture : WebHostFixture { private static readonly int _httpsPort = GetAvailableLoopbackPort(); - // One in-memory database name for the whole process: WebHostFixture - // is a per-process singleton (see _app, _isInitialized, _sharedServices - // in the base class), so every WebServerFixture instance shares the - // same backing store. That is intentional — the "Yavsc Server" test - // collection groups tests that should see the same seeded state, and - // re-initialising the store per fixture would just regress the - // order-dependence we are trying to eliminate. The GUID still matters - // because TestWebApplicationFactory and WebServerFixture must not - // collide in the in-memory store; see InMemoryDatabaseName. - private static readonly string _fixtureId = Guid.NewGuid().ToString("N"); - protected override int HttpsPort => _httpsPort; private static IConfiguration? _sharedConfiguration; @@ -91,7 +80,7 @@ public sealed class WebServerFixture : WebHostFixture // that plus the in-memory overrides below. builder.AddConfiguration(null).AddInMemoryCollection(new Dictionary { - [$"ConnectionStrings:{Yavsc.Constants.YavscConnectionStringName}"] = InMemoryDatabaseName.For(_fixtureId), + [$"ConnectionStrings:{Yavsc.Constants.YavscConnectionStringName}"] = "InMemory", // SMTP test config: UserName non-null so MailSender // exercises the Authenticate branch — the // RecordingSmtpClient captures it. diff --git a/src/Yavsc.Org/Extensions/HostingExtensions.cs b/src/Yavsc.Org/Extensions/HostingExtensions.cs index 6528b9c6..f92dc3c9 100644 --- a/src/Yavsc.Org/Extensions/HostingExtensions.cs +++ b/src/Yavsc.Org/Extensions/HostingExtensions.cs @@ -169,20 +169,10 @@ public static class HostingExtensions public static IdentityBuilder AddIdentityDBAndStores(this WebApplicationBuilder builder) { IServiceCollection services = builder.Services; + var connectionString = builder.Configuration.GetConnectionString(Constants.YavscConnectionStringName); - services.AddDbContext((sp, options) => + services.AddDbContext(options => { - // Read the connection string at DbContext construction time - // rather than at AddDbContext registration time, so test - // fixtures (e.g. WebApplicationFactory) can - // override the value via the host's IConfiguration before - // any DbContext is built. Reading it eagerly at the top of - // this method would freeze whatever was in configuration - // when Program.Main ran — too early for the test host's - // ConfigureAppConfiguration / UseSetting hooks to apply. - var connectionString = sp.GetRequiredService() - .GetConnectionString(Constants.YavscConnectionStringName); - if (UsesInMemoryProvider(connectionString)) { options.UseInMemoryDatabase(connectionString); @@ -327,21 +317,10 @@ public static class HostingExtensions options.ClaimsIdentity.RoleClaimType = Constants.RoleClaimType; }); var migrationsAssembly = typeof(Program).GetTypeInfo().Assembly.GetName().Name; - - // The IdentityServer8.EntityFramework ConfigurationStoreOptions - // and OperationalStoreOptions expose ConfigureDbContext as an - // Action with no service-provider - // access, so the connection string has to be captured here at - // registration time. For the production runtime this is fine: - // the connection string does not change after startup. For - // tests, this is the one knob we cannot push into the per-fixture - // config pipeline; the TestWebApplicationFactory bridge instead - // sets ConnectionStrings__YavscConnection as an environment - // variable, which AddEnvironmentVariables picks up as the last - // configuration provider in AddConfiguration. See - // Yavsc.Server/Helpers/ConfigHelpers.cs. var connectionString = builder.Configuration.GetConnectionString(Constants.YavscConnectionStringName); + string sqliteConnectionString = $"Data Source={Path.Combine(Path.GetTempPath(), "yavsc_test.db")}"; + var identityServerBuilder = builder.Services.AddIdentityServer(options => { options.Events.RaiseErrorEvents = true; @@ -621,13 +600,7 @@ public static class HostingExtensions private static bool UsesInMemoryProvider(string connectionString) { - // Test fixtures may suffix the connection string with a - // per-fixture GUID (see InMemoryDatabaseName in - // Yavsc.Tests.Shared) to keep their in-memory stores - // isolated. The base name "InMemory" is still what - // identifies an in-memory provider — anything starting - // with it is one. - return connectionString.StartsWith(InMemoryProviderName, StringComparison.OrdinalIgnoreCase); + return string.Equals(connectionString, InMemoryProviderName, StringComparison.OrdinalIgnoreCase); } private static Action EnsureDefaultApplicationScopes() diff --git a/src/Yavsc.Tests.Shared/InMemoryDatabaseName.cs b/src/Yavsc.Tests.Shared/InMemoryDatabaseName.cs deleted file mode 100644 index 1c78c6dd..00000000 --- a/src/Yavsc.Tests.Shared/InMemoryDatabaseName.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Yavsc.Tests.Shared; - -/// -/// Helpers for the in-memory connection string used by test fixtures. -/// -/// EF Core's UseInMemoryDatabase(name) returns the same backing -/// store to every DbContext that asks for it under the same -/// , in the same process. That means every -/// fixture that uses the bare "InMemory" connection string -/// shares the same in-memory database — which leaks state between -/// fixtures that are supposed to be independent, and silently makes -/// tests order-dependent. -/// -/// The fix is to give each fixture its own suffix. -/// returns a stable, fixture-scoped connection string. The fixture -/// stores the suffix in an instance field so successive calls within -/// the same fixture always resolve to the same database. -/// -public static class InMemoryDatabaseName -{ - /// Base connection string for the in-memory provider, - /// as it appears in appsettings-org.Testing.json. - public const string Base = "InMemory"; - - /// Builds a per-fixture connection string. Two calls - /// with the same return the same - /// string; two calls with different ids return different - /// strings, isolating the underlying in-memory stores. - public static string For(string fixtureId) => $"{Base}-{fixtureId}"; -}