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
ec901e1f10
commit
afac287a18
22 changed files with 806 additions and 460 deletions
|
|
@ -1,90 +0,0 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Headless.XUnit;
|
||||
using Avalonia.VisualTree;
|
||||
using Yavsc.Blogspot;
|
||||
using Yavsc.Api.Client;
|
||||
using PostIt.Services;
|
||||
using PostIt.ViewModels;
|
||||
using PostIt.Views;
|
||||
namespace PostIt.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Headless UI tests for the "Save" flow in <see cref="MainPage"/>.
|
||||
/// The pattern is the one <c>SessionStatusBannerTests</c>
|
||||
/// established: <c>[AvaloniaFact]</c>, a <see cref="Window"/>
|
||||
/// hosting the page (via a <see cref="Frame"/> because
|
||||
/// <c>MainPage</c> is a <c>ContentPage</c>), then drive the
|
||||
/// controls through their public surface and assert on what
|
||||
/// <see cref="RecordingYavscApiClient"/> saw go on the wire.
|
||||
///
|
||||
/// <para>The bug we are pinning: the title <c>TextBox</c> is
|
||||
/// currently <c>{Binding SelectedPost.Title, Mode=TwoWay}</c>.
|
||||
/// When <c>SelectedPost is null</c> (i.e. the user has not yet
|
||||
/// clicked an item in the posts list — which is the only state
|
||||
/// in which a brand-new post can be created), the binding has
|
||||
/// no target and the user's keystrokes are silently dropped.
|
||||
/// Clicking "Save" then routes to the VM branch
|
||||
/// <c>if (SelectedPost is null) { new BlogPostDto { Title = string.Empty, ... } }</c>
|
||||
/// which the controller rejects with 400 "The Title field is
|
||||
/// required." This test fails on that branch today and will
|
||||
/// pass once the VM owns a dedicated <c>Title</c>/<c>Article</c>
|
||||
/// buffer that the XAML binds to and the Save command consumes.</para>
|
||||
/// </summary>
|
||||
public class MainPageSaveTests
|
||||
{
|
||||
[AvaloniaFact]
|
||||
public async Task Typing_a_title_then_clicking_Save_sends_that_title_in_the_post_body()
|
||||
{
|
||||
// Arrange: VM with a recording API client, mounted in a
|
||||
// headless window via a Frame (MainPage is a ContentPage,
|
||||
// not a Control, so it needs a navigation host).
|
||||
var recorder = new CallRecorder();
|
||||
var api = new RecordingYavscApiClient(recorder);
|
||||
var blog = new BlogApiClient(api, "http://localhost/");
|
||||
var viewModel = new MainPageViewModel(blog);
|
||||
|
||||
var page = new MainPage { DataContext = viewModel };
|
||||
// MainPage is a ContentPage (a Page, not a Control), so it
|
||||
// must be hosted in a navigation surface. The production
|
||||
// MainWindow.axaml uses NavigationPage, and the API is the
|
||||
// same one App.axaml.cs drives at boot (PushAsync, fire-
|
||||
// and-forget in prod because the page is the top of the
|
||||
// stack immediately).
|
||||
var nav = new NavigationPage();
|
||||
_ = nav.PushAsync(page);
|
||||
var window = new Window { Content = nav };
|
||||
window.Show();
|
||||
|
||||
// Act: type a title into the editor's TextBox without
|
||||
// first selecting a post in the list — the only state in
|
||||
// which a new post can be created. Then click Save.
|
||||
var titleBox = window.GetVisualDescendants()
|
||||
.OfType<TextBox>()
|
||||
.First(t => t.PlaceholderText == "Title");
|
||||
const string typed = "Mon premier billet";
|
||||
titleBox.Text = typed;
|
||||
|
||||
var saveButton = window.GetVisualDescendants()
|
||||
.OfType<Button>()
|
||||
.Single(b => b.Content as string == "Save");
|
||||
saveButton.Command!.Execute(null);
|
||||
|
||||
// The Save command is async (RelayCommand over Task) but
|
||||
// ExecuteAsync would await; the sync Execute enqueues the
|
||||
// task on the dispatcher. Give the dispatcher a chance to
|
||||
// run so the awaited CallAsync has actually fired before
|
||||
// we inspect the recorder.
|
||||
await Task.Delay(200);
|
||||
|
||||
// Assert: the first POST to "blog" carried a BlogPostDto
|
||||
// whose Title is exactly what the user typed. The bug
|
||||
// fails this assertion with Title == string.Empty.
|
||||
Assert.NotEmpty(recorder.Calls);
|
||||
var (method, path, body) = recorder.FirstCall;
|
||||
Assert.Equal(HttpMethod.Post, method);
|
||||
Assert.Equal("blog", path);
|
||||
var sent = Assert.IsType<BlogPostDto>(body);
|
||||
Assert.Equal(typed, sent.Title);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue