yavsc/src/Yavsc.Blogs.Tests/BlogAclApiTests.cs

302 lines
11 KiB
C#
Raw Normal View History

feat(postit): circles+ACL UI, blog fixture→SQLite, seed default user Bundled end-of-branch commit on feat/postit-acl-members. PostIt UI for circles + per-post ACL - Reorganise PostIt.Tests into Auth/ and Blogs/ subfolders (Bearer/OIDC scope tests vs. blog API fakes live where they belong) and introduces PostItHeadlessCollection so the Avalonia.Headless tests share a single xUnit collection instead of contending with the EF-Core test host. - Adds BlogAclApiTests (a brand-new behavioural layer over POST /api/v1/blogacl) and the fakes it relies on (BlogApiTestFakes, BlogPostAuthorDtoTests, AddCircleMember DialogTests); pulls UserId-through-OIDC-sub path into BearerScopeTests / FakeAuthorizingBrowser / OidcStubAuthority. - App.axaml.cs gets a small PushPageAsync touch-up the new tests rely on. - Drops UnitTest1.cs (xUnit scaffold, never used). Yavsc.Blogs.Tests — SQLite instead of InMemory - Bumps Yavsc.Blogs.Tests.csproj on Microsoft.EntityFrameworkCore.Sqlite and rewrites BlogsWebServerFixture to hold a single shared SqliteConnection (Cache=Shared) for the fixture lifetime, with a sync Dispose close to dodge async teardown hangs. Reason: the EF Core InMemory provider silently ignores FKs, which masked the kind of bug we are about to pin in the ACL tests. SQLite enforces them, so any future INSERT that forgets to seed its parent rows fails loudly here instead of passing the test and breaking prod. - PublishEndpointTests and BlogApiSmokeTests get a one-line tweak to follow the new connection lifecycle. Foreign-key fallout: seed the default user in the fixture - Adds BlogsWebServerFixture.SeedUser(userName). Now that SQLite enforces BlogPost.AuthorId → AspNetUsers.Id, every test that POST/PUT/DELETE a BlogPost and sends AuthorId= 'tester' in the payload needs an AspNetUsers row to satisfy the FK or it returns 500 with SQLite Error 19. - BlogApiTests wraps the existing ResetDatabase with a ResetAndSeedDefaultUser helper for the six mutating tests; the four GET-only and ModelState-only tests keep the bare ResetDatabase. - Side benefit: every test in Yavsc.Blogs.Tests now finishes cleanly instead of hanging at teardown — previously a stuck test held the shared SqliteConnection open and the next tests waited indefinitely. Verified: dotnet test src/Yavsc.Blogs.Tests passes 25/25 green from a clean run, no fixture teardown hang.
2026-08-20 23:59:21 +01:00
using System.Net;
using System.Net.Http.Json;
2026-08-28 20:10:32 +01:00
using System.Text.Json;
acl post: never 500 regression sentinel + async CheckOwner + fixture seed The hard rule on POST /api/v1/blogacl is: a 500 is never acceptable, regardless of the payload shape. The prod 500 logged on 2026-08-21 on mercure was caused by the PostIt client sending { circleId } only, which the server deserialised into CircleAuthorizationToBlogPost with BlogPostId = default(long) = 0; EF Core refused the INSERT with InvalidOperationException: The value of 'CircleAuthorizationToBlogPost.BlogPostId' is unknown. The PostIt fix lives in b82b6722 (enrich the payload with blogPostId). The server-side guard lives in this commit: - BlogAclApiController.CheckOwner is now async and uses FirstOrDefaultAsync instead of First, so it does not deadlock the request thread and returns false on a missing circle (which the controller already maps to ChallengeResult). - BlogsWebServerFixture now seeds Alice, her Circle and her BlogPost in ConfigurePipelineAsync, once at host startup, against the shared SqliteConnection (Cache=Shared). EnsureCreated is idempotent and runs against the connection that every DbContext resolves through, so the test theory can POST payloads with real FK ids against a schema that actually has the Circle / BlogSpot tables. - BlogAclApiTests: - PostCircleAuthorization_returns_201_when_payload_mirrors_PostIt_shape is the regression sentinel for the prod fix. - PostCircleAuthorization_never_returns_500 is a [Theory] over several payload shapes; any future commit that reintroduces a 500 path turns it red. CleanupAcl at the start of each insert- bearing test isolates against xUnit's no-guarantee-of-order execution: a successful POST in test N would otherwise conflict with test N+1 against the same (CircleId, BlogPostId) pair.
2026-08-21 22:00:27 +01:00
using Microsoft.EntityFrameworkCore;
feat(postit): circles+ACL UI, blog fixture→SQLite, seed default user Bundled end-of-branch commit on feat/postit-acl-members. PostIt UI for circles + per-post ACL - Reorganise PostIt.Tests into Auth/ and Blogs/ subfolders (Bearer/OIDC scope tests vs. blog API fakes live where they belong) and introduces PostItHeadlessCollection so the Avalonia.Headless tests share a single xUnit collection instead of contending with the EF-Core test host. - Adds BlogAclApiTests (a brand-new behavioural layer over POST /api/v1/blogacl) and the fakes it relies on (BlogApiTestFakes, BlogPostAuthorDtoTests, AddCircleMember DialogTests); pulls UserId-through-OIDC-sub path into BearerScopeTests / FakeAuthorizingBrowser / OidcStubAuthority. - App.axaml.cs gets a small PushPageAsync touch-up the new tests rely on. - Drops UnitTest1.cs (xUnit scaffold, never used). Yavsc.Blogs.Tests — SQLite instead of InMemory - Bumps Yavsc.Blogs.Tests.csproj on Microsoft.EntityFrameworkCore.Sqlite and rewrites BlogsWebServerFixture to hold a single shared SqliteConnection (Cache=Shared) for the fixture lifetime, with a sync Dispose close to dodge async teardown hangs. Reason: the EF Core InMemory provider silently ignores FKs, which masked the kind of bug we are about to pin in the ACL tests. SQLite enforces them, so any future INSERT that forgets to seed its parent rows fails loudly here instead of passing the test and breaking prod. - PublishEndpointTests and BlogApiSmokeTests get a one-line tweak to follow the new connection lifecycle. Foreign-key fallout: seed the default user in the fixture - Adds BlogsWebServerFixture.SeedUser(userName). Now that SQLite enforces BlogPost.AuthorId → AspNetUsers.Id, every test that POST/PUT/DELETE a BlogPost and sends AuthorId= 'tester' in the payload needs an AspNetUsers row to satisfy the FK or it returns 500 with SQLite Error 19. - BlogApiTests wraps the existing ResetDatabase with a ResetAndSeedDefaultUser helper for the six mutating tests; the four GET-only and ModelState-only tests keep the bare ResetDatabase. - Side benefit: every test in Yavsc.Blogs.Tests now finishes cleanly instead of hanging at teardown — previously a stuck test held the shared SqliteConnection open and the next tests waited indefinitely. Verified: dotnet test src/Yavsc.Blogs.Tests passes 25/25 green from a clean run, no fixture teardown hang.
2026-08-20 23:59:21 +01:00
using Microsoft.Extensions.DependencyInjection;
using Yavsc.Abstract.BlogSpot;
feat(postit): circles+ACL UI, blog fixture→SQLite, seed default user Bundled end-of-branch commit on feat/postit-acl-members. PostIt UI for circles + per-post ACL - Reorganise PostIt.Tests into Auth/ and Blogs/ subfolders (Bearer/OIDC scope tests vs. blog API fakes live where they belong) and introduces PostItHeadlessCollection so the Avalonia.Headless tests share a single xUnit collection instead of contending with the EF-Core test host. - Adds BlogAclApiTests (a brand-new behavioural layer over POST /api/v1/blogacl) and the fakes it relies on (BlogApiTestFakes, BlogPostAuthorDtoTests, AddCircleMember DialogTests); pulls UserId-through-OIDC-sub path into BearerScopeTests / FakeAuthorizingBrowser / OidcStubAuthority. - App.axaml.cs gets a small PushPageAsync touch-up the new tests rely on. - Drops UnitTest1.cs (xUnit scaffold, never used). Yavsc.Blogs.Tests — SQLite instead of InMemory - Bumps Yavsc.Blogs.Tests.csproj on Microsoft.EntityFrameworkCore.Sqlite and rewrites BlogsWebServerFixture to hold a single shared SqliteConnection (Cache=Shared) for the fixture lifetime, with a sync Dispose close to dodge async teardown hangs. Reason: the EF Core InMemory provider silently ignores FKs, which masked the kind of bug we are about to pin in the ACL tests. SQLite enforces them, so any future INSERT that forgets to seed its parent rows fails loudly here instead of passing the test and breaking prod. - PublishEndpointTests and BlogApiSmokeTests get a one-line tweak to follow the new connection lifecycle. Foreign-key fallout: seed the default user in the fixture - Adds BlogsWebServerFixture.SeedUser(userName). Now that SQLite enforces BlogPost.AuthorId → AspNetUsers.Id, every test that POST/PUT/DELETE a BlogPost and sends AuthorId= 'tester' in the payload needs an AspNetUsers row to satisfy the FK or it returns 500 with SQLite Error 19. - BlogApiTests wraps the existing ResetDatabase with a ResetAndSeedDefaultUser helper for the six mutating tests; the four GET-only and ModelState-only tests keep the bare ResetDatabase. - Side benefit: every test in Yavsc.Blogs.Tests now finishes cleanly instead of hanging at teardown — previously a stuck test held the shared SqliteConnection open and the next tests waited indefinitely. Verified: dotnet test src/Yavsc.Blogs.Tests passes 25/25 green from a clean run, no fixture teardown hang.
2026-08-20 23:59:21 +01:00
using Yavsc.Models;
using Yavsc.Models.Access;
2026-08-28 20:10:32 +01:00
using Yavsc.Models.Blog;
feat(postit): circles+ACL UI, blog fixture→SQLite, seed default user Bundled end-of-branch commit on feat/postit-acl-members. PostIt UI for circles + per-post ACL - Reorganise PostIt.Tests into Auth/ and Blogs/ subfolders (Bearer/OIDC scope tests vs. blog API fakes live where they belong) and introduces PostItHeadlessCollection so the Avalonia.Headless tests share a single xUnit collection instead of contending with the EF-Core test host. - Adds BlogAclApiTests (a brand-new behavioural layer over POST /api/v1/blogacl) and the fakes it relies on (BlogApiTestFakes, BlogPostAuthorDtoTests, AddCircleMember DialogTests); pulls UserId-through-OIDC-sub path into BearerScopeTests / FakeAuthorizingBrowser / OidcStubAuthority. - App.axaml.cs gets a small PushPageAsync touch-up the new tests rely on. - Drops UnitTest1.cs (xUnit scaffold, never used). Yavsc.Blogs.Tests — SQLite instead of InMemory - Bumps Yavsc.Blogs.Tests.csproj on Microsoft.EntityFrameworkCore.Sqlite and rewrites BlogsWebServerFixture to hold a single shared SqliteConnection (Cache=Shared) for the fixture lifetime, with a sync Dispose close to dodge async teardown hangs. Reason: the EF Core InMemory provider silently ignores FKs, which masked the kind of bug we are about to pin in the ACL tests. SQLite enforces them, so any future INSERT that forgets to seed its parent rows fails loudly here instead of passing the test and breaking prod. - PublishEndpointTests and BlogApiSmokeTests get a one-line tweak to follow the new connection lifecycle. Foreign-key fallout: seed the default user in the fixture - Adds BlogsWebServerFixture.SeedUser(userName). Now that SQLite enforces BlogPost.AuthorId → AspNetUsers.Id, every test that POST/PUT/DELETE a BlogPost and sends AuthorId= 'tester' in the payload needs an AspNetUsers row to satisfy the FK or it returns 500 with SQLite Error 19. - BlogApiTests wraps the existing ResetDatabase with a ResetAndSeedDefaultUser helper for the six mutating tests; the four GET-only and ModelState-only tests keep the bare ResetDatabase. - Side benefit: every test in Yavsc.Blogs.Tests now finishes cleanly instead of hanging at teardown — previously a stuck test held the shared SqliteConnection open and the next tests waited indefinitely. Verified: dotnet test src/Yavsc.Blogs.Tests passes 25/25 green from a clean run, no fixture teardown hang.
2026-08-20 23:59:21 +01:00
using Yavsc.Tests.Shared;
using static Yavsc.Constants;
namespace Yavsc.Blogs.Tests;
/// <summary>
/// Behavioural tests for <c>BlogAclApiController.PostCircleAuthorizationToBlogPost</c>:
/// <c>POST /api/v1/blogacl</c> with a JSON body of
remove dead 'Comment' field from CircleAuthorizationToBlogPost The bool Comment on CircleAuthorizationToBlogPost was dead code: never read or written by any caller in src/, no UI exposure, no behavioural semantics. The wire DTO (CircleAuthorization in Yavsc.Abstract) doesn't carry it, no reader consumes it, and the PostIt client builds its payload without it. What changes: - src/Yavsc.Server/Models/Access/CircleAuthorizationToBlogPost.cs: remove the property. - src/Yavsc.Blogs.Tests/BlogAclApiTests.cs: drop 'Comment = true' from the existing test payload and trim the now-inaccurate XML doc comment ('CircleId + BlogPostId + Comment' -> 'CircleId + BlogPostId'). Also adds a new [Fact] pinning the prod bug reported on 2026-08-21 (HTTP 500 'BlogPostId is unknown' when PostIt POSTs the bare { circleId } shape). That test stays red: the real fix for the 500 is in PostIt (payload needs blogPostId) + on the wire DTO + server-side validation, and lives in a follow-up commit. Migration: - src/Yavsc.Org/Migrations/20260820232152_DropCommentFromCircleAuthorizationToBlogPost drops the boolean 'Comment' column on CircleAuthorizationToBlogPost. The generated scaffold also wanted to drop three 'ClientId1' shadow FK columns on ClientScopes / ClientRedirectUris / ClientGrantTypes (from leftover HasOne<Client>() overrides in ApplicationDbContext.OnModelCreating); those were removed from the .cs to keep the migration scoped to this fix. Cleaning up the shadow property declarations themselves is left as a separate task. The ModelSnapshot still reflects the shadow 'ClientId1' columns intentionally: they exist in the prod database today (all NULL), and EF will rescaffold a drop migration for them on the next 'migrations add' regardless. No data loss.
2026-08-21 00:27:27 +01:00
/// <c>CircleAuthorizationToBlogPost</c> (CircleId + BlogPostId).
feat(postit): circles+ACL UI, blog fixture→SQLite, seed default user Bundled end-of-branch commit on feat/postit-acl-members. PostIt UI for circles + per-post ACL - Reorganise PostIt.Tests into Auth/ and Blogs/ subfolders (Bearer/OIDC scope tests vs. blog API fakes live where they belong) and introduces PostItHeadlessCollection so the Avalonia.Headless tests share a single xUnit collection instead of contending with the EF-Core test host. - Adds BlogAclApiTests (a brand-new behavioural layer over POST /api/v1/blogacl) and the fakes it relies on (BlogApiTestFakes, BlogPostAuthorDtoTests, AddCircleMember DialogTests); pulls UserId-through-OIDC-sub path into BearerScopeTests / FakeAuthorizingBrowser / OidcStubAuthority. - App.axaml.cs gets a small PushPageAsync touch-up the new tests rely on. - Drops UnitTest1.cs (xUnit scaffold, never used). Yavsc.Blogs.Tests — SQLite instead of InMemory - Bumps Yavsc.Blogs.Tests.csproj on Microsoft.EntityFrameworkCore.Sqlite and rewrites BlogsWebServerFixture to hold a single shared SqliteConnection (Cache=Shared) for the fixture lifetime, with a sync Dispose close to dodge async teardown hangs. Reason: the EF Core InMemory provider silently ignores FKs, which masked the kind of bug we are about to pin in the ACL tests. SQLite enforces them, so any future INSERT that forgets to seed its parent rows fails loudly here instead of passing the test and breaking prod. - PublishEndpointTests and BlogApiSmokeTests get a one-line tweak to follow the new connection lifecycle. Foreign-key fallout: seed the default user in the fixture - Adds BlogsWebServerFixture.SeedUser(userName). Now that SQLite enforces BlogPost.AuthorId → AspNetUsers.Id, every test that POST/PUT/DELETE a BlogPost and sends AuthorId= 'tester' in the payload needs an AspNetUsers row to satisfy the FK or it returns 500 with SQLite Error 19. - BlogApiTests wraps the existing ResetDatabase with a ResetAndSeedDefaultUser helper for the six mutating tests; the four GET-only and ModelState-only tests keep the bare ResetDatabase. - Side benefit: every test in Yavsc.Blogs.Tests now finishes cleanly instead of hanging at teardown — previously a stuck test held the shared SqliteConnection open and the next tests waited indefinitely. Verified: dotnet test src/Yavsc.Blogs.Tests passes 25/25 green from a clean run, no fixture teardown hang.
2026-08-20 23:59:21 +01:00
///
/// <para>Same fixture as <see cref="CircleMembersApiTests"/>:
/// <see cref="BlogsWebServerFixture"/> provides a SQLite
/// <c>:memory:</c> <c>ApplicationDbContext</c> (so FKs are
/// enforced the way a real relational engine would) and JWT
/// bearer auth via <c>TestTokenIssuer</c>. No mocks — the real
/// DbContext receives the real INSERT attempt.</para>
///
/// <para>The bug being pinned by these tests: the POST endpoint
/// calls <c>_context.CircleAuthorizationToBlogPost.Add(...)</c>
/// then <c>SaveChangesAsync</c>. The entity has a composite
/// key (CircleId + BlogPostId) and two FKs; EF Core refuses
/// the INSERT with
/// <c>System.InvalidOperationException: The value of
/// 'CircleAuthorizationToBlogPost.BlogPostId' is unknown when
/// attempting to save changes</c> when the principal entities
/// (the existing <c>BlogPost</c> and <c>Circle</c>) are not
/// attached to the DbContext in the same change-tracker graph.</para>
/// </summary>
[Collection("Yavsc Blogs")]
public sealed class BlogAclApiTests : IClassFixture<BlogsWebServerFixture>
{
private readonly BlogsWebServerFixture _fixture;
public BlogAclApiTests(BlogsWebServerFixture fixture)
{
_fixture = fixture;
}
2026-08-21 20:25:13 +01:00
2026-08-28 20:10:32 +01:00
private string BlogUrl()
=> $"{_fixture.Addresses.First(a => a.StartsWith("https://"))}/{APIPrefix}/{BlogSpotPath}";
feat(postit): circles+ACL UI, blog fixture→SQLite, seed default user Bundled end-of-branch commit on feat/postit-acl-members. PostIt UI for circles + per-post ACL - Reorganise PostIt.Tests into Auth/ and Blogs/ subfolders (Bearer/OIDC scope tests vs. blog API fakes live where they belong) and introduces PostItHeadlessCollection so the Avalonia.Headless tests share a single xUnit collection instead of contending with the EF-Core test host. - Adds BlogAclApiTests (a brand-new behavioural layer over POST /api/v1/blogacl) and the fakes it relies on (BlogApiTestFakes, BlogPostAuthorDtoTests, AddCircleMember DialogTests); pulls UserId-through-OIDC-sub path into BearerScopeTests / FakeAuthorizingBrowser / OidcStubAuthority. - App.axaml.cs gets a small PushPageAsync touch-up the new tests rely on. - Drops UnitTest1.cs (xUnit scaffold, never used). Yavsc.Blogs.Tests — SQLite instead of InMemory - Bumps Yavsc.Blogs.Tests.csproj on Microsoft.EntityFrameworkCore.Sqlite and rewrites BlogsWebServerFixture to hold a single shared SqliteConnection (Cache=Shared) for the fixture lifetime, with a sync Dispose close to dodge async teardown hangs. Reason: the EF Core InMemory provider silently ignores FKs, which masked the kind of bug we are about to pin in the ACL tests. SQLite enforces them, so any future INSERT that forgets to seed its parent rows fails loudly here instead of passing the test and breaking prod. - PublishEndpointTests and BlogApiSmokeTests get a one-line tweak to follow the new connection lifecycle. Foreign-key fallout: seed the default user in the fixture - Adds BlogsWebServerFixture.SeedUser(userName). Now that SQLite enforces BlogPost.AuthorId → AspNetUsers.Id, every test that POST/PUT/DELETE a BlogPost and sends AuthorId= 'tester' in the payload needs an AspNetUsers row to satisfy the FK or it returns 500 with SQLite Error 19. - BlogApiTests wraps the existing ResetDatabase with a ResetAndSeedDefaultUser helper for the six mutating tests; the four GET-only and ModelState-only tests keep the bare ResetDatabase. - Side benefit: every test in Yavsc.Blogs.Tests now finishes cleanly instead of hanging at teardown — previously a stuck test held the shared SqliteConnection open and the next tests waited indefinitely. Verified: dotnet test src/Yavsc.Blogs.Tests passes 25/25 green from a clean run, no fixture teardown hang.
2026-08-20 23:59:21 +01:00
private string BlogAclUrl()
2026-08-28 20:10:32 +01:00
=> $"{_fixture.Addresses.First(a => a.StartsWith("https://"))}/{APIPrefix}/{BlogAclPath}";
feat(postit): circles+ACL UI, blog fixture→SQLite, seed default user Bundled end-of-branch commit on feat/postit-acl-members. PostIt UI for circles + per-post ACL - Reorganise PostIt.Tests into Auth/ and Blogs/ subfolders (Bearer/OIDC scope tests vs. blog API fakes live where they belong) and introduces PostItHeadlessCollection so the Avalonia.Headless tests share a single xUnit collection instead of contending with the EF-Core test host. - Adds BlogAclApiTests (a brand-new behavioural layer over POST /api/v1/blogacl) and the fakes it relies on (BlogApiTestFakes, BlogPostAuthorDtoTests, AddCircleMember DialogTests); pulls UserId-through-OIDC-sub path into BearerScopeTests / FakeAuthorizingBrowser / OidcStubAuthority. - App.axaml.cs gets a small PushPageAsync touch-up the new tests rely on. - Drops UnitTest1.cs (xUnit scaffold, never used). Yavsc.Blogs.Tests — SQLite instead of InMemory - Bumps Yavsc.Blogs.Tests.csproj on Microsoft.EntityFrameworkCore.Sqlite and rewrites BlogsWebServerFixture to hold a single shared SqliteConnection (Cache=Shared) for the fixture lifetime, with a sync Dispose close to dodge async teardown hangs. Reason: the EF Core InMemory provider silently ignores FKs, which masked the kind of bug we are about to pin in the ACL tests. SQLite enforces them, so any future INSERT that forgets to seed its parent rows fails loudly here instead of passing the test and breaking prod. - PublishEndpointTests and BlogApiSmokeTests get a one-line tweak to follow the new connection lifecycle. Foreign-key fallout: seed the default user in the fixture - Adds BlogsWebServerFixture.SeedUser(userName). Now that SQLite enforces BlogPost.AuthorId → AspNetUsers.Id, every test that POST/PUT/DELETE a BlogPost and sends AuthorId= 'tester' in the payload needs an AspNetUsers row to satisfy the FK or it returns 500 with SQLite Error 19. - BlogApiTests wraps the existing ResetDatabase with a ResetAndSeedDefaultUser helper for the six mutating tests; the four GET-only and ModelState-only tests keep the bare ResetDatabase. - Side benefit: every test in Yavsc.Blogs.Tests now finishes cleanly instead of hanging at teardown — previously a stuck test held the shared SqliteConnection open and the next tests waited indefinitely. Verified: dotnet test src/Yavsc.Blogs.Tests passes 25/25 green from a clean run, no fixture teardown hang.
2026-08-20 23:59:21 +01:00
acl post: never 500 regression sentinel + async CheckOwner + fixture seed The hard rule on POST /api/v1/blogacl is: a 500 is never acceptable, regardless of the payload shape. The prod 500 logged on 2026-08-21 on mercure was caused by the PostIt client sending { circleId } only, which the server deserialised into CircleAuthorizationToBlogPost with BlogPostId = default(long) = 0; EF Core refused the INSERT with InvalidOperationException: The value of 'CircleAuthorizationToBlogPost.BlogPostId' is unknown. The PostIt fix lives in b82b6722 (enrich the payload with blogPostId). The server-side guard lives in this commit: - BlogAclApiController.CheckOwner is now async and uses FirstOrDefaultAsync instead of First, so it does not deadlock the request thread and returns false on a missing circle (which the controller already maps to ChallengeResult). - BlogsWebServerFixture now seeds Alice, her Circle and her BlogPost in ConfigurePipelineAsync, once at host startup, against the shared SqliteConnection (Cache=Shared). EnsureCreated is idempotent and runs against the connection that every DbContext resolves through, so the test theory can POST payloads with real FK ids against a schema that actually has the Circle / BlogSpot tables. - BlogAclApiTests: - PostCircleAuthorization_returns_201_when_payload_mirrors_PostIt_shape is the regression sentinel for the prod fix. - PostCircleAuthorization_never_returns_500 is a [Theory] over several payload shapes; any future commit that reintroduces a 500 path turns it red. CleanupAcl at the start of each insert- bearing test isolates against xUnit's no-guarantee-of-order execution: a successful POST in test N would otherwise conflict with test N+1 against the same (CircleId, BlogPostId) pair.
2026-08-21 22:00:27 +01:00
/// <summary>Delete any ACL rows tied to the fixture's seeded
/// <c>(CircleId, BlogPostId)</c> pair. The shared SQLite store
/// persists across tests, so tests that POST a successful ACL
/// row would otherwise conflict with whichever other test runs
/// next against the same pair — xUnit does not guarantee
/// execution order. Calling this at the start of each
/// insert-bearing test guarantees a clean slate regardless of
/// the previous test's outcome.</summary>
private void CleanupAcl()
{
using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
db.CircleAuthorizationToBlogPost
.Where(a => a.CircleId == _fixture.CircleId
&& a.BlogPostId == _fixture.PostId)
.ExecuteDelete();
}
feat(postit): circles+ACL UI, blog fixture→SQLite, seed default user Bundled end-of-branch commit on feat/postit-acl-members. PostIt UI for circles + per-post ACL - Reorganise PostIt.Tests into Auth/ and Blogs/ subfolders (Bearer/OIDC scope tests vs. blog API fakes live where they belong) and introduces PostItHeadlessCollection so the Avalonia.Headless tests share a single xUnit collection instead of contending with the EF-Core test host. - Adds BlogAclApiTests (a brand-new behavioural layer over POST /api/v1/blogacl) and the fakes it relies on (BlogApiTestFakes, BlogPostAuthorDtoTests, AddCircleMember DialogTests); pulls UserId-through-OIDC-sub path into BearerScopeTests / FakeAuthorizingBrowser / OidcStubAuthority. - App.axaml.cs gets a small PushPageAsync touch-up the new tests rely on. - Drops UnitTest1.cs (xUnit scaffold, never used). Yavsc.Blogs.Tests — SQLite instead of InMemory - Bumps Yavsc.Blogs.Tests.csproj on Microsoft.EntityFrameworkCore.Sqlite and rewrites BlogsWebServerFixture to hold a single shared SqliteConnection (Cache=Shared) for the fixture lifetime, with a sync Dispose close to dodge async teardown hangs. Reason: the EF Core InMemory provider silently ignores FKs, which masked the kind of bug we are about to pin in the ACL tests. SQLite enforces them, so any future INSERT that forgets to seed its parent rows fails loudly here instead of passing the test and breaking prod. - PublishEndpointTests and BlogApiSmokeTests get a one-line tweak to follow the new connection lifecycle. Foreign-key fallout: seed the default user in the fixture - Adds BlogsWebServerFixture.SeedUser(userName). Now that SQLite enforces BlogPost.AuthorId → AspNetUsers.Id, every test that POST/PUT/DELETE a BlogPost and sends AuthorId= 'tester' in the payload needs an AspNetUsers row to satisfy the FK or it returns 500 with SQLite Error 19. - BlogApiTests wraps the existing ResetDatabase with a ResetAndSeedDefaultUser helper for the six mutating tests; the four GET-only and ModelState-only tests keep the bare ResetDatabase. - Side benefit: every test in Yavsc.Blogs.Tests now finishes cleanly instead of hanging at teardown — previously a stuck test held the shared SqliteConnection open and the next tests waited indefinitely. Verified: dotnet test src/Yavsc.Blogs.Tests passes 25/25 green from a clean run, no fixture teardown hang.
2026-08-20 23:59:21 +01:00
private HttpClient NewClient(string subject)
{
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (_, _, _, _) => true
};
var http = new HttpClient(handler)
{
BaseAddress = new Uri(_fixture.Addresses.First(a => a.StartsWith("https://")))
};
// The Blogs fixture disables JwtSecurityTokenHandler's
// inbound claim-type remap, so the JWT's "sub" stays "sub"
// rather than being rewritten to ClaimTypes.NameIdentifier.
// The controller, however, reads the user id via
// User.FindFirstValue(ClaimTypes.NameIdentifier), so we add
// an explicit nameid claim to keep the legacy lookup happy.
http.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue(
"Bearer",
TestTokenIssuer.Issue(
subject,
extraClaims: new[]
{
new System.Security.Claims.Claim(
System.Security.Claims.ClaimTypes.NameIdentifier,
subject),
}));
return http;
}
remove dead 'Comment' field from CircleAuthorizationToBlogPost The bool Comment on CircleAuthorizationToBlogPost was dead code: never read or written by any caller in src/, no UI exposure, no behavioural semantics. The wire DTO (CircleAuthorization in Yavsc.Abstract) doesn't carry it, no reader consumes it, and the PostIt client builds its payload without it. What changes: - src/Yavsc.Server/Models/Access/CircleAuthorizationToBlogPost.cs: remove the property. - src/Yavsc.Blogs.Tests/BlogAclApiTests.cs: drop 'Comment = true' from the existing test payload and trim the now-inaccurate XML doc comment ('CircleId + BlogPostId + Comment' -> 'CircleId + BlogPostId'). Also adds a new [Fact] pinning the prod bug reported on 2026-08-21 (HTTP 500 'BlogPostId is unknown' when PostIt POSTs the bare { circleId } shape). That test stays red: the real fix for the 500 is in PostIt (payload needs blogPostId) + on the wire DTO + server-side validation, and lives in a follow-up commit. Migration: - src/Yavsc.Org/Migrations/20260820232152_DropCommentFromCircleAuthorizationToBlogPost drops the boolean 'Comment' column on CircleAuthorizationToBlogPost. The generated scaffold also wanted to drop three 'ClientId1' shadow FK columns on ClientScopes / ClientRedirectUris / ClientGrantTypes (from leftover HasOne<Client>() overrides in ApplicationDbContext.OnModelCreating); those were removed from the .cs to keep the migration scoped to this fix. Cleaning up the shadow property declarations themselves is left as a separate task. The ModelSnapshot still reflects the shadow 'ClientId1' columns intentionally: they exist in the prod database today (all NULL), and EF will rescaffold a drop migration for them on the next 'migrations add' regardless. No data loss.
2026-08-21 00:27:27 +01:00
/// <summary>
/// Reproduces the prod 500 logged on 2026-08-21 on mercure:
/// <c>InvalidOperationException: The value of
/// 'CircleAuthorizationToBlogPost.BlogPostId' is unknown</c>
/// when <see cref="PostAclDialogViewModel.AddAsync"/> POSTs the
/// shape <c>{ "circleId": &lt;id&gt; }</c> — the exact body the
/// PostIt client builds from <see cref="CircleAuthorization"/>
/// (which only carries <c>CircleId</c>). The server deserialises
/// it into <see cref="CircleAuthorizationToBlogPost"/>, leaves
/// <c>BlogPostId</c> at its <c>default(long) = 0</c>, attaches
/// no <c>Target</c> navigation, and EF Core refuses to INSERT
/// during <c>PrepareToSave()</c>. The fix lives in PostIt
/// (enrich the payload with <c>blogPostId</c> + <c>comment</c>)
/// and on the wire DTO (<see cref="CircleAuthorization"/> must
/// carry those fields); the server validates. Until that ships,
/// this test stays red.
/// </summary>
[Fact]
public async Task PostCircleAuthorization_returns_201_when_payload_mirrors_PostIt_shape_against_existing_circle_named_test()
{
// 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.
acl post: never 500 regression sentinel + async CheckOwner + fixture seed The hard rule on POST /api/v1/blogacl is: a 500 is never acceptable, regardless of the payload shape. The prod 500 logged on 2026-08-21 on mercure was caused by the PostIt client sending { circleId } only, which the server deserialised into CircleAuthorizationToBlogPost with BlogPostId = default(long) = 0; EF Core refused the INSERT with InvalidOperationException: The value of 'CircleAuthorizationToBlogPost.BlogPostId' is unknown. The PostIt fix lives in b82b6722 (enrich the payload with blogPostId). The server-side guard lives in this commit: - BlogAclApiController.CheckOwner is now async and uses FirstOrDefaultAsync instead of First, so it does not deadlock the request thread and returns false on a missing circle (which the controller already maps to ChallengeResult). - BlogsWebServerFixture now seeds Alice, her Circle and her BlogPost in ConfigurePipelineAsync, once at host startup, against the shared SqliteConnection (Cache=Shared). EnsureCreated is idempotent and runs against the connection that every DbContext resolves through, so the test theory can POST payloads with real FK ids against a schema that actually has the Circle / BlogSpot tables. - BlogAclApiTests: - PostCircleAuthorization_returns_201_when_payload_mirrors_PostIt_shape is the regression sentinel for the prod fix. - PostCircleAuthorization_never_returns_500 is a [Theory] over several payload shapes; any future commit that reintroduces a 500 path turns it red. CleanupAcl at the start of each insert- bearing test isolates against xUnit's no-guarantee-of-order execution: a successful POST in test N would otherwise conflict with test N+1 against the same (CircleId, BlogPostId) pair.
2026-08-21 22:00:27 +01:00
CleanupAcl();
2026-08-28 20:10:32 +01:00
using var http = NewClient(_fixture.DefaultUserLogin);
remove dead 'Comment' field from CircleAuthorizationToBlogPost The bool Comment on CircleAuthorizationToBlogPost was dead code: never read or written by any caller in src/, no UI exposure, no behavioural semantics. The wire DTO (CircleAuthorization in Yavsc.Abstract) doesn't carry it, no reader consumes it, and the PostIt client builds its payload without it. What changes: - src/Yavsc.Server/Models/Access/CircleAuthorizationToBlogPost.cs: remove the property. - src/Yavsc.Blogs.Tests/BlogAclApiTests.cs: drop 'Comment = true' from the existing test payload and trim the now-inaccurate XML doc comment ('CircleId + BlogPostId + Comment' -> 'CircleId + BlogPostId'). Also adds a new [Fact] pinning the prod bug reported on 2026-08-21 (HTTP 500 'BlogPostId is unknown' when PostIt POSTs the bare { circleId } shape). That test stays red: the real fix for the 500 is in PostIt (payload needs blogPostId) + on the wire DTO + server-side validation, and lives in a follow-up commit. Migration: - src/Yavsc.Org/Migrations/20260820232152_DropCommentFromCircleAuthorizationToBlogPost drops the boolean 'Comment' column on CircleAuthorizationToBlogPost. The generated scaffold also wanted to drop three 'ClientId1' shadow FK columns on ClientScopes / ClientRedirectUris / ClientGrantTypes (from leftover HasOne<Client>() overrides in ApplicationDbContext.OnModelCreating); those were removed from the .cs to keep the migration scoped to this fix. Cleaning up the shadow property declarations themselves is left as a separate task. The ModelSnapshot still reflects the shadow 'ClientId1' columns intentionally: they exist in the prod database today (all NULL), and EF will rescaffold a drop migration for them on the next 'migrations add' regardless. No data loss.
2026-08-21 00:27:27 +01:00
var payload = new PostAccessControlRulePayload
remove dead 'Comment' field from CircleAuthorizationToBlogPost The bool Comment on CircleAuthorizationToBlogPost was dead code: never read or written by any caller in src/, no UI exposure, no behavioural semantics. The wire DTO (CircleAuthorization in Yavsc.Abstract) doesn't carry it, no reader consumes it, and the PostIt client builds its payload without it. What changes: - src/Yavsc.Server/Models/Access/CircleAuthorizationToBlogPost.cs: remove the property. - src/Yavsc.Blogs.Tests/BlogAclApiTests.cs: drop 'Comment = true' from the existing test payload and trim the now-inaccurate XML doc comment ('CircleId + BlogPostId + Comment' -> 'CircleId + BlogPostId'). Also adds a new [Fact] pinning the prod bug reported on 2026-08-21 (HTTP 500 'BlogPostId is unknown' when PostIt POSTs the bare { circleId } shape). That test stays red: the real fix for the 500 is in PostIt (payload needs blogPostId) + on the wire DTO + server-side validation, and lives in a follow-up commit. Migration: - src/Yavsc.Org/Migrations/20260820232152_DropCommentFromCircleAuthorizationToBlogPost drops the boolean 'Comment' column on CircleAuthorizationToBlogPost. The generated scaffold also wanted to drop three 'ClientId1' shadow FK columns on ClientScopes / ClientRedirectUris / ClientGrantTypes (from leftover HasOne<Client>() overrides in ApplicationDbContext.OnModelCreating); those were removed from the .cs to keep the migration scoped to this fix. Cleaning up the shadow property declarations themselves is left as a separate task. The ModelSnapshot still reflects the shadow 'ClientId1' columns intentionally: they exist in the prod database today (all NULL), and EF will rescaffold a drop migration for them on the next 'migrations add' regardless. No data loss.
2026-08-21 00:27:27 +01:00
{
2026-08-21 20:45:46 +01:00
CircleId = _fixture.CircleId,
BlogPostId = _fixture.PostId
remove dead 'Comment' field from CircleAuthorizationToBlogPost The bool Comment on CircleAuthorizationToBlogPost was dead code: never read or written by any caller in src/, no UI exposure, no behavioural semantics. The wire DTO (CircleAuthorization in Yavsc.Abstract) doesn't carry it, no reader consumes it, and the PostIt client builds its payload without it. What changes: - src/Yavsc.Server/Models/Access/CircleAuthorizationToBlogPost.cs: remove the property. - src/Yavsc.Blogs.Tests/BlogAclApiTests.cs: drop 'Comment = true' from the existing test payload and trim the now-inaccurate XML doc comment ('CircleId + BlogPostId + Comment' -> 'CircleId + BlogPostId'). Also adds a new [Fact] pinning the prod bug reported on 2026-08-21 (HTTP 500 'BlogPostId is unknown' when PostIt POSTs the bare { circleId } shape). That test stays red: the real fix for the 500 is in PostIt (payload needs blogPostId) + on the wire DTO + server-side validation, and lives in a follow-up commit. Migration: - src/Yavsc.Org/Migrations/20260820232152_DropCommentFromCircleAuthorizationToBlogPost drops the boolean 'Comment' column on CircleAuthorizationToBlogPost. The generated scaffold also wanted to drop three 'ClientId1' shadow FK columns on ClientScopes / ClientRedirectUris / ClientGrantTypes (from leftover HasOne<Client>() overrides in ApplicationDbContext.OnModelCreating); those were removed from the .cs to keep the migration scoped to this fix. Cleaning up the shadow property declarations themselves is left as a separate task. The ModelSnapshot still reflects the shadow 'ClientId1' columns intentionally: they exist in the prod database today (all NULL), and EF will rescaffold a drop migration for them on the next 'migrations add' regardless. No data loss.
2026-08-21 00:27:27 +01:00
};
2026-08-28 21:33:11 +01:00
var response = await http.PostAsJsonAsync(
BlogAclUrl(), payload,
2026-08-21 20:25:13 +01:00
TestContext.Current.CancellationToken);
remove dead 'Comment' field from CircleAuthorizationToBlogPost The bool Comment on CircleAuthorizationToBlogPost was dead code: never read or written by any caller in src/, no UI exposure, no behavioural semantics. The wire DTO (CircleAuthorization in Yavsc.Abstract) doesn't carry it, no reader consumes it, and the PostIt client builds its payload without it. What changes: - src/Yavsc.Server/Models/Access/CircleAuthorizationToBlogPost.cs: remove the property. - src/Yavsc.Blogs.Tests/BlogAclApiTests.cs: drop 'Comment = true' from the existing test payload and trim the now-inaccurate XML doc comment ('CircleId + BlogPostId + Comment' -> 'CircleId + BlogPostId'). Also adds a new [Fact] pinning the prod bug reported on 2026-08-21 (HTTP 500 'BlogPostId is unknown' when PostIt POSTs the bare { circleId } shape). That test stays red: the real fix for the 500 is in PostIt (payload needs blogPostId) + on the wire DTO + server-side validation, and lives in a follow-up commit. Migration: - src/Yavsc.Org/Migrations/20260820232152_DropCommentFromCircleAuthorizationToBlogPost drops the boolean 'Comment' column on CircleAuthorizationToBlogPost. The generated scaffold also wanted to drop three 'ClientId1' shadow FK columns on ClientScopes / ClientRedirectUris / ClientGrantTypes (from leftover HasOne<Client>() overrides in ApplicationDbContext.OnModelCreating); those were removed from the .cs to keep the migration scoped to this fix. Cleaning up the shadow property declarations themselves is left as a separate task. The ModelSnapshot still reflects the shadow 'ClientId1' columns intentionally: they exist in the prod database today (all NULL), and EF will rescaffold a drop migration for them on the next 'migrations add' regardless. No data loss.
2026-08-21 00:27:27 +01:00
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
}
2026-08-21 19:37:22 +01:00
/// <summary>
/// Payload templates for <see cref="PostCircleAuthorization_never_returns_500"/>.
/// Each row carries the shape we want to POST; <c>-1L</c> and
/// <c>-2L</c> are negative sentinels that the test substitutes
/// with the ids of freshly seeded <c>Circle</c> / <c>BlogPost</c>
/// rows before sending, so every shape lands against a real
/// principal entity and the seeded fixtures are not dead.
/// </summary>
acl post: never 500 regression sentinel + async CheckOwner + fixture seed The hard rule on POST /api/v1/blogacl is: a 500 is never acceptable, regardless of the payload shape. The prod 500 logged on 2026-08-21 on mercure was caused by the PostIt client sending { circleId } only, which the server deserialised into CircleAuthorizationToBlogPost with BlogPostId = default(long) = 0; EF Core refused the INSERT with InvalidOperationException: The value of 'CircleAuthorizationToBlogPost.BlogPostId' is unknown. The PostIt fix lives in b82b6722 (enrich the payload with blogPostId). The server-side guard lives in this commit: - BlogAclApiController.CheckOwner is now async and uses FirstOrDefaultAsync instead of First, so it does not deadlock the request thread and returns false on a missing circle (which the controller already maps to ChallengeResult). - BlogsWebServerFixture now seeds Alice, her Circle and her BlogPost in ConfigurePipelineAsync, once at host startup, against the shared SqliteConnection (Cache=Shared). EnsureCreated is idempotent and runs against the connection that every DbContext resolves through, so the test theory can POST payloads with real FK ids against a schema that actually has the Circle / BlogSpot tables. - BlogAclApiTests: - PostCircleAuthorization_returns_201_when_payload_mirrors_PostIt_shape is the regression sentinel for the prod fix. - PostCircleAuthorization_never_returns_500 is a [Theory] over several payload shapes; any future commit that reintroduces a 500 path turns it red. CleanupAcl at the start of each insert- bearing test isolates against xUnit's no-guarantee-of-order execution: a successful POST in test N would otherwise conflict with test N+1 against the same (CircleId, BlogPostId) pair.
2026-08-21 22:00:27 +01:00
public static IEnumerable<object[]> BlogAclPayloadsForNever500()
2026-08-21 19:37:22 +01:00
{
// circleId only (the historical bug shape, 2026-08-21 mercure):
// must be rejected, never 500.
acl post: never 500 regression sentinel + async CheckOwner + fixture seed The hard rule on POST /api/v1/blogacl is: a 500 is never acceptable, regardless of the payload shape. The prod 500 logged on 2026-08-21 on mercure was caused by the PostIt client sending { circleId } only, which the server deserialised into CircleAuthorizationToBlogPost with BlogPostId = default(long) = 0; EF Core refused the INSERT with InvalidOperationException: The value of 'CircleAuthorizationToBlogPost.BlogPostId' is unknown. The PostIt fix lives in b82b6722 (enrich the payload with blogPostId). The server-side guard lives in this commit: - BlogAclApiController.CheckOwner is now async and uses FirstOrDefaultAsync instead of First, so it does not deadlock the request thread and returns false on a missing circle (which the controller already maps to ChallengeResult). - BlogsWebServerFixture now seeds Alice, her Circle and her BlogPost in ConfigurePipelineAsync, once at host startup, against the shared SqliteConnection (Cache=Shared). EnsureCreated is idempotent and runs against the connection that every DbContext resolves through, so the test theory can POST payloads with real FK ids against a schema that actually has the Circle / BlogSpot tables. - BlogAclApiTests: - PostCircleAuthorization_returns_201_when_payload_mirrors_PostIt_shape is the regression sentinel for the prod fix. - PostCircleAuthorization_never_returns_500 is a [Theory] over several payload shapes; any future commit that reintroduces a 500 path turns it red. CleanupAcl at the start of each insert- bearing test isolates against xUnit's no-guarantee-of-order execution: a successful POST in test N would otherwise conflict with test N+1 against the same (CircleId, BlogPostId) pair.
2026-08-21 22:00:27 +01:00
return new object[][]
{
[
new PostAccessControlRulePayload
{
BlogPostId = -2,
CircleId = -1
}
],
[new PostAccessControlRulePayload
{
BlogPostId = 1,
CircleId = -1
}
],
[new PostAccessControlRulePayload
{
BlogPostId = 1,
CircleId = 1
}
]
} ;
2026-08-21 19:37:22 +01:00
}
/// <summary>
/// Hard rule (Paul, 2026-08-21): a 500 is never acceptable
/// </summary>
[Theory]
[MemberData(nameof(BlogAclPayloadsForNever500))]
acl post: never 500 regression sentinel + async CheckOwner + fixture seed The hard rule on POST /api/v1/blogacl is: a 500 is never acceptable, regardless of the payload shape. The prod 500 logged on 2026-08-21 on mercure was caused by the PostIt client sending { circleId } only, which the server deserialised into CircleAuthorizationToBlogPost with BlogPostId = default(long) = 0; EF Core refused the INSERT with InvalidOperationException: The value of 'CircleAuthorizationToBlogPost.BlogPostId' is unknown. The PostIt fix lives in b82b6722 (enrich the payload with blogPostId). The server-side guard lives in this commit: - BlogAclApiController.CheckOwner is now async and uses FirstOrDefaultAsync instead of First, so it does not deadlock the request thread and returns false on a missing circle (which the controller already maps to ChallengeResult). - BlogsWebServerFixture now seeds Alice, her Circle and her BlogPost in ConfigurePipelineAsync, once at host startup, against the shared SqliteConnection (Cache=Shared). EnsureCreated is idempotent and runs against the connection that every DbContext resolves through, so the test theory can POST payloads with real FK ids against a schema that actually has the Circle / BlogSpot tables. - BlogAclApiTests: - PostCircleAuthorization_returns_201_when_payload_mirrors_PostIt_shape is the regression sentinel for the prod fix. - PostCircleAuthorization_never_returns_500 is a [Theory] over several payload shapes; any future commit that reintroduces a 500 path turns it red. CleanupAcl at the start of each insert- bearing test isolates against xUnit's no-guarantee-of-order execution: a successful POST in test N would otherwise conflict with test N+1 against the same (CircleId, BlogPostId) pair.
2026-08-21 22:00:27 +01:00
public async Task PostCircleAuthorization_never_returns_500(PostAccessControlRulePayload payload)
2026-08-21 19:37:22 +01:00
{
2026-08-28 20:10:32 +01:00
using var http = NewClient(_fixture.DefaultUserLogin);
2026-08-21 19:37:22 +01:00
var response = await http.PostAsJsonAsync(
BlogAclUrl(), payload,
TestContext.Current.CancellationToken);
Assert.NotEqual(HttpStatusCode.InternalServerError, response.StatusCode);
}
acl post: never 500 regression sentinel + async CheckOwner + fixture seed The hard rule on POST /api/v1/blogacl is: a 500 is never acceptable, regardless of the payload shape. The prod 500 logged on 2026-08-21 on mercure was caused by the PostIt client sending { circleId } only, which the server deserialised into CircleAuthorizationToBlogPost with BlogPostId = default(long) = 0; EF Core refused the INSERT with InvalidOperationException: The value of 'CircleAuthorizationToBlogPost.BlogPostId' is unknown. The PostIt fix lives in b82b6722 (enrich the payload with blogPostId). The server-side guard lives in this commit: - BlogAclApiController.CheckOwner is now async and uses FirstOrDefaultAsync instead of First, so it does not deadlock the request thread and returns false on a missing circle (which the controller already maps to ChallengeResult). - BlogsWebServerFixture now seeds Alice, her Circle and her BlogPost in ConfigurePipelineAsync, once at host startup, against the shared SqliteConnection (Cache=Shared). EnsureCreated is idempotent and runs against the connection that every DbContext resolves through, so the test theory can POST payloads with real FK ids against a schema that actually has the Circle / BlogSpot tables. - BlogAclApiTests: - PostCircleAuthorization_returns_201_when_payload_mirrors_PostIt_shape is the regression sentinel for the prod fix. - PostCircleAuthorization_never_returns_500 is a [Theory] over several payload shapes; any future commit that reintroduces a 500 path turns it red. CleanupAcl at the start of each insert- bearing test isolates against xUnit's no-guarantee-of-order execution: a successful POST in test N would otherwise conflict with test N+1 against the same (CircleId, BlogPostId) pair.
2026-08-21 22:00:27 +01:00
[Fact]
async Task PostCircleAuthorization_dosent_return_500 ()
{
CleanupAcl();
await PostCircleAuthorization_never_returns_500(
new PostAccessControlRulePayload
{
BlogPostId = -1,
CircleId = _fixture.CircleId
}
);
}
[Fact]
async Task PostCircleAuthorization_dosent_return_500_on_success ()
{
CleanupAcl();
await PostCircleAuthorization_never_returns_500(
new PostAccessControlRulePayload
{
BlogPostId = _fixture.PostId,
CircleId = _fixture.CircleId
}
);
}
2026-08-28 20:10:32 +01:00
[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",
false,
new String[]
{
_fixture.DefaultUserLogin,
"tester"
});
using var http = NewClient(_fixture.DefaultUserLogin );
// Create a minimal BlogPost. The server assigns Id, so we
// send 0 + an explicit AuthorId; the production
// BlogSpotService.Create() tolerates that.
var draft = new BlogPost
{
Id = 0,
Title = "Premier billet",
AuthorId = "tester",
Article = "Contenu de test.",
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow,
ACL = new List<CircleAuthorizationToBlogPost>(
new CircleAuthorizationToBlogPost[]
{
new CircleAuthorizationToBlogPost
{
CircleId = _fixture.CircleId,
BlogPostId = _fixture.PostId
}
}
)
};
var postResponse = await http.PostAsJsonAsync(
BlogUrl(),
draft,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, postResponse.StatusCode);
// The POST returns the server-issued post (with a real Id).
var created = await postResponse.Content.ReadFromJsonAsync<BlogPost>(
TestContext.Current.CancellationToken
);
Assert.NotNull(created);
Assert.NotEqual(0, created!.Id);
Assert.Equal(draft.Title, created.Title);
// The list should now contain exactly one entry.
var listResponse = await http.GetAsync(
BlogUrl(),
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, listResponse.StatusCode);
using var doc = JsonDocument.Parse(await listResponse.Content.ReadAsStringAsync(
TestContext.Current.CancellationToken
));
Assert.Equal(JsonValueKind.Array, doc.RootElement.ValueKind);
Assert.Equal(2, doc.RootElement.GetArrayLength());
Assert.Equal(created.Id, doc.RootElement[0].GetProperty("id").GetInt64());
// detail should return the same post, with ACL and tags.
var detailResponse = await http.GetAsync(
$"{BlogUrl()}/{created.Id}",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, detailResponse.StatusCode);
using var detailDoc = JsonDocument.Parse(await detailResponse.Content.ReadAsStringAsync(
TestContext.Current.CancellationToken
));
Assert.Equal(JsonValueKind.Object, detailDoc.RootElement.ValueKind);
Assert.Equal(created.Id, detailDoc.RootElement.GetProperty("id").GetInt64());
Assert.Equal(1, detailDoc.RootElement.GetProperty("acl").GetArrayLength());
}
feat(postit): circles+ACL UI, blog fixture→SQLite, seed default user Bundled end-of-branch commit on feat/postit-acl-members. PostIt UI for circles + per-post ACL - Reorganise PostIt.Tests into Auth/ and Blogs/ subfolders (Bearer/OIDC scope tests vs. blog API fakes live where they belong) and introduces PostItHeadlessCollection so the Avalonia.Headless tests share a single xUnit collection instead of contending with the EF-Core test host. - Adds BlogAclApiTests (a brand-new behavioural layer over POST /api/v1/blogacl) and the fakes it relies on (BlogApiTestFakes, BlogPostAuthorDtoTests, AddCircleMember DialogTests); pulls UserId-through-OIDC-sub path into BearerScopeTests / FakeAuthorizingBrowser / OidcStubAuthority. - App.axaml.cs gets a small PushPageAsync touch-up the new tests rely on. - Drops UnitTest1.cs (xUnit scaffold, never used). Yavsc.Blogs.Tests — SQLite instead of InMemory - Bumps Yavsc.Blogs.Tests.csproj on Microsoft.EntityFrameworkCore.Sqlite and rewrites BlogsWebServerFixture to hold a single shared SqliteConnection (Cache=Shared) for the fixture lifetime, with a sync Dispose close to dodge async teardown hangs. Reason: the EF Core InMemory provider silently ignores FKs, which masked the kind of bug we are about to pin in the ACL tests. SQLite enforces them, so any future INSERT that forgets to seed its parent rows fails loudly here instead of passing the test and breaking prod. - PublishEndpointTests and BlogApiSmokeTests get a one-line tweak to follow the new connection lifecycle. Foreign-key fallout: seed the default user in the fixture - Adds BlogsWebServerFixture.SeedUser(userName). Now that SQLite enforces BlogPost.AuthorId → AspNetUsers.Id, every test that POST/PUT/DELETE a BlogPost and sends AuthorId= 'tester' in the payload needs an AspNetUsers row to satisfy the FK or it returns 500 with SQLite Error 19. - BlogApiTests wraps the existing ResetDatabase with a ResetAndSeedDefaultUser helper for the six mutating tests; the four GET-only and ModelState-only tests keep the bare ResetDatabase. - Side benefit: every test in Yavsc.Blogs.Tests now finishes cleanly instead of hanging at teardown — previously a stuck test held the shared SqliteConnection open and the next tests waited indefinitely. Verified: dotnet test src/Yavsc.Blogs.Tests passes 25/25 green from a clean run, no fixture teardown hang.
2026-08-20 23:59:21 +01:00
}