Files
atomic-design-poc/apps/ssp/src/app/brief/infrastructure/brief.adapter.spec.ts
T
ehoandClaude Sonnet 5 e7156c5132 feat(WP-67): merge behandelportal into this repo as a monorepo
Restructures into apps/ssp + apps/behandelportal (two Angular projects)
plus libs/shared + libs/beheer (cross-app libraries), replacing WP-61's
separate sibling repo. That split had already produced real drift: a
hand-vendored copy of the backend's OpenAPI doc, a shared/ui+layout tree
forked and silently diverging (7 files), and beheer + the styles.scss
token bridge duplicated byte-for-byte across both repos.

- git mv the SSP's src/app/* into apps/ssp/; fold shared/, beheer/,
  environments/, the Storybook docs/*.mdx, and styles.scss into
  libs/shared + libs/beheer (all confirmed identical between the two
  repos before merging). auth stays deliberately duplicated per
  ADR-0002 (actor-specific, expected to diverge) - amended there.
- One generated API client (libs/shared), no more vendored swagger.json.
- .dependency-cruiser split into a base factory + one config per app,
  and Storybook into .storybook-ssp/.storybook-behandelportal - both
  forced by the @auth/* alias resolving to different directories per app.
- SiteHeaderComponent/ShellComponent gained HEADER_NAV_ITEMS/
  HEADER_ADMIN_LINKS/DEBUG_PANEL injection tokens so each app supplies
  its own nav/admin-links/dev-panel instead of one being hardcoded.
- CLAUDE.md, ARCHITECTURE.md, dependencies.md, and ADR-0002 updated;
  WP-67 backlog entry documents the full decision trail.

npm run ci green (lint, dep:check x2, 360 tests across ssp/
behandelportal/shared/beheer, both localized builds, backend tests,
snippet + api-client drift); both dev servers, both Storybook
instances, and docker compose verified working.

The old sibling repo (/home/eho/repos/behandelportal) is left
untouched, not deleted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 21:01:57 +02:00

257 lines
7.9 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { BriefViewDto } from '@shared/infrastructure/api-client';
import {
parseBrief,
parseBriefView,
parseNode,
parseOrgTemplate,
parseStatus,
} from './brief.adapter';
const view: BriefViewDto = {
brief: {
briefId: 'b1',
beroep: 'arts',
templateId: 't1',
drafterId: 'demo-drafter',
status: { tag: 'submitted', submittedBy: 'demo-drafter', submittedAt: '2026-07-01' },
placeholders: [
{ key: 'naam', label: 'Naam', autoResolvable: true },
{ key: 'code', label: 'Code', autoResolvable: true, fillable: false },
],
sections: [
{
sectionKey: 'aanhef',
title: 'Aanhef',
required: true,
blocks: [
{
type: 'passage',
blockId: 'local-1',
content: { paragraphs: [{ nodes: [{ type: 'placeholder', key: 'naam' }] }] },
sourcePassageId: 'p1',
sourceVersion: 2,
edited: true,
},
{
type: 'freeText',
blockId: 'local-2',
content: { paragraphs: [{ nodes: [{ type: 'text', text: 'hoi', marks: ['bold'] }] }] },
},
],
},
],
},
availablePassages: [
{
passageId: 'p1',
scope: 'global',
sectionKey: 'aanhef',
label: 'Aanhef',
content: { paragraphs: [{ nodes: [] }] },
version: 1,
},
{
passageId: 'p-neg-scholing',
scope: 'global',
sectionKey: 'kern',
label: 'Onvoldoende scholing',
content: { paragraphs: [{ nodes: [{ type: 'text', text: 'x' }] }] },
version: 1,
besluit: 'negatief',
reason: 'onvoldoende_scholing',
},
],
decisions: {
canEdit: false,
canApprove: true,
canReject: true,
canSend: false,
canRevealBigNummer: false,
},
orgTemplate: {
subOrgId: 'cibg-registers',
orgName: 'CIBG — Registers',
returnAddress: 'BIG-register\nPostbus 00000\n2500 AA Den Haag',
footerContact: 'www.bigregister.nl\ninfo@voorbeeld.example',
footerLegal: 'KvK 00000000',
signatureName: 'A. de Vries',
signatureRole: 'Hoofd Registratie',
signatureClosing: 'Met vriendelijke groet,',
margins: { topMm: 25, rightMm: 25, bottomMm: 25, leftMm: 25 },
version: 1,
},
caseContext: {
zorgverlenerNaam: 'Dr. A. (Anna) de Vries',
bigNummer: '19012345601',
beroep: 'arts',
aanvraagReferentie: 'HER-2026-000842',
},
};
describe('brief.adapter parse boundary', () => {
it('parses a well-formed view into the domain unions', () => {
const r = parseBriefView(view);
expect(r.ok).toBe(true);
if (!r.ok) return;
expect(r.value.brief.status).toEqual({
tag: 'submitted',
submittedBy: 'demo-drafter',
submittedAt: '2026-07-01',
});
const [passage, free] = r.value.brief.sections[0].blocks;
expect(passage.type === 'passage' && passage.edited).toBe(true);
expect(free.type).toBe('freeText');
expect(r.value.brief.placeholders[1]).toEqual({
key: 'code',
label: 'Code',
autoResolvable: true,
fillable: false,
});
expect(r.value.decisions).toEqual({
canEdit: false,
canApprove: true,
canReject: true,
canSend: false,
canRevealBigNummer: false,
});
// Guided-drafting tags survive the boundary; the untagged passage has neither.
expect(r.value.availablePassages[0].besluit).toBeUndefined();
expect(r.value.availablePassages[1]).toMatchObject({
besluit: 'negatief',
reason: 'onvoldoende_scholing',
});
expect(r.value.caseContext).toEqual({
zorgverlenerNaam: 'Dr. A. (Anna) de Vries',
bigNummer: '19012345601',
beroep: 'arts',
aanvraagReferentie: 'HER-2026-000842',
});
});
it('rejects a view whose case context is missing or malformed', () => {
expect(parseBriefView({ ...view, caseContext: undefined }).ok).toBe(false);
expect(
parseBriefView({
...view,
caseContext: { ...view.caseContext!, bigNummer: undefined as never },
}).ok,
).toBe(false);
});
it('parses the org template and drops a null logoDocumentId', () => {
const r = parseOrgTemplate(view.orgTemplate);
expect(r.ok).toBe(true);
if (!r.ok) return;
expect(r.value.orgName).toBe('CIBG — Registers');
expect(r.value.margins).toEqual({ topMm: 25, rightMm: 25, bottomMm: 25, leftMm: 25 });
expect('logoDocumentId' in r.value).toBe(false);
});
it('rejects a view whose org template is missing or malformed', () => {
expect(parseBriefView({ ...view, orgTemplate: undefined }).ok).toBe(false);
expect(parseOrgTemplate({ ...view.orgTemplate, signatureName: undefined }).ok).toBe(false);
expect(
parseOrgTemplate({
...view.orgTemplate,
margins: { topMm: 25, rightMm: 25, bottomMm: 25 },
}).ok,
).toBe(false);
});
it('rejects a view whose decisions are missing or malformed', () => {
expect(parseBriefView({ ...view, decisions: undefined as never }).ok).toBe(false);
expect(
parseBriefView({ ...view, decisions: { ...view.decisions, canSend: 'yes' as never } }).ok,
).toBe(false);
// The PII-reveal flag (PRD-0002 §5c) is required at the boundary too.
expect(
parseBriefView({
...view,
decisions: { ...view.decisions, canRevealBigNummer: undefined as never },
}).ok,
).toBe(false);
});
it('narrows node variants and rejects unknown ones', () => {
expect(parseNode({ type: 'text', text: 'x' })).toEqual({
ok: true,
value: { type: 'text', text: 'x' },
});
expect(parseNode({ type: 'placeholder', key: 'k' })).toEqual({
ok: true,
value: { type: 'placeholder', key: 'k' },
});
expect(parseNode({ type: 'lineBreak' })).toEqual({ ok: true, value: { type: 'lineBreak' } });
expect(parseNode({ type: 'text' }).ok).toBe(false); // missing text
expect(parseNode({ type: 'bogus' } as never).ok).toBe(false);
});
it('rejects a status DTO missing its required fields', () => {
expect(parseStatus({ tag: 'submitted' }).ok).toBe(false); // no submittedBy/At
expect(parseStatus({ tag: 'rejected', rejectedBy: 'x', rejectedAt: 't' }).ok).toBe(false); // no comments
expect(parseStatus({ tag: 'draft' }).ok).toBe(true);
});
it('reads section.locked (default false) and paragraph.list', () => {
const r = parseBrief({
...view.brief,
sections: [
{
sectionKey: 'aanhef',
title: 'Aanhef',
required: true,
locked: true,
blocks: [
{
type: 'freeText',
blockId: 'b1',
content: {
paragraphs: [
{ nodes: [{ type: 'text', text: 'een' }], list: 'bullet' },
{ nodes: [{ type: 'text', text: 'twee' }], list: 'number' },
{ nodes: [{ type: 'text', text: 'plat' }] },
],
},
},
],
},
{ sectionKey: 'kern', title: 'Kern', required: true, blocks: [] }, // no `locked` → false
],
});
expect(r.ok).toBe(true);
if (!r.ok) return;
const [aanhef, kern] = r.value.sections;
expect(aanhef.locked).toBe(true);
expect(kern.locked).toBe(false);
expect(aanhef.blocks[0].content.paragraphs.map((p) => p.list)).toEqual([
'bullet',
'number',
undefined,
]);
});
it('rejects a library passage with an unknown scope', () => {
const r = parseBriefView({
...view,
availablePassages: [{ ...view.availablePassages![0], scope: 'bogus' as never }],
});
expect(r.ok).toBe(false);
});
it('rejects a passage block missing provenance', () => {
const r = parseBrief({
...view.brief,
sections: [
{
sectionKey: 's',
title: 'S',
required: false,
blocks: [{ type: 'passage', blockId: 'b', content: { paragraphs: [] } }],
},
],
});
expect(r.ok).toBe(false);
});
});