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.
This commit is contained in:
parent
6825f74308
commit
a44c04ad77
22 changed files with 806 additions and 460 deletions
161
src/Yavsc.Blogs.Tests/BlogAclApiTests.cs
Normal file
161
src/Yavsc.Blogs.Tests/BlogAclApiTests.cs
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
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
|
||||
/// <c>CircleAuthorizationToBlogPost</c> (CircleId + BlogPostId + Comment).
|
||||
///
|
||||
/// <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;
|
||||
}
|
||||
|
||||
/// <summary>Reset the in-memory database and seed <c>alice</c>.
|
||||
/// The shared SQLite <c>:memory:</c> store persists across
|
||||
/// requests, so each test starts from a clean slate.</summary>
|
||||
private void ResetDatabaseWithAlice()
|
||||
{
|
||||
using var scope = _fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
db.Database.EnsureDeleted();
|
||||
db.Database.EnsureCreated();
|
||||
|
||||
db.Users.Add(new ApplicationUser
|
||||
{
|
||||
Id = "alice",
|
||||
UserName = "alice",
|
||||
Email = "alice@example.com",
|
||||
EmailConfirmed = true,
|
||||
FullName = "Alice Dupont",
|
||||
Avatar = "/avatars/alice.png",
|
||||
});
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
/// <summary>Create a circle owned by <paramref name="ownerId"/>
|
||||
/// directly in the SQLite store and return its server-assigned
|
||||
/// id.</summary>
|
||||
private long SeedCircle(string ownerId, string name)
|
||||
{
|
||||
using var scope = _fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
var circle = new Circle { OwnerId = ownerId, Name = name };
|
||||
db.Circle.Add(circle);
|
||||
db.SaveChanges();
|
||||
return circle.Id;
|
||||
}
|
||||
|
||||
/// <summary>Create a blog post owned by <paramref name="authorId"/>
|
||||
/// directly in the SQLite store and return its server-assigned
|
||||
/// id.</summary>
|
||||
private long SeedBlogPost(string authorId, string title)
|
||||
{
|
||||
using var scope = _fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
var post = new BlogPost
|
||||
{
|
||||
AuthorId = authorId,
|
||||
Title = title,
|
||||
Article = "Test article body.",
|
||||
DateCreated = DateTime.UtcNow,
|
||||
DateModified = DateTime.UtcNow,
|
||||
};
|
||||
db.BlogSpot.Add(post);
|
||||
db.SaveChanges();
|
||||
return post.Id;
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
ResetDatabaseWithAlice();
|
||||
var circleId = SeedCircle("alice", "Famille");
|
||||
var postId = SeedBlogPost("alice", "Billet de test");
|
||||
using var http = NewClient("alice");
|
||||
|
||||
// Mirror PostIt's payload: scalar FK ids only, no nav props.
|
||||
var payload = new CircleAuthorizationToBlogPost
|
||||
{
|
||||
CircleId = circleId,
|
||||
BlogPostId = postId,
|
||||
Comment = true,
|
||||
};
|
||||
|
||||
var response = await http.PostAsJsonAsync(BlogAclUrl(), payload);
|
||||
|
||||
// Expected: 201 Created (per controller line 133: return
|
||||
// CreatedAtRoute("GetCircleAuthorizationToBlogPost", ...)).
|
||||
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ namespace Yavsc.Blogs.Tests;
|
|||
/// surface. The first behavioural test (GET /api/v1/blog returns
|
||||
/// 200) lands in a follow-up commit.
|
||||
/// </summary>
|
||||
[Collection("Yavsc Blogs")]
|
||||
public sealed class BlogApiSmokeTests : IClassFixture<BlogsWebServerFixture>
|
||||
{
|
||||
private readonly BlogsWebServerFixture _fixture;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ namespace Yavsc.Blogs.Tests;
|
|||
/// header (or sending a token signed with the wrong key) gets a
|
||||
/// 401 back from the framework.
|
||||
/// </summary>
|
||||
[Collection("JwtClaimMapping")]
|
||||
[Collection("Yavsc Blogs")]
|
||||
public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
|
||||
{
|
||||
private readonly BlogsWebServerFixture _fixture;
|
||||
|
|
@ -45,6 +45,21 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
db.Database.EnsureCreated();
|
||||
}
|
||||
|
||||
/// <summary>Reset the database and seed the
|
||||
/// <c>tester</c> <see cref="ApplicationUser"/> row. Required
|
||||
/// for any test that POST/PUT/DELETE a <c>BlogPost</c>:
|
||||
/// <c>BlogPost.AuthorId</c> is a FK to
|
||||
/// <c>AspNetUsers.Id</c>, and SQLite (unlike the EF Core
|
||||
/// InMemory provider) enforces it. Without the seed, the
|
||||
/// POST handler hits
|
||||
/// <c>SQLite Error 19: 'FOREIGN KEY constraint failed'</c>
|
||||
/// at <c>SaveChanges</c> and the controller returns 500.</summary>
|
||||
private void ResetAndSeedDefaultUser()
|
||||
{
|
||||
ResetDatabase();
|
||||
_fixture.SeedUser("tester");
|
||||
}
|
||||
|
||||
/// <summary>The fixture's <c>WebApplication</c> is bound to
|
||||
/// <c>https://localhost:<random></c> via
|
||||
/// <see cref="WebHostFixture.Addresses"/>. We pick the first
|
||||
|
|
@ -116,7 +131,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
[Fact]
|
||||
public async Task PostBlog_creates_a_post_and_Get_returns_it_in_the_list()
|
||||
{
|
||||
ResetDatabase();
|
||||
ResetAndSeedDefaultUser();
|
||||
using var http = NewClient();
|
||||
|
||||
// Create a minimal BlogPost. The server assigns Id, so we
|
||||
|
|
@ -154,7 +169,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
[Fact]
|
||||
public async Task PostBlog_sets_AuthorId_on_created_post_and_list_entry()
|
||||
{
|
||||
ResetDatabase();
|
||||
ResetAndSeedDefaultUser();
|
||||
using var http = NewClient(subject: "tester");
|
||||
|
||||
var draft = new BlogPost
|
||||
|
|
@ -186,7 +201,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
[Fact]
|
||||
public async Task PostBlogComment_returns_201_for_existing_post()
|
||||
{
|
||||
ResetDatabase();
|
||||
ResetAndSeedDefaultUser();
|
||||
using var http = NewClient(subject: "tester");
|
||||
|
||||
var draft = new BlogPost
|
||||
|
|
@ -249,7 +264,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
[Fact]
|
||||
public async Task PutBlog_with_valid_token_and_owner_returns_204_and_Get_reflects_update()
|
||||
{
|
||||
ResetDatabase();
|
||||
ResetAndSeedDefaultUser();
|
||||
// The JWT's sub must match the post's AuthorId:
|
||||
// PermissionHandler.IsOwner checks blog.AuthorId == user.GetUserId(),
|
||||
// and UserHelpers.GetUserId reads "sub" off the principal.
|
||||
|
|
@ -300,7 +315,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
[Fact]
|
||||
public async Task DeleteBlog_removes_a_post_and_Get_returns_an_empty_list()
|
||||
{
|
||||
ResetDatabase();
|
||||
ResetAndSeedDefaultUser();
|
||||
using var http = NewClient();
|
||||
|
||||
// Seed a post we can delete.
|
||||
|
|
@ -342,7 +357,7 @@ public sealed class BlogApiTests : IClassFixture<BlogsWebServerFixture>
|
|||
// ModelState validation starts rejecting the PostIt payload
|
||||
// (missing field, wrong casing, etc.), this test fails
|
||||
// before the regression reaches a user.
|
||||
ResetDatabase();
|
||||
ResetAndSeedDefaultUser();
|
||||
using var http = NewClient(subject: "tester");
|
||||
|
||||
// Mirrors what MainPageViewModel.Save builds: a BlogPost with
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ using System.Text;
|
|||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Yavsc.Blogs.Controllers;
|
||||
|
|
@ -14,14 +14,20 @@ using Yavsc.Tests.Shared;
|
|||
namespace Yavsc.Blogs.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Test host for the Yavsc.Blogs API surface. Specialisation of
|
||||
/// <see cref="WebHostFixture"/> that wires up only the bits the
|
||||
/// blog API actually depends on:
|
||||
/// Shared integration-test host for the Yavsc.Blogs API surface.
|
||||
/// Specialisation of <see cref="WebHostFixture"/> that wires up
|
||||
/// only the bits the blog API actually depends on:
|
||||
///
|
||||
/// <list type="bullet">
|
||||
/// <item><description>An in-memory <see cref="ApplicationDbContext"/>
|
||||
/// (the real one — no mock) so <c>BlogSpotService.Index</c> can run
|
||||
/// against an empty table and return an empty list.</description></item>
|
||||
/// <item><description>A SQLite <c>:memory:</c> database
|
||||
/// (<see cref="Microsoft.EntityFrameworkCore.Sqlite"/>) backed
|
||||
/// by a single shared <see cref="SqliteConnection"/> held open
|
||||
/// for the lifetime of the host. SQLite enforces real foreign
|
||||
/// keys and real transactional semantics, so the tests see the
|
||||
/// same INSERT-time FK validation a production Postgres host
|
||||
/// would — unlike the EF Core InMemory provider, which silently
|
||||
/// ignores FKs and masks bugs that surface only against a real
|
||||
/// relational engine.</description></item>
|
||||
/// <item><description>A trivial <see cref="IFileSystemAuthManager"/>
|
||||
/// stub: the GET index path doesn't read the file system, so any
|
||||
/// implementation is fine.</description></item>
|
||||
|
|
@ -44,31 +50,58 @@ namespace Yavsc.Blogs.Tests;
|
|||
///
|
||||
/// No IdentityServer, no SMTP, no static assets — the Org fixture
|
||||
/// owns all of that and we don't need any of it for blog integration
|
||||
/// tests.
|
||||
/// tests. Marked <see cref="CollectionDefinitionAttribute"/> so the
|
||||
/// host is shared across every <c>[Collection("Yavsc Blogs")]</c>
|
||||
/// test class: one host, one SQLite DB, one Kestrel port.
|
||||
/// </summary>
|
||||
[CollectionDefinition("Yavsc Blogs")]
|
||||
public sealed class BlogsWebServerFixture : WebHostFixture
|
||||
{
|
||||
protected override int HttpsPort => 5103;
|
||||
|
||||
private InMemoryDatabaseRoot? _inMemoryRoot;
|
||||
// A single SqliteConnection held open at the static level,
|
||||
// mirroring how Yavsc.Org.Tests.WebServerFixture hoists its
|
||||
// shared configuration into static slots. Closing the
|
||||
// connection destroys the in-memory database — so we close
|
||||
// it only when the last fixture instance is disposed (see
|
||||
// Dispose below), exactly when WebHostFixture tears down the
|
||||
// host.
|
||||
private static SqliteConnection? _sharedSqliteConnection;
|
||||
private static readonly object _sqliteLock = new();
|
||||
|
||||
protected override WebApplication BuildApp(WebApplicationBuilder builder)
|
||||
{
|
||||
// Use the real ApplicationDbContext with an in-memory store.
|
||||
// BlogSpotService reads _context.BlogSpot directly, so any
|
||||
// attempt to mock it would be wasted work; the real service
|
||||
// against an empty table returns an empty list, which is
|
||||
// exactly what the first test wants to assert.
|
||||
//
|
||||
// Share a single InMemoryDatabaseRoot across the test
|
||||
// lifetime so POST + GET on the same fixture see the same
|
||||
// store. Without the root, EF Core's In-Memory provider
|
||||
// creates independent stores per DbContext in some
|
||||
// configurations, and the second request would see an
|
||||
// empty list even after the first wrote a row.
|
||||
_inMemoryRoot = new InMemoryDatabaseRoot();
|
||||
// Open the shared in-memory connection lazily on the first
|
||||
// fixture construction. Subsequent constructions (xUnit
|
||||
// creates one fixture instance per IClassFixture) reuse
|
||||
// the same connection so all DbContexts across all tests
|
||||
// see the same database.
|
||||
SqliteConnection sharedConnection;
|
||||
lock (_sqliteLock)
|
||||
{
|
||||
if (_sharedSqliteConnection is null)
|
||||
{
|
||||
// Mode=Memory + Cache=Shared gives us a named
|
||||
// in-memory database that every connection string
|
||||
// referencing "File:YavscBlogsTests?mode=memory&cache=shared"
|
||||
// will resolve to the same backing store, as long
|
||||
// as at least one SqliteConnection stays open
|
||||
// against it.
|
||||
_sharedSqliteConnection = new SqliteConnection(
|
||||
"Data Source=YavscBlogsTests;Mode=Memory;Cache=Shared");
|
||||
_sharedSqliteConnection.Open();
|
||||
}
|
||||
sharedConnection = _sharedSqliteConnection;
|
||||
}
|
||||
|
||||
builder.Services.AddDbContext<ApplicationDbContext>(opt =>
|
||||
opt.UseInMemoryDatabase("Yavsc.Blogs.Tests", _inMemoryRoot));
|
||||
// UseSqlite(DbConnection) keeps the connection we just
|
||||
// opened alive for the DbContext's lifetime, instead of
|
||||
// letting EF open and close its own. Without this,
|
||||
// each DbContext would get a fresh connection pointing
|
||||
// at an empty :memory: store and nothing would persist
|
||||
// across requests.
|
||||
opt.UseSqlite(sharedConnection));
|
||||
|
||||
// Trivial file-system auth: the GET index path never calls
|
||||
// into it, but the DI container needs an instance.
|
||||
|
|
@ -168,6 +201,75 @@ public sealed class BlogsWebServerFixture : WebHostFixture
|
|||
return app;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Seed an <see cref="ApplicationUser"/> in the shared
|
||||
/// SQLite store, so tests that POST/PUT/DELETE a
|
||||
/// <c>BlogPost</c> (whose <c>AuthorId</c> is a FK to
|
||||
/// <c>AspNetUsers.Id</c>) don't trip the FK constraint that
|
||||
/// SQLite enforces but the EF Core InMemory provider silently
|
||||
/// ignored. Idempotent on <paramref name="userName"/>: a
|
||||
/// second call for the same id is a no-op (the user already
|
||||
/// exists).</summary>
|
||||
/// <param name="userName">Both the PK id and the login name.
|
||||
/// The JWT subject in tests is this same string, so seeding
|
||||
/// this id is enough to make the FK from a
|
||||
/// <c>BlogPost.AuthorId</c> resolve.</param>
|
||||
/// <param name="configure">Optional hook to fill in fields
|
||||
/// like <c>FullName</c> / <c>Avatar</c> / <c>EmailConfirmed</c>
|
||||
/// that downstream tests assert on.</param>
|
||||
public ApplicationUser SeedUser(string userName, Action<ApplicationUser>? configure = null)
|
||||
{
|
||||
using var scope = Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
var existing = db.Users.SingleOrDefault(u => u.Id == userName);
|
||||
if (existing != null) return existing;
|
||||
|
||||
// Email is an alternate key on ApplicationUser; seeding
|
||||
// it explicitly avoids the InMemory provider's null-claim
|
||||
// tracking quirk (cf. PublishEndpointTests.ResetDatabase)
|
||||
// and keeps the column shape realistic for prod.
|
||||
var user = new ApplicationUser
|
||||
{
|
||||
Id = userName,
|
||||
UserName = userName,
|
||||
Email = $"{userName}@example.test",
|
||||
};
|
||||
configure?.Invoke(user);
|
||||
db.Users.Add(user);
|
||||
db.SaveChanges();
|
||||
return user;
|
||||
}
|
||||
|
||||
/// <summary>Trivial <see cref="IFileSystemAuthManager"/> stub. The
|
||||
/// blog API endpoints exercised by the first tests don't read the
|
||||
/// file system, so the implementation can be a no-op.</summary>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ namespace Yavsc.Blogs.Tests;
|
|||
/// in-memory <c>ApplicationDbContext</c>, JWT bearer auth
|
||||
/// via <see cref="TestTokenIssuer"/>.</para>
|
||||
/// </summary>
|
||||
[Collection("JwtClaimMapping")]
|
||||
[Collection("Yavsc Blogs")]
|
||||
public sealed class PublishEndpointTests : IClassFixture<BlogsWebServerFixture>
|
||||
{
|
||||
private readonly BlogsWebServerFixture _fixture;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.v3.common" />
|
||||
<PackageReference Include="xunit.v3.extensibility.core" />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue