import { provideHttpClient } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { TestBed } from '@angular/core/testing'; import { AUTH_BFF_BASE_URL, AUTH_NAVIGATOR } from './auth.config'; import { AuthService } from './auth.service'; import type { CurrentUser } from './auth.types'; const BFF_BASE = 'http://bff.test/api'; const ME_URL = `${BFF_BASE}/auth/me`; const USER: CurrentUser = { oid: 'user-oid', tid: 'tenant-id', username: 'jane.doe@apf.example', displayName: 'Jane Doe', }; interface Fixture { service: AuthService; http: HttpTestingController; navigate: ReturnType; } function setup(): Fixture { const navigate = vi.fn(); TestBed.configureTestingModule({ providers: [ provideHttpClient(), provideHttpClientTesting(), { provide: AUTH_BFF_BASE_URL, useValue: BFF_BASE }, { provide: AUTH_NAVIGATOR, useValue: navigate }, AuthService, ], }); const service = TestBed.inject(AuthService); const http = TestBed.inject(HttpTestingController); return { service, http, navigate }; } describe('AuthService', () => { afterEach(() => { TestBed.resetTestingModule(); }); describe('bootstrap fetch', () => { it('starts in the loading state before /me resolves', () => { const { service, http } = setup(); expect(service.state().kind).toBe('loading'); expect(service.isLoading()).toBe(true); expect(service.currentUser()).toBeNull(); // Drain the in-flight bootstrap request to keep the controller clean. http.expectOne(ME_URL).flush({}, { status: 401, statusText: 'Unauthorized' }); }); it('transitions to authenticated when /me returns the user', async () => { const { service, http } = setup(); http.expectOne(ME_URL).flush(USER); // Let the awaited firstValueFrom commit the state. await Promise.resolve(); expect(service.state()).toEqual({ kind: 'authenticated', user: USER }); expect(service.currentUser()).toEqual(USER); expect(service.isLoading()).toBe(false); }); it('transitions to anonymous on 401', async () => { const { service, http } = setup(); http .expectOne(ME_URL) .flush({ error: 'unauthenticated' }, { status: 401, statusText: 'Unauthorized' }); await Promise.resolve(); expect(service.state().kind).toBe('anonymous'); expect(service.currentUser()).toBeNull(); }); it('transitions to error on a non-401 failure (network / 5xx / malformed)', async () => { const { service, http } = setup(); http.expectOne(ME_URL).flush('boom', { status: 500, statusText: 'Internal Server Error' }); await Promise.resolve(); expect(service.state().kind).toBe('error'); expect(service.currentUser()).toBeNull(); }); }); describe('refresh()', () => { it('can be called again after the bootstrap fetch and re-fetches /me', async () => { const { service, http } = setup(); http.expectOne(ME_URL).flush(USER); await Promise.resolve(); expect(service.state().kind).toBe('authenticated'); const promise = service.refresh(); http .expectOne(ME_URL) .flush({ error: 'unauthenticated' }, { status: 401, statusText: 'Unauthorized' }); await promise; expect(service.state().kind).toBe('anonymous'); }); }); describe('URL accessors', () => { it('derives me / login / logout URLs from the injected base', () => { const { service, http } = setup(); expect(service.meUrl).toBe(`${BFF_BASE}/auth/me`); expect(service.loginUrl).toBe(`${BFF_BASE}/auth/login`); expect(service.logoutUrl).toBe(`${BFF_BASE}/auth/logout`); http.expectOne(ME_URL).flush({}, { status: 401, statusText: 'Unauthorized' }); }); }); describe('login() / logout()', () => { it('navigates to /auth/login via AUTH_NAVIGATOR', () => { const { service, http, navigate } = setup(); service.login(); expect(navigate).toHaveBeenCalledWith(`${BFF_BASE}/auth/login`); http.expectOne(ME_URL).flush({}, { status: 401, statusText: 'Unauthorized' }); }); it('navigates to /auth/logout via AUTH_NAVIGATOR', () => { const { service, http, navigate } = setup(); service.logout(); expect(navigate).toHaveBeenCalledWith(`${BFF_BASE}/auth/logout`); http.expectOne(ME_URL).flush({}, { status: 401, statusText: 'Unauthorized' }); }); }); });