The status was derived in Contracts/Mappers.ToStatusDto, not the domain; Concept was a magic "Concept" string with no AanvraagStatusTag member; and the besluit endpoint re-derived its own guard by reading the status back out of the DTO and Enum.Parse-ing it. New Domain/Applications/AanvraagStatus.cs models the full status (Concept included, via a null Tag rather than a sixth enum member) as a closed type, constructible only through its factories. Aanvraag.StatusAt(now) carries the logic verbatim; Mappers.ToStatusDto and ZgwZaakMapper's two status producers become one-line projections onto the same wire DTO, so the wire shape is unchanged (gen:api shows zero diff beyond F1's). The one remaining Enum.Parse (the beoordeling GET, which crosses the IZaakSource wire boundary) is now non-throwing on an unrecognised tag. Also, WP-68 F2: the besluit transition-legality check now runs inside ApplicationStore.RecordBesluit's write lock instead of in the endpoint beforehand — two concurrent besluiten used to both pass the check before either wrote, letting the second silently overwrite a terminal decision. RecordBesluit returns an Ok/NotFound/Conflict outcome, mirroring DocumentStore.DeleteResult. Also, WP-68 F6: the "toelichting required" rule moves from an inline endpoint check into BeoordelingRules.RequiresToelichting, alongside CanDecide. The three tests naming this refactor's regression net (AanvraagStatusTag_covers_the_published_lifecycle, AutoApprovable_flips_to_goedgekeurd_after_the_window, ZgwZaakMapperTests) pass unmodified. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
89 lines
4.5 KiB
C#
89 lines
4.5 KiB
C#
using System.Net.Http.Json;
|
|
using BigRegister.Api.Contracts;
|
|
using BigRegister.Api.Data;
|
|
using BigRegister.Domain.Applications;
|
|
using BigRegister.Domain.Authorization;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
using Microsoft.AspNetCore.TestHost;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
namespace BigRegister.Tests;
|
|
|
|
/// <summary>Wraps <see cref="LocalZaakSource"/> but returns a DIFFERENT case id than the
|
|
/// underlying Aanvraag.Id — reproduces exactly what <c>OpenZaakZaakSource</c> does in
|
|
/// production (the FE-facing case id from <c>ListCases</c> is the ZGW zaak's own uuid, not
|
|
/// <c>ApplicationStore</c>'s primary key) without needing a live OpenZaak, so the besluit
|
|
/// endpoint's Referentie-based resolution (the fix below) gets coverage on every push.</summary>
|
|
file sealed class IdMismatchZaakSource : IZaakSource
|
|
{
|
|
private readonly LocalZaakSource inner = new();
|
|
private static ApplicationSummaryDto Rekey(ApplicationSummaryDto dto) => dto with { Id = $"zaak-{dto.Id}" };
|
|
|
|
public IReadOnlyList<ApplicationSummaryDto> ListCases(DateTimeOffset now) =>
|
|
inner.ListCases(now).Select(Rekey).ToList();
|
|
|
|
public IReadOnlyList<ApplicationSummaryDto> ListMyCases(ZorgverlenerCaller caller, DateTimeOffset now) =>
|
|
inner.ListMyCases(caller, now).Select(Rekey).ToList();
|
|
|
|
public (string Referentie, AanvraagStatusDto Status, string? ZaakUrl) CreateZaak(
|
|
Aanvraag aanvraag, DateTimeOffset now, CallerIdentity caller) => inner.CreateZaak(aanvraag, now, caller);
|
|
|
|
public void RecordBesluit(Aanvraag aanvraag, Besluit besluit, string? toelichting, DateTimeOffset now, CallerIdentity caller) =>
|
|
inner.RecordBesluit(aanvraag, besluit, toelichting, now, caller);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Regression for a real, live-repro'd bug: recording a besluit from the behandelportal always
|
|
/// 404'd against a real OpenZaak. Root cause — <c>POST /beoordeling/{id}/besluit</c> looked
|
|
/// <c>id</c> up directly in <c>ApplicationStore</c> (its own primary key), but <c>id</c> is
|
|
/// whatever <c>IZaakSource.ListCases</c> handed the FE; under <c>OpenZaakZaakSource</c> that's
|
|
/// the ZGW zaak's own uuid, a different value entirely. Fixed by resolving the case through
|
|
/// the same <c>ListCases</c> seak the GET sibling (<see cref="BeoordelingTests"/>) already uses,
|
|
/// then to the local <c>Aanvraag</c> via its Referentie (<c>ApplicationStore.GetByReferentie</c>)
|
|
/// — the one identifier stable across both sources. <see cref="IdMismatchZaakSource"/>
|
|
/// reproduces the id divergence without a live OpenZaak.
|
|
/// </summary>
|
|
public class BeoordelingIdMismatchTests
|
|
{
|
|
private static WebApplicationFactory<Program> Factory()
|
|
{
|
|
var dbPath = Path.Combine(Path.GetTempPath(), $"bigregister-id-mismatch-{Guid.NewGuid():N}.db");
|
|
return new WebApplicationFactory<Program>().WithWebHostBuilder(builder => builder
|
|
.UseSetting("ConnectionStrings:AppDb", $"Data Source={dbPath}")
|
|
.ConfigureTestServices(services => services.AddSingleton<IZaakSource, IdMismatchZaakSource>()));
|
|
}
|
|
|
|
private static HttpRequestMessage Behandelaar(HttpMethod method, string path, object? body = null)
|
|
{
|
|
var req = new HttpRequestMessage(method, path);
|
|
req.Headers.Add("X-Medewerker", "medewerker-1");
|
|
if (body is not null) req.Content = JsonContent.Create(body);
|
|
return req;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Besluit_resolves_by_referentie_when_the_case_id_differs_from_the_local_aanvraag_id()
|
|
{
|
|
using var factory = Factory();
|
|
using var client = factory.CreateClient();
|
|
|
|
var created = await client.PostAsJsonAsync("/api/v1/applications", new { type = "registratie" });
|
|
var app = (await created.Content.ReadFromJsonAsync<ApplicationDetailDto>())!;
|
|
var submit = await client.PostAsJsonAsync($"/api/v1/applications/{app.Id}/submit", new { diplomaHerkomst = "handmatig" });
|
|
submit.EnsureSuccessStatusCode();
|
|
|
|
var werkvoorraad = await client.SendAsync(Behandelaar(HttpMethod.Get, "/api/v1/werkvoorraad"));
|
|
var items = (await werkvoorraad.Content.ReadFromJsonAsync<List<ApplicationSummaryDto>>())!;
|
|
var caseId = Assert.Single(items).Id;
|
|
// Sanity: the id divergence this test exists for is real, not accidentally absent.
|
|
Assert.NotEqual(app.Id, caseId);
|
|
|
|
var res = await client.SendAsync(Behandelaar(HttpMethod.Post, $"/api/v1/beoordeling/{caseId}/besluit",
|
|
new { besluit = "Afwijzen", toelichting = "onvolledig" }));
|
|
|
|
res.EnsureSuccessStatusCode();
|
|
var body = (await res.Content.ReadFromJsonAsync<RecordBesluitResponse>())!;
|
|
Assert.Equal("Afgewezen", body.Status.Tag);
|
|
}
|
|
}
|