Move the authoritative business rules off the frontend into a real backend,
realising the BFF-lite + decision-DTO design (ADR-0001) that until now lived
only in static mock JSON.
Backend (backend/):
- ASP.NET Core (.NET 10) minimal API, contract-first, Swagger UI at /swagger.
- DDD Domain/ rules layer: profession derivation + applicable policy questions
(DiplomaRules), herregistratie eligibility + reason (HerregistratieRule),
scholing threshold (IntakePolicy), submit rejections + reference generation
(SubmissionRules). In-memory seeded data, ProblemDetails (RFC 7807) errors.
- 27 xUnit tests: rule units + endpoint integration incl. BRP no-address and
DUO not-found fallbacks and 422 submit paths.
Frontend (only infrastructure/ + contracts/ change, as the architecture promised):
- NSwag-generated typed client (api-client.ts), routed through Angular HttpClient
via a small fetch adapter so the ?scenario= interceptor still applies.
- GET adapters use resource({ loader: client.x }); submit commands call the client
and map ProblemDetails -> err. The hardcoded uren==0 / manual-diploma rules are
deleted (now server-side). Domain, stores, UI and format validators unchanged.
- Deleted the now-dead public/mock/*.json.
Tooling/docs:
- npm start proxies /api -> backend; npm run gen:api regenerates the client;
docker compose up runs both (bind mounts use :z for SELinux/Fedora).
- backend/README.md walkthrough: adding a policy question is a one-file backend
change, no FE change, no client regen. Updated CLAUDE.md + ARCHITECTURE.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
101 lines
3.7 KiB
C#
101 lines
3.7 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using BigRegister.Api.Contracts;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
|
|
namespace BigRegister.Tests;
|
|
|
|
public class EndpointTests(WebApplicationFactory<Program> factory) : IClassFixture<WebApplicationFactory<Program>>
|
|
{
|
|
private readonly HttpClient _client = factory.CreateClient();
|
|
|
|
[Fact]
|
|
public async Task DashboardView_computes_eligibility_decision()
|
|
{
|
|
var dto = await _client.GetFromJsonAsync<DashboardViewDto>("/api/dashboard-view");
|
|
Assert.NotNull(dto);
|
|
Assert.Equal("19012345601", dto.Registration.BigNummer);
|
|
Assert.Equal("Geregistreerd", dto.Registration.Status.Tag);
|
|
// seed deadline 2027-03-01 is within 12 months of "today" (2026) → eligible
|
|
Assert.True(dto.Decisions.EligibleForHerregistratie);
|
|
Assert.NotNull(dto.Decisions.HerregistratieReason);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Notes_returns_seeded_aantekeningen()
|
|
{
|
|
var notes = await _client.GetFromJsonAsync<List<AantekeningDto>>("/api/notes");
|
|
Assert.Equal(3, notes!.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Brp_returns_address()
|
|
{
|
|
var dto = await _client.GetFromJsonAsync<BrpAddressDto>("/api/brp/address");
|
|
Assert.True(dto!.Gevonden);
|
|
Assert.Equal("2514 EA", dto.Adres!.Postcode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Duo_lookup_carries_server_decided_questions_and_professions()
|
|
{
|
|
var dto = await _client.GetFromJsonAsync<DuoLookupDto>("/api/duo/diplomas");
|
|
Assert.NotNull(dto);
|
|
|
|
var english = dto.Diplomas.Single(d => d.Id == "d2");
|
|
Assert.Equal("Arts", english.Beroep);
|
|
Assert.Contains(english.PolicyQuestions, q => q.Id == "nl-taalvaardigheid");
|
|
|
|
var dutch = dto.Diplomas.Single(d => d.Id == "d1");
|
|
Assert.Empty(dutch.PolicyQuestions);
|
|
|
|
// DUO "not found" fallback: an unlisted diploma → user uses the manual path,
|
|
// which the same lookup provides (maximal question set + declarable professions).
|
|
Assert.DoesNotContain(dto.Diplomas, d => d.Id == "unknown-id");
|
|
Assert.Equal(3, dto.Handmatig.PolicyQuestions.Count);
|
|
Assert.Equal(5, dto.Handmatig.Beroepen.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task IntakePolicy_returns_scholing_threshold()
|
|
{
|
|
var dto = await _client.GetFromJsonAsync<IntakePolicyDto>("/api/intake/policy");
|
|
Assert.Equal(1000, dto!.ScholingThreshold);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Registration_with_duo_diploma_succeeds()
|
|
{
|
|
var res = await _client.PostAsJsonAsync("/api/registrations", new RegistratieRequest("duo"));
|
|
res.EnsureSuccessStatusCode();
|
|
var body = await res.Content.ReadFromJsonAsync<ReferentieResponse>();
|
|
Assert.StartsWith("BIG-2026-", body!.Referentie);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Registration_with_manual_diploma_is_rejected_with_problem_details()
|
|
{
|
|
var res = await _client.PostAsJsonAsync("/api/registrations", new RegistratieRequest("handmatig"));
|
|
Assert.Equal(HttpStatusCode.UnprocessableEntity, res.StatusCode);
|
|
Assert.Contains("application/problem+json", res.Content.Headers.ContentType!.ToString());
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/api/intakes")]
|
|
[InlineData("/api/herregistraties")]
|
|
public async Task Zero_hours_submission_is_rejected(string route)
|
|
{
|
|
var res = await _client.PostAsJsonAsync(route, new { uren = 0 });
|
|
Assert.Equal(HttpStatusCode.UnprocessableEntity, res.StatusCode);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/api/intakes")]
|
|
[InlineData("/api/herregistraties")]
|
|
public async Task Worked_hours_submission_succeeds(string route)
|
|
{
|
|
var res = await _client.PostAsJsonAsync(route, new { uren = 40 });
|
|
res.EnsureSuccessStatusCode();
|
|
}
|
|
}
|