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 { SESSION_PORT } from '@shared/application/session.port'; import { authGuard, capabilityGuard } from './auth.guard'; type Opts = { authed: boolean; can?: (c: string) => boolean; whenReady?: () => Promise; }; 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: SESSION_PORT, 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 = (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>(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>(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>(guard())).resolves.toEqual({ tree: ['/login'] }); expect(readySpy).not.toHaveBeenCalled(); }); // The guard reads SESSION_PORT, never a concrete SessionStore — that is what lets one // copy serve both apps while ADR-0002 §3 keeps their `auth` contexts separate. it('reads authentication through the port, not an app-local store', () => { setup({ authed: true }); expect(TestBed.inject(SESSION_PORT).isAuthenticated()).toBe(true); }); });