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;
|
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
2026-08-21 16:58:27 +01:00
|
|
|
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;
|
|
|
|
|
using Yavsc.Models.Blog;
|
|
|
|
|
using Yavsc.Models.Relationship;
|
|
|
|
|
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
|
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;
|
|
|
|
|
|
2026-08-21 20:25:13 +01:00
|
|
|
|
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
|
|
|
public BlogAclApiTests(BlogsWebServerFixture fixture)
|
|
|
|
|
{
|
|
|
|
|
_fixture = fixture;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-21 20:25:13 +01:00
|
|
|
|
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()
|
|
|
|
|
=> $"{_fixture.Addresses.First(a => a.StartsWith("https://"))}/{APIPrefix}/blogacl";
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>PostIt sends only the FK ids (<c>CircleId</c> +
|
|
|
|
|
/// <c>BlogPostId</c>) plus scalar fields, never the navigation
|
|
|
|
|
/// properties <c>Target</c> / <c>Allowed</c>. The controller
|
|
|
|
|
/// must accept that shape and persist the ACL row.</summary>
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task PostCircleAuthorization_returns_201_when_adding_existing_circle_to_existing_post()
|
|
|
|
|
{
|
|
|
|
|
using var http = NewClient("alice");
|
|
|
|
|
|
|
|
|
|
// Mirror PostIt's payload: scalar FK ids only, no nav props.
|
|
|
|
|
var payload = new CircleAuthorizationToBlogPost
|
|
|
|
|
{
|
2026-08-21 20:45:46 +01:00
|
|
|
CircleId = _fixture.CircleId,
|
|
|
|
|
BlogPostId = _fixture.PostId,
|
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
|
|
|
};
|
|
|
|
|
|
2026-08-21 20:25:13 +01:00
|
|
|
var response = await http.PostAsJsonAsync(BlogAclUrl(), payload,
|
|
|
|
|
TestContext.Current.CancellationToken);
|
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
|
|
|
|
|
|
|
|
// Expected: 201 Created (per controller line 133: return
|
|
|
|
|
// CreatedAtRoute("GetCircleAuthorizationToBlogPost", ...)).
|
|
|
|
|
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
|
|
|
|
}
|
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": <id> }</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.
|
|
|
|
|
using var http = NewClient("alice");
|
|
|
|
|
|
2026-08-21 16:58:27 +01:00
|
|
|
|
|
|
|
|
var payload = new PostAccessControlRulePayload
|
2026-08-21 00:27:27 +01:00
|
|
|
{
|
2026-08-21 20:45:46 +01:00
|
|
|
CircleId = _fixture.CircleId,
|
|
|
|
|
BlogPostId = _fixture.PostId
|
2026-08-21 00:27:27 +01:00
|
|
|
};
|
|
|
|
|
|
2026-08-21 20:25:13 +01:00
|
|
|
var response = await http.PostAsJsonAsync(BlogAclUrl(), payload,
|
|
|
|
|
TestContext.Current.CancellationToken);
|
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>
|
|
|
|
|
public static IEnumerable<PostAccessControlRulePayload?[]> BlogAclPayloadsForNever500()
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
// circleId only (the historical bug shape, 2026-08-21 mercure):
|
|
|
|
|
// must be rejected, never 500.
|
|
|
|
|
yield return new PostAccessControlRulePayload?[]
|
|
|
|
|
{
|
|
|
|
|
new PostAccessControlRulePayload
|
|
|
|
|
{
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
new PostAccessControlRulePayload
|
|
|
|
|
{
|
|
|
|
|
BlogPostId = -1,
|
2026-08-21 20:25:13 +01:00
|
|
|
CircleId = 1
|
2026-08-21 19:37:22 +01:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Empty body: must be rejected at validation/auth, never 500.
|
|
|
|
|
yield return new PostAccessControlRulePayload?[]
|
|
|
|
|
{
|
|
|
|
|
new PostAccessControlRulePayload
|
|
|
|
|
{
|
|
|
|
|
BlogPostId = -1,
|
|
|
|
|
CircleId = -1
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// blogPostId only: must be rejected, never 500.
|
|
|
|
|
yield return new PostAccessControlRulePayload?[]
|
|
|
|
|
{
|
|
|
|
|
new PostAccessControlRulePayload
|
|
|
|
|
{
|
|
|
|
|
BlogPostId = -2,
|
|
|
|
|
CircleId = -1
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Explicit BlogPostId = 0 (default(long)): must be rejected,
|
|
|
|
|
// never 500. This is the precise shape that EF Core's
|
|
|
|
|
// shaper used to crash on.
|
|
|
|
|
yield return new PostAccessControlRulePayload?[]
|
|
|
|
|
{
|
|
|
|
|
new PostAccessControlRulePayload(),
|
|
|
|
|
null
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Hard rule (Paul, 2026-08-21): a 500 is never acceptable
|
|
|
|
|
/// </summary>
|
|
|
|
|
[Theory]
|
|
|
|
|
[MemberData(nameof(BlogAclPayloadsForNever500))]
|
|
|
|
|
public async Task PostCircleAuthorization_never_returns_500(
|
|
|
|
|
Dictionary<string, PostAccessControlRulePayload?> payload)
|
|
|
|
|
{
|
|
|
|
|
using var http = NewClient("alice");
|
|
|
|
|
|
|
|
|
|
var response = await http.PostAsJsonAsync(
|
|
|
|
|
BlogAclUrl(), payload,
|
|
|
|
|
TestContext.Current.CancellationToken);
|
|
|
|
|
|
|
|
|
|
Assert.NotEqual(HttpStatusCode.InternalServerError, response.StatusCode);
|
|
|
|
|
}
|
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
|
|
|
}
|