using System.Net;
using System.Net.Http.Json;
using BigRegister.Api.Contracts;
using BigRegister.Api.Data;
using Microsoft.AspNetCore.Mvc.Testing;
namespace BigRegister.Tests;
///
/// The brief is a single process-global demo entity, so each test resets it first
/// (tests within a class run sequentially in xUnit). Role is the dev-only X-Role
/// header: absent = drafter, "approver" = a different identity.
///
public class BriefEndpointTests(WebApplicationFactory factory) : IClassFixture>
{
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") }) }));
// Fill every REQUIRED section with a block so submit is allowed.
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 async Task Get()
{
BriefStore.Reset();
var view = await _client.GetFromJsonAsync("/api/v1/brief");
return view!.Brief;
}
private HttpRequestMessage Post(string path, string? role = null, object? body = null)
{
var req = new HttpRequestMessage(HttpMethod.Post, path);
if (role is not null) req.Headers.Add("X-Role", role);
if (body is not null) req.Content = JsonContent.Create(body);
return req;
}
[Fact]
public async Task Get_creates_a_draft_from_the_template_with_scoped_passages()
{
var brief = await Get();
Assert.Equal("draft", brief.Status.Tag);
Assert.Equal(new[] { "aanhef", "kern", "slot" }, brief.Sections.Select(s => s.SectionKey));
// aanhef + slot are locked, predefined and prefilled; only kern is editable + empty.
var aanhef = brief.Sections.Single(s => s.SectionKey == "aanhef");
Assert.True(aanhef.Locked);
Assert.NotEmpty(aanhef.Blocks);
Assert.True(brief.Sections.Single(s => s.SectionKey == "slot").Locked);
var kern = brief.Sections.Single(s => s.SectionKey == "kern");
Assert.False(kern.Locked);
Assert.Empty(kern.Blocks);
var view = await _client.GetFromJsonAsync("/api/v1/brief");
// global passages + the arts-scoped one; no other-beroep passages leak in.
Assert.Contains(view!.AvailablePassages, p => p.PassageId == "p-kern-arts");
Assert.All(view.AvailablePassages, p => Assert.True(p.Scope == "global" || p.Beroep == "arts"));
}
[Fact]
public async Task Save_is_drafter_only()
{
var brief = await Get();
var save = FilledFrom(brief);
var approver = Post("/api/v1/brief", role: "approver");
approver.Method = HttpMethod.Put;
approver.Content = JsonContent.Create(save);
Assert.Equal(HttpStatusCode.Forbidden, (await _client.SendAsync(approver)).StatusCode);
Assert.Equal(HttpStatusCode.OK, (await _client.PutAsJsonAsync("/api/v1/brief", save)).StatusCode);
}
[Fact]
public async Task Submit_blocks_on_empty_required_section_then_succeeds_when_filled()
{
await Get();
// Nothing filled yet → required sections empty → 409.
Assert.Equal(HttpStatusCode.Conflict, (await _client.SendAsync(Post("/api/v1/brief/submit"))).StatusCode);
var brief = (await _client.GetFromJsonAsync("/api/v1/brief"))!.Brief;
await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(brief));
var res = await _client.SendAsync(Post("/api/v1/brief/submit"));
res.EnsureSuccessStatusCode();
var submitted = await res.Content.ReadFromJsonAsync();
Assert.Equal("submitted", submitted!.Status.Tag);
}
[Fact]
public async Task Drafter_cannot_approve_own_letter_but_a_different_reviewer_can()
{
var brief = await Get();
await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(brief));
await _client.SendAsync(Post("/api/v1/brief/submit"));
// drafter role approving own letter → 403
Assert.Equal(HttpStatusCode.Forbidden, (await _client.SendAsync(Post("/api/v1/brief/approve"))).StatusCode);
var res = await _client.SendAsync(Post("/api/v1/brief/approve", role: "approver"));
res.EnsureSuccessStatusCode();
Assert.Equal("approved", (await res.Content.ReadFromJsonAsync())!.Status.Tag);
}
[Fact]
public async Task Reject_returns_comments_and_editing_reopens_to_draft()
{
var brief = await Get();
await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(brief));
await _client.SendAsync(Post("/api/v1/brief/submit"));
var rejected = await (await _client.SendAsync(
Post("/api/v1/brief/reject", role: "approver", body: new RejectBriefRequest("Graag aanvullen.")))).Content.ReadFromJsonAsync();
Assert.Equal("rejected", rejected!.Status.Tag);
Assert.Equal("Graag aanvullen.", rejected.Status.Comments);
// A drafter save on a rejected letter reopens it to draft.
var reopened = await (await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(brief))).Content.ReadFromJsonAsync();
Assert.Equal("draft", reopened!.Status.Tag);
}
[Fact]
public async Task Send_only_from_approved()
{
var brief = await Get();
await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(brief));
await _client.SendAsync(Post("/api/v1/brief/submit"));
// submitted (not approved) → send 409
Assert.Equal(HttpStatusCode.Conflict, (await _client.SendAsync(Post("/api/v1/brief/send"))).StatusCode);
await _client.SendAsync(Post("/api/v1/brief/approve", role: "approver"));
var res = await _client.SendAsync(Post("/api/v1/brief/send"));
res.EnsureSuccessStatusCode();
Assert.Equal("sent", (await res.Content.ReadFromJsonAsync())!.Status.Tag);
}
[Fact]
public async Task Reset_recreates_a_fresh_draft_with_locked_prefilled_sections()
{
var brief = await Get();
// Advance out of draft so the reset back to draft is observable.
await _client.PutAsJsonAsync("/api/v1/brief", FilledFrom(brief));
await _client.SendAsync(Post("/api/v1/brief/submit"));
var res = await _client.SendAsync(Post("/api/v1/brief/reset"));
res.EnsureSuccessStatusCode();
var view = await res.Content.ReadFromJsonAsync();
Assert.Equal("draft", view!.Brief.Status.Tag);
var aanhef = view.Brief.Sections.Single(s => s.SectionKey == "aanhef");
Assert.True(aanhef.Locked);
Assert.NotEmpty(aanhef.Blocks);
Assert.Empty(view.Brief.Sections.Single(s => s.SectionKey == "kern").Blocks);
}
}