GET /brief allocated a row on first call (BriefStore.GetOrCreate) — the one endpoint in the backend where a read performed a persisted write. The FE retries GETs automatically, so a transient failure could enter the create path more than once; a lock prevented a duplicate row, but the safety depended on the lock, not on the endpoint being a query. Split GetOrCreate into Get (a pure query) and the already-existing ResetAndCreate (POST /brief/reset owns creation). GET /brief now 404s when the owner has no brief yet. GET /brief/preview used GetOrCreate too, so it gets the same Get + 404 treatment, forced by the split. RB-22 already made BriefStore.load() on the FE tolerate a 404 by calling reset() once; this ticket is what makes that branch live. Updated the brief/preview/org-template backend tests that assumed GET seeded a brief on first call to create one explicitly first, and added a test that GET 404s and writes no row without the fix (verified red beforehand). Regenerated the API client (npm run gen:api). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
103 lines
4.4 KiB
C#
103 lines
4.4 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using BigRegister.Api.Contracts;
|
|
using BigRegister.Api.Data;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
|
|
namespace BigRegister.Tests;
|
|
|
|
/// <summary>
|
|
/// WP-25: the two HTML preview endpoints. Both are excluded from the OpenAPI doc
|
|
/// (see the drift check in the API-client generation step) — these tests hit them
|
|
/// as plain HTTP, the same way the hand-written FE fetch does.
|
|
/// </summary>
|
|
public class PreviewEndpointTests(TestWebApplicationFactory factory) : IClassFixture<TestWebApplicationFactory>
|
|
{
|
|
private const string Registers = OrgTemplateSeed.Registers;
|
|
private readonly HttpClient _client = factory.CreateClient();
|
|
|
|
private static LetterBlockDto FreeText(string id) =>
|
|
new("freeText", id, new RichTextBlockDto(new[] { new ParagraphDto(new[] { new RichTextNodeDto("text", Text: "inhoud") }) }));
|
|
|
|
private static SaveBriefRequest FilledFrom(BriefDto brief)
|
|
{
|
|
var i = 0;
|
|
var sections = brief.Sections
|
|
.Select(s => new LetterSectionDto(s.SectionKey, s.Title, s.Required, s.Required ? new[] { FreeText($"local-{++i}") } : s.Blocks))
|
|
.ToList();
|
|
return new SaveBriefRequest(sections);
|
|
}
|
|
|
|
private static void ResetStores()
|
|
{
|
|
BriefStore.Reset();
|
|
OrgTemplateStore.Reset();
|
|
}
|
|
|
|
private HttpRequestMessage Req(HttpMethod method, string path, string? role = null) =>
|
|
role is null ? new HttpRequestMessage(method, path) : new HttpRequestMessage(method, path) { Headers = { { "X-Role", role } } };
|
|
|
|
[Fact]
|
|
public async Task Preview_of_an_unsent_brief_renders_live_with_a_watermark()
|
|
{
|
|
ResetStores();
|
|
await _client.PostAsync("/api/v1/brief/reset", null); // RB-23: GET no longer seeds — create explicitly
|
|
|
|
var res = await _client.GetAsync("/api/v1/brief/preview");
|
|
res.EnsureSuccessStatusCode();
|
|
Assert.Equal("text/html", res.Content.Headers.ContentType?.MediaType);
|
|
var html = await res.Content.ReadAsStringAsync();
|
|
Assert.Contains("<div class=\"preview-watermark\"", html);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Preview_of_a_sent_brief_serves_the_archive_unchanged_after_a_republish()
|
|
{
|
|
ResetStores();
|
|
var resetRes = await _client.PostAsync("/api/v1/brief/reset", null); // RB-23: create explicitly
|
|
var brief = (await resetRes.Content.ReadFromJsonAsync<BriefViewDto>())!.Brief;
|
|
await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(brief));
|
|
await _client.SendAsync(Req(HttpMethod.Post, "/api/v1/brief/submit"));
|
|
await _client.SendAsync(Req(HttpMethod.Post, "/api/v1/brief/approve", role: "approver"));
|
|
await _client.SendAsync(Req(HttpMethod.Post, "/api/v1/brief/send"));
|
|
|
|
var sentHtml = await (await _client.GetAsync("/api/v1/brief/preview")).Content.ReadAsStringAsync();
|
|
Assert.DoesNotContain("<div class=\"preview-watermark\"", sentHtml);
|
|
Assert.Contains("BIG-register", sentHtml);
|
|
|
|
// Republish the org template under a new name.
|
|
var adminView = await _client.SendAsync(Req(HttpMethod.Get, $"/api/v1/admin/org-template/{Registers}", role: "admin"));
|
|
var draft = (await adminView.Content.ReadFromJsonAsync<OrgTemplateAdminViewDto>())!.Draft;
|
|
await _client.SendAsync(new HttpRequestMessage(HttpMethod.Put, $"/api/v1/admin/org-template/{Registers}")
|
|
{
|
|
Headers = { { "X-Role", "admin" } },
|
|
Content = JsonContent.Create(new SaveOrgTemplateRequest(draft with { OrgName = "Hertitelde organisatie" })),
|
|
});
|
|
await _client.SendAsync(Req(HttpMethod.Post, $"/api/v1/admin/org-template/{Registers}/publish", role: "admin"));
|
|
|
|
// The sent brief's preview is unchanged — still the archived rendering.
|
|
var afterRepublish = await (await _client.GetAsync("/api/v1/brief/preview")).Content.ReadAsStringAsync();
|
|
Assert.Equal(sentHtml, afterRepublish);
|
|
Assert.DoesNotContain("Hertitelde organisatie", afterRepublish);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Proefbrief_is_admin_only()
|
|
{
|
|
ResetStores();
|
|
Assert.Equal(HttpStatusCode.Forbidden,
|
|
(await _client.GetAsync($"/api/v1/admin/org-template/{Registers}/preview")).StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Proefbrief_renders_the_draft_template_with_a_watermark()
|
|
{
|
|
ResetStores();
|
|
var res = await _client.SendAsync(Req(HttpMethod.Get, $"/api/v1/admin/org-template/{Registers}/preview", role: "admin"));
|
|
res.EnsureSuccessStatusCode();
|
|
var html = await res.Content.ReadAsStringAsync();
|
|
Assert.Contains("BIG-register", html);
|
|
Assert.Contains("<div class=\"preview-watermark\"", html);
|
|
}
|
|
}
|