From 67715f9d67464eae572fa71748655db99dc7a9a6 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 12 Sep 2026 17:31:18 +0100 Subject: [PATCH] tests en vert --- .../Fixtures/ApiWebServerFixture.cs | 20 ++------ src/Yavsc.Blogs.Tests/BlogAclApiTests.cs | 51 +++++++++++-------- .../Fixtures/BlogsWebServerFixture.cs | 34 +++---------- 3 files changed, 40 insertions(+), 65 deletions(-) diff --git a/src/Yavsc.Api.Test/Fixtures/ApiWebServerFixture.cs b/src/Yavsc.Api.Test/Fixtures/ApiWebServerFixture.cs index d8663ed8f..96c220bff 100644 --- a/src/Yavsc.Api.Test/Fixtures/ApiWebServerFixture.cs +++ b/src/Yavsc.Api.Test/Fixtures/ApiWebServerFixture.cs @@ -357,21 +357,9 @@ public sealed class ApiWebServerFixture : WebHostFixture public override void Dispose() { - try - { - base.Dispose(); - } - finally - { - lock (_sqliteLock) - { - if (_sharedSqliteConnection is not null) - { - _sharedSqliteConnection.Close(); - _sharedSqliteConnection.Dispose(); - _sharedSqliteConnection = null; - } - } - } + // Keep the shared in-memory SQLite connection alive for the + // whole test process. Closing it from one fixture instance can + // drop the schema while other tests are still running. + base.Dispose(); } } diff --git a/src/Yavsc.Blogs.Tests/BlogAclApiTests.cs b/src/Yavsc.Blogs.Tests/BlogAclApiTests.cs index ab5ebcc66..174838173 100644 --- a/src/Yavsc.Blogs.Tests/BlogAclApiTests.cs +++ b/src/Yavsc.Blogs.Tests/BlogAclApiTests.cs @@ -51,7 +51,7 @@ public sealed class BlogAclApiTests : IClassFixture private string BlogAclUrl() => $"{_fixture.Addresses.First(a => a.StartsWith("https://"))}/{APIPrefix}/{BlogAclPath}"; - /// Delete any ACL rows tied to the fixture's seeded + /// Delete any ACL rows tied to the specified /// (CircleId, BlogPostId) pair. The shared SQLite store /// persists across tests, so tests that POST a successful ACL /// row would otherwise conflict with whichever other test runs @@ -59,13 +59,13 @@ public sealed class BlogAclApiTests : IClassFixture /// execution order. Calling this at the start of each /// insert-bearing test guarantees a clean slate regardless of /// the previous test's outcome. - private void CleanupAcl() + private void CleanupAcl(long circleId, long blogPostId) { using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); db.CircleAuthorizationToBlogPost - .Where(a => a.CircleId == _fixture.CircleId - && a.BlogPostId == _fixture.PostId) + .Where(a => a.CircleId == circleId + && a.BlogPostId == blogPostId) .ExecuteDelete(); } @@ -122,13 +122,16 @@ public sealed class BlogAclApiTests : IClassFixture // The prod circle already exists with Name="test", Public=true, // owned by the caller. We seed the same shape pre-POST so the // test reproduces the prod scenario end-to-end. - CleanupAcl(); + _fixture.SeedUser(_fixture.DefaultUserLogin); + var seededCircleId = _fixture.SeedCircle(_fixture.DefaultUserLogin, "test"); + var seededBlogPostId = _fixture.SeedBlogPost(_fixture.DefaultUserLogin, "acl-target"); + CleanupAcl(seededCircleId, seededBlogPostId); using var http = NewClient(_fixture.DefaultUserLogin); var payload = new PostAccessControlRulePayload { - CircleId = _fixture.CircleId, - BlogPostId = _fixture.PostId + CircleId = seededCircleId, + BlogPostId = seededBlogPostId }; var response = await http.PostAsJsonAsync( @@ -194,13 +197,16 @@ public sealed class BlogAclApiTests : IClassFixture [Fact] async Task PostCircleAuthorization_dosent_return_500 () { - CleanupAcl(); + _fixture.SeedUser(_fixture.DefaultUserLogin); + var seededCircleId = _fixture.SeedCircle(_fixture.DefaultUserLogin, "test-" + Guid.NewGuid().ToString("N")); + var seededBlogPostId = _fixture.SeedBlogPost(_fixture.DefaultUserLogin, "acl-never-500"); + CleanupAcl(seededCircleId, seededBlogPostId); await PostCircleAuthorization_never_returns_500( new PostAccessControlRulePayload { BlogPostId = -1, - CircleId = _fixture.CircleId + CircleId = seededCircleId } ); @@ -209,13 +215,16 @@ public sealed class BlogAclApiTests : IClassFixture [Fact] async Task PostCircleAuthorization_dosent_return_500_on_success () { - CleanupAcl(); + _fixture.SeedUser(_fixture.DefaultUserLogin); + var seededCircleId = _fixture.SeedCircle(_fixture.DefaultUserLogin, "test-" + Guid.NewGuid().ToString("N")); + var seededBlogPostId = _fixture.SeedBlogPost(_fixture.DefaultUserLogin, "acl-never-500-success"); + CleanupAcl(seededCircleId, seededBlogPostId); await PostCircleAuthorization_never_returns_500( new PostAccessControlRulePayload { - BlogPostId = _fixture.PostId, - CircleId = _fixture.CircleId + BlogPostId = seededBlogPostId, + CircleId = seededCircleId } ); @@ -224,16 +233,17 @@ public sealed class BlogAclApiTests : IClassFixture [Fact] public async Task PostBlog_with_ACL_creates_a_post_and_Get_returns_it_in_the_list() { - CleanupAcl(); _fixture.SeedUser(_fixture.DefaultUserLogin); _fixture.SeedUser("tester"); - _fixture.SeedCircle(_fixture.DefaultUserLogin, "test", + var seededCircleId = _fixture.SeedCircle(_fixture.DefaultUserLogin, "test-" + Guid.NewGuid().ToString("N"), false, new String[] { _fixture.DefaultUserLogin, "tester" }); + var seededBlogPostId = _fixture.SeedBlogPost(_fixture.DefaultUserLogin, "acl-seeded-target"); + CleanupAcl(seededCircleId, seededBlogPostId); using var http = NewClient(_fixture.DefaultUserLogin ); // Create a minimal BlogPost. The server assigns Id, so we @@ -252,8 +262,8 @@ public sealed class BlogAclApiTests : IClassFixture { new CircleAuthorizationToBlogPost { - CircleId = _fixture.CircleId, - BlogPostId = _fixture.PostId + CircleId = seededCircleId, + BlogPostId = seededBlogPostId } } ) @@ -303,17 +313,16 @@ public sealed class BlogAclApiTests : IClassFixture var aclEntry = acl[0]; Assert.Equal(JsonValueKind.Object, aclEntry.ValueKind); - Assert.True(aclEntry.TryGetProperty("circleId", out var circleId)); - Assert.Equal(_fixture.CircleId, circleId.GetInt64()); + Assert.True(aclEntry.TryGetProperty("circleId", out var returnedCircleId)); + Assert.Equal(seededCircleId, returnedCircleId.GetInt64()); } [Fact] public async Task Non_owner_can_read_restricted_post_but_receives_empty_acl_in_list_and_detail() { - CleanupAcl(); _fixture.SeedUser(_fixture.DefaultUserLogin); _fixture.SeedUser("tester"); - _fixture.SeedCircle(_fixture.DefaultUserLogin, "test", false, + var seededCircleId = _fixture.SeedCircle(_fixture.DefaultUserLogin, "test-" + Guid.NewGuid().ToString("N"), false, new[] { _fixture.DefaultUserLogin, "tester" }); using var ownerHttp = NewClient(_fixture.DefaultUserLogin); @@ -343,7 +352,7 @@ public sealed class BlogAclApiTests : IClassFixture BlogAclUrl(), new PostAccessControlRulePayload { - CircleId = _fixture.CircleId, + CircleId = seededCircleId, BlogPostId = created.Id }, TestContext.Current.CancellationToken); diff --git a/src/Yavsc.Blogs.Tests/Fixtures/BlogsWebServerFixture.cs b/src/Yavsc.Blogs.Tests/Fixtures/BlogsWebServerFixture.cs index ea6837b1c..e0606f5d7 100644 --- a/src/Yavsc.Blogs.Tests/Fixtures/BlogsWebServerFixture.cs +++ b/src/Yavsc.Blogs.Tests/Fixtures/BlogsWebServerFixture.cs @@ -289,33 +289,11 @@ public sealed class BlogsWebServerFixture : WebHostFixture public override void Dispose() { - try - { - base.Dispose(); - } - finally - { - // Close the shared SQLite connection only when the - // last fixture instance goes away, matching the - // lifetime contract of WebHostFixture.Dispose. We - // rely on base.Dispose's _instanceCount decrement - // having run, so we close only if the host is gone - // (base already nulled _app when count==0). - lock (_sqliteLock) - { - if (_sharedSqliteConnection is not null) - { - // Synchronous close: SQLite's Close() is - // documented as safe to call from a sync - // context and avoids the GetAwaiter().GetResult() - // pattern that's historically caused teardown - // hangs in this repo's async pipeline. - _sharedSqliteConnection.Close(); - _sharedSqliteConnection.Dispose(); - _sharedSqliteConnection = null; - } - } - } + // Keep the shared in-memory SQLite connection alive for the + // whole test process. Closing it from one fixture instance can + // destroy the database while other collections are still using + // it, which surfaces as intermittent "no such table" failures. + base.Dispose(); } /// Seed an in the shared @@ -374,7 +352,7 @@ public sealed class BlogsWebServerFixture : WebHostFixture /// directly in the SQLite store and return its server-assigned /// id. public long SeedCircle(string ownerId, string name, bool isPublic = false, - ICollection members = null + ICollection? members = null ) { using var scope = Services.CreateScope();