// @vitest-environment jsdom // Session-expiry contract: the auth middleware is the only source of the // AUTH_REQUIRED code (no valid session) — api.ts dispatches 'auth:expired' on // it so AuthProvider clears the user and RequireAuth redirects. Any other 401 // (code AUTH_ERROR — wrong credential input, e.g. change-password with a bad // current password, on ANY path) must NOT dispatch. A path prefix cannot make // that distinction — /profile/change-password proved it in review. import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { api } from '@/api'; function mock401(code: string): typeof fetch { return vi.fn( async () => new Response(JSON.stringify({ error: code, message: 'Unauthorized', code }), { status: 401, headers: { 'Content-Type': 'application/json' }, }), ) as unknown as typeof fetch; } describe('session-expiry dispatch on 401', () => { let expiredEvents: number; const onExpired = () => { expiredEvents += 1; }; beforeEach(() => { expiredEvents = 0; window.addEventListener('auth:expired', onExpired); }); afterEach(() => { window.removeEventListener('auth:expired', onExpired); vi.unstubAllGlobals(); }); it('dispatches auth:expired for AUTH_REQUIRED (dead session) on a data endpoint', async () => { vi.stubGlobal('fetch', mock401('AUTH_REQUIRED')); await expect(api.bills()).rejects.toMatchObject({ status: 401 }); expect(expiredEvents).toBe(1); }); it('does NOT dispatch for AUTH_ERROR (wrong credential) — even outside /auth/*', async () => { vi.stubGlobal('fetch', mock401('AUTH_ERROR')); await expect( api.changeProfilePassword({ current_password: 'wrong', new_password: 'longenough1' }), ).rejects.toMatchObject({ status: 401 }); await expect( api.changePassword({ current_password: 'wrong', new_password: 'longenough1' }), ).rejects.toMatchObject({ status: 401 }); expect(expiredEvents).toBe(0); }); });