All checks were successful
## What & why
Finishes **S-12 · Behandel-portal — werkbak + beoordeling**. The backend sub-slices (S-12a/b/c-1/c-2) were merged, but the slice's stated outcome — a behandel *portal* with medewerker login, a werkbak, and decide — had no frontend. This adds it.
- **`libs/auth`**: `MedewerkerAuthService` + `provideMedewerkerAuth` (Keycloak `medewerker` realm), a `roles`/`hasRole` surface on the shared `AuthService`, and a realm-roles protocol mapper so the SPA can read `behandelaar`/`teamlead` from the token. The BFF remains the security boundary (ADR-0013).
- **`apps/behandel`**: a new Nx Angular app mirroring self-service — medewerker OIDC login and a **werkbak** page listing registrations awaiting beoordeling (`GET /behandel/werkbak`) with per-row **Goedkeuren/Afwijzen** actions (`POST /behandel/registrations/{id}/decide`) that refresh the list. NL DS/Utrecht, standalone + signals.
- **e2e**: the walking-skeleton happy path now approves through the real portal (behandelaar logs in, finds the row by reference, clicks Goedkeuren) instead of the temporary admin endpoint.
- **infra/docs**: behandel service in compose (`:8142`, depends on Keycloak); added to the smoke `WAIT_SVCS` + CI log dump; `frontend-decisions.md` and `demo-script.md` updated.
Closes #13
## Definition of Done
- [x] Linked Gitea issue (above).
- [x] Failing test committed before the implementation.
- [x] Implementation makes the test pass; refactor commit if structure improved.
- [x] Conventional Commits referencing the issue (`refs #13`).
- [ ] CI green — all Gitea Actions jobs.
- [x] `docker compose up` from a fresh clone reaches green health checks within 3 minutes. *(behandel image + container verified locally; full stack gated in CI.)*
- [x] Docs updated if behaviour, contracts, or operations changed.
- [x] ADR added — ADR-0013 (merged with the backend sub-slices) already covers the wiring; no new decision here.
- [x] Demo note in `docs/demo-script.md`.
## Notes for reviewers
- Verified locally: auth + behandel + all frontend projects pass lint & unit tests (incl. axe WCAG 2.1 AA); production build green; the behandel Docker image builds and serves with the correct baked `medewerker` config + SPA fallback.
- The full compose-up smoke, e2e, and mutation are CI-gated (known local full-stack verify limits).
- **Follow-ups (not in scope):** the `WerkbakItem` contract has no citizen name (werkbak shows the BSN) — adding one is a BFF+domain contract change; and the domain's temporary admin `approve` endpoint is now unused by the e2e and could be removed.
Reviewed-on: #87
74 lines
3.0 KiB
TypeScript
74 lines
3.0 KiB
TypeScript
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
|
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
|
import { TestBed } from '@angular/core/testing';
|
|
import { BffApiV1Service } from 'api-client';
|
|
import { authInterceptor } from 'auth';
|
|
import { AbstractSecurityStorage, ConfigurationService } from 'angular-auth-oidc-client';
|
|
import { SECURE_API_ROUTES } from './app.config';
|
|
|
|
// Guards the medewerker token wiring end-to-end. The api-client calls the BFF with RELATIVE URLs, and
|
|
// the angular-auth-oidc-client interceptor attaches the token only when `req.url` starts with a
|
|
// configured secureRoute. A regression to an absolute origin makes the relative URL never match, so
|
|
// the behandel calls go out unauthenticated and the BFF answers 401. This drives the REAL interceptor
|
|
// and the REAL api-client against the REAL production route value (SECURE_API_ROUTES); only the config
|
|
// source and token storage are faked, so the assertion turns on the actual route-matching.
|
|
describe('behandel medewerker token wiring', () => {
|
|
let http: HttpTestingController;
|
|
let bff: BffApiV1Service;
|
|
const token = 'medewerker-access-token';
|
|
|
|
beforeEach(() => {
|
|
TestBed.configureTestingModule({
|
|
providers: [
|
|
provideHttpClient(withInterceptors([authInterceptor()])),
|
|
provideHttpClientTesting(),
|
|
{
|
|
provide: ConfigurationService,
|
|
useValue: {
|
|
hasAtLeastOneConfig: () => true,
|
|
getAllConfigurations: () => [{ configId: 'medewerker', secureRoutes: SECURE_API_ROUTES }],
|
|
},
|
|
},
|
|
{
|
|
// A signed-in session: the storage the interceptor's token lookup reads from.
|
|
provide: AbstractSecurityStorage,
|
|
useValue: {
|
|
read: () => JSON.stringify({ authzData: token, authnResult: { id_token: 'id-token' } }),
|
|
write: () => undefined,
|
|
remove: () => undefined,
|
|
clear: () => undefined,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
http = TestBed.inject(HttpTestingController);
|
|
bff = TestBed.inject(BffApiV1Service);
|
|
});
|
|
|
|
afterEach(() => http.verify());
|
|
|
|
it('attaches the bearer token to the relative werkbak call', () => {
|
|
bff.getBehandelWerkbak().subscribe();
|
|
|
|
const req = http.expectOne('/behandel/werkbak');
|
|
expect(req.request.headers.get('Authorization')).toBe(`Bearer ${token}`);
|
|
req.flush([]);
|
|
});
|
|
|
|
it('attaches the bearer token to the relative decide call', () => {
|
|
bff.postBehandelRegistrationsIdDecide('reg-1', { besluit: 'goedkeuren' }).subscribe();
|
|
|
|
const req = http.expectOne('/behandel/registrations/reg-1/decide');
|
|
expect(req.request.headers.get('Authorization')).toBe(`Bearer ${token}`);
|
|
req.flush(null);
|
|
});
|
|
|
|
it('leaves the anonymous openbaar register call unauthenticated', () => {
|
|
bff.getOpenbaarRegister().subscribe();
|
|
|
|
const req = http.expectOne((r) => r.url === '/openbaar/register');
|
|
expect(req.request.headers.has('Authorization')).toBe(false);
|
|
req.flush([]);
|
|
});
|
|
});
|