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>
63 lines
2.4 KiB
TypeScript
63 lines
2.4 KiB
TypeScript
import { TestBed } from '@angular/core/testing';
|
|
import { Router } from '@angular/router';
|
|
import { describe, it, expect, vi } from 'vitest';
|
|
import { AccessStore } from '@shared/application/access.store';
|
|
import { SessionStore } from './application/session.store';
|
|
import { authGuard, capabilityGuard } from './auth.guard';
|
|
|
|
type Opts = {
|
|
authed: boolean;
|
|
can?: (c: string) => boolean;
|
|
whenReady?: () => Promise<void>;
|
|
};
|
|
|
|
function setup({ authed, can = () => false, whenReady = () => Promise.resolve() }: Opts) {
|
|
const createUrlTree = vi.fn((cmds: string[]) => ({ tree: cmds }));
|
|
const readySpy = vi.fn(whenReady);
|
|
TestBed.configureTestingModule({
|
|
providers: [
|
|
{ provide: SessionStore, useValue: { isAuthenticated: () => authed } },
|
|
{ provide: AccessStore, useValue: { whenReady: readySpy, can } },
|
|
{ provide: Router, useValue: { createUrlTree } },
|
|
],
|
|
});
|
|
return { createUrlTree, readySpy };
|
|
}
|
|
|
|
// The guards ignore their (route, state) args; cast to call with none.
|
|
const call = <T>(fn: unknown) => TestBed.runInInjectionContext(() => (fn as () => T)());
|
|
|
|
describe('authGuard', () => {
|
|
it('allows an authenticated user', () => {
|
|
setup({ authed: true });
|
|
expect(call(authGuard)).toBe(true);
|
|
});
|
|
|
|
it('redirects an anonymous user to /login', () => {
|
|
const { createUrlTree } = setup({ authed: false });
|
|
expect(call(authGuard)).toEqual({ tree: ['/login'] });
|
|
expect(createUrlTree).toHaveBeenCalledWith(['/login']);
|
|
});
|
|
});
|
|
|
|
describe('capabilityGuard', () => {
|
|
const guard = () => capabilityGuard('stamdata:edit');
|
|
|
|
it('waits for /me, then allows an entitled admin', async () => {
|
|
const { readySpy } = setup({ authed: true, can: (c) => c === 'stamdata:edit' });
|
|
await expect(call<Promise<unknown>>(guard())).resolves.toBe(true);
|
|
expect(readySpy).toHaveBeenCalledOnce(); // it awaited caps before deciding
|
|
});
|
|
|
|
it('sends an authenticated-but-unentitled user to /dashboard (not a login loop)', async () => {
|
|
setup({ authed: true, can: () => false });
|
|
await expect(call<Promise<unknown>>(guard())).resolves.toEqual({ tree: ['/dashboard'] });
|
|
});
|
|
|
|
it('redirects an anonymous user to /login without waiting for caps', async () => {
|
|
const { readySpy } = setup({ authed: false, can: () => true });
|
|
await expect(call<Promise<unknown>>(guard())).resolves.toEqual({ tree: ['/login'] });
|
|
expect(readySpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|