ADR-0002 SS3 models Zorgverlener/Medewerker as different Principal variants with different login flows. Actor #2 (apps/behandelportal) landed in WP-61/67 and the union never followed: grep -rn "Principal" returned one hit, a comment. Both apps' auth/domain/session.ts stayed byte-identical (`{ bsn, naam }`), so the backoffice's Behandelaar carried a BSN and logged into the backoffice as a citizen, by DigiD, under a fabricated citizen's name (login.page.ts). The divergence ADR-0002 predicted took an orthogonal side door instead (medewerker.interceptor.ts's X-Medewerker/X-Rollen stamp, which never touches SessionStore) -- which is why ssp/auth and bhp/auth still measured as 100%/84% duplicated after ADR-C-006 shared the route guards. RB-09 (landed the day before) made the backend's IIdentityProvider able to say "no identity" and fail closed; this ticket is its named FE half. Each app's auth/domain/session.ts becomes principal.ts, holding the one Principal variant that app actually has an actor for: ssp keeps `{ kind: 'zorgverlener', bsn, naam }` (G1 still strips the BSN before persisting); behandelportal gets `{ kind: 'medewerker', medewerkerId, naam, rollen }` (no BSN to strip -- G2 shape validation only). A new MedewerkerAdapter replaces DigidAdapter in behandelportal, resolving the existing MEDEWERKER_ID/currentRollen() dev stand-in into a Principal; because there is no credential to check, it returns Principal directly rather than a Result whose error variant could never occur. login.page.ts stops being a BSN/wachtwoord form -- one explainer line and an "Inloggen met SSO" button -- and its dead error-handling branch goes with the Result wrapper that justified it. Measured with tools/baseline-scan.mjs --dup: auth duplication drops from 168/168 (ssp) and 168/200 (bhp) to 32/179 and 32/259 -- under the backlog's <40 target. What remains is the ADR-C-006 route-guard re-export (deliberately identical), generic test/story-file boilerplate, and one shared fragment of the root-singleton-store idiom -- not re-converged identity or login-flow logic. SS3's prediction that the two actors would authenticate differently enough to justify not sharing auth has now actually been tested, not just asserted, and held. Also: renamed Session.bsn to Principal.bsn in two doc comments (libs/shared/src/infrastructure/subject.ts, subject.interceptor.ts) that cited the old type name; regenerated libs/shared/docs/behaviour-spec.mdx (generated file, per its own banner); recorded the resolution in ADR-0002 as a new amendment, replacing its "Known debt" section. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
85 lines
2.3 KiB
TypeScript
85 lines
2.3 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { isAuthenticated, parseRollen, parseStoredPrincipal, Principal } from './principal';
|
|
|
|
const principal: Principal = {
|
|
kind: 'medewerker',
|
|
medewerkerId: 'medewerker-1',
|
|
naam: 'Test',
|
|
rollen: ['behandelaar'],
|
|
};
|
|
|
|
describe('isAuthenticated', () => {
|
|
it('narrows a present principal to Principal', () => {
|
|
expect(isAuthenticated(principal)).toBe(true);
|
|
});
|
|
|
|
it('reports no principal as not authenticated', () => {
|
|
expect(isAuthenticated(null)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('parseStoredPrincipal', () => {
|
|
it('returns null when nothing is stored', () => {
|
|
expect(parseStoredPrincipal(null)).toBeNull();
|
|
});
|
|
|
|
it('returns null for a non-JSON string', () => {
|
|
expect(parseStoredPrincipal('not json')).toBeNull();
|
|
});
|
|
|
|
it('returns null when the stored shape is wrong (no naam)', () => {
|
|
expect(
|
|
parseStoredPrincipal(JSON.stringify({ kind: 'medewerker', medewerkerId: 'medewerker-1' })),
|
|
).toBeNull();
|
|
});
|
|
|
|
it('returns null when kind is not medewerker', () => {
|
|
expect(
|
|
parseStoredPrincipal(
|
|
JSON.stringify({
|
|
kind: 'zorgverlener',
|
|
medewerkerId: 'medewerker-1',
|
|
naam: 'Test',
|
|
rollen: [],
|
|
}),
|
|
),
|
|
).toBeNull();
|
|
});
|
|
|
|
it('returns null when rollen holds an unrecognized token', () => {
|
|
expect(
|
|
parseStoredPrincipal(
|
|
JSON.stringify({
|
|
kind: 'medewerker',
|
|
medewerkerId: 'medewerker-1',
|
|
naam: 'Test',
|
|
rollen: ['geen'],
|
|
}),
|
|
),
|
|
).toBeNull();
|
|
});
|
|
|
|
it('restores a well-shaped stored principal as-is (no BSN to strip)', () => {
|
|
const restored = parseStoredPrincipal(JSON.stringify(principal));
|
|
expect(restored).toEqual(principal);
|
|
});
|
|
});
|
|
|
|
describe('parseRollen', () => {
|
|
it('parses a single recognized rol', () => {
|
|
expect(parseRollen('behandelaar')).toEqual(['behandelaar']);
|
|
});
|
|
|
|
it('is case-insensitive and trims whitespace', () => {
|
|
expect(parseRollen(' Behandelaar , behandelaar ')).toEqual(['behandelaar', 'behandelaar']);
|
|
});
|
|
|
|
it('drops unrecognized tokens (the deny-path toggle, e.g. ?rollen=geen)', () => {
|
|
expect(parseRollen('geen')).toEqual([]);
|
|
});
|
|
|
|
it('returns an empty list for an empty string', () => {
|
|
expect(parseRollen('')).toEqual([]);
|
|
});
|
|
});
|