import { describe, it, expect, vi, beforeEach } from 'vitest'; vi.mock('@sentry/sveltekit', () => ({ init: vi.fn(), handleErrorWithSentry: (fn: (args: unknown) => unknown) => fn, lastEventId: vi.fn(() => 'sentry-event-id-abc123') })); class RedirectMarker { constructor( public status: number, public location: string ) {} } vi.mock('@sveltejs/kit', () => ({ redirect: vi.fn((status: number, location: string) => new RedirectMarker(status, location)), isRedirect: (e: unknown) => e instanceof RedirectMarker })); vi.mock('@sveltejs/kit/hooks', () => ({ sequence: vi.fn((...fns: unknown[]) => fns[0]) })); vi.mock('$lib/paraglide/server', () => ({ paraglideMiddleware: vi.fn() })); vi.mock('$lib/paraglide/runtime', () => ({ cookieName: 'locale', cookieMaxAge: 86400 })); vi.mock('$lib/shared/server/locale', () => ({ detectLocale: vi.fn(() => 'de') })); vi.mock('process', () => ({ env: { API_INTERNAL_URL: 'http://backend:8080' } })); const makeEvent = () => ({ url: { pathname: '/documents/123' }, locals: {} }); describe('hooks.server handleError', () => { beforeEach(() => { vi.resetModules(); }); it('returns Sentry lastEventId as errorId', async () => { const Sentry = await import('@sentry/sveltekit'); vi.mocked(Sentry.lastEventId).mockReturnValue('sentry-event-id-abc123'); const { handleError } = await import('./hooks.server'); const result = (handleError as (args: unknown) => { message: string; errorId: string })({ error: new Error('boom'), event: makeEvent(), status: 500, message: 'Internal Error' }); expect(result.errorId).toBe('sentry-event-id-abc123'); expect(result.message).toBe('An unexpected error occurred'); }); it('falls back to crypto.randomUUID when lastEventId returns undefined', async () => { const Sentry = await import('@sentry/sveltekit'); vi.mocked(Sentry.lastEventId).mockReturnValue(undefined); const { handleError } = await import('./hooks.server'); const result = (handleError as (args: unknown) => { message: string; errorId: string })({ error: new Error('boom'), event: makeEvent(), status: 500, message: 'Internal Error' }); expect(result.errorId).toMatch( /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ ); expect(result.message).toBe('An unexpected error occurred'); }); }); interface UserGroupEvent { url: URL; cookies: { get: ReturnType; delete: ReturnType }; locals: { user?: unknown }; request: Request; } function makeUserGroupEvent(pathname: string, sessionId?: string): UserGroupEvent { return { url: new URL(`http://localhost${pathname}`), cookies: { get: vi.fn((name: string) => (name === 'fa_session' ? sessionId : undefined)), delete: vi.fn() }, locals: {}, request: new Request(`http://localhost${pathname}`) }; } describe('hooks.server userGroup (session lookup + 401 handling)', () => { beforeEach(() => { vi.resetModules(); vi.stubGlobal('fetch', vi.fn()); }); it('redirects to /login?reason=expired when backend rejects the session on a non-public path', async () => { vi.mocked(fetch).mockResolvedValue(new Response(null, { status: 401 })); const { handle } = await import('./hooks.server'); const event = makeUserGroupEvent('/documents/123', 'stale-session'); const resolve = vi.fn(); await expect((handle as (a: unknown) => unknown)({ event, resolve })).rejects.toMatchObject({ status: 302, location: '/login?reason=expired' }); expect(event.cookies.delete).toHaveBeenCalledWith('fa_session', { path: '/' }); expect(resolve).not.toHaveBeenCalled(); }); it('does not redirect when backend 401 fires on a public path (no /login → /login loop)', async () => { vi.mocked(fetch).mockResolvedValue(new Response(null, { status: 401 })); const { handle } = await import('./hooks.server'); const event = makeUserGroupEvent('/login', 'stale-session'); const resolve = vi.fn().mockResolvedValue(new Response()); await (handle as (a: unknown) => Promise)({ event, resolve }); expect(event.cookies.delete).toHaveBeenCalledWith('fa_session', { path: '/' }); expect(resolve).toHaveBeenCalledWith(event); }); it('passes through when no fa_session cookie is present', async () => { const { handle } = await import('./hooks.server'); const event = makeUserGroupEvent('/documents/123', undefined); const resolve = vi.fn().mockResolvedValue(new Response()); await (handle as (a: unknown) => Promise)({ event, resolve }); expect(fetch).not.toHaveBeenCalled(); expect(resolve).toHaveBeenCalledWith(event); }); it('attaches the user to locals when backend returns 200', async () => { vi.mocked(fetch).mockResolvedValue( new Response(JSON.stringify({ id: 'u1', email: 'a@b.de' }), { status: 200, headers: { 'Content-Type': 'application/json' } }) ); const { handle } = await import('./hooks.server'); const event = makeUserGroupEvent('/documents/123', 'valid-session'); const resolve = vi.fn().mockResolvedValue(new Response()); await (handle as (a: unknown) => Promise)({ event, resolve }); expect((event.locals as { user: { email: string } }).user.email).toBe('a@b.de'); }); });