import { beforeEach, describe, expect, it, vi } from 'vitest'; vi.mock('$env/dynamic/private', () => ({ env: { API_INTERNAL_URL: 'http://backend:8080' } })); import { actions, load } from './+page.server'; type ActionsRecord = Record unknown>; function makeRequest(form: Record): Request { const fd = new FormData(); for (const [k, v] of Object.entries(form)) fd.set(k, v); return new Request('http://localhost/login?/login', { method: 'POST', body: fd }); } function makeCookies() { return { set: vi.fn(), delete: vi.fn(), get: vi.fn() }; } function loadEvent(search: string) { return { url: new URL(`http://localhost/login${search}`), request: new Request('http://localhost/login', { method: 'GET' }), route: { id: '/login' } } as never; } describe('login load', () => { it('exposes registered=true when ?registered=1 is present', async () => { const result = await load(loadEvent('?registered=1')); expect(result).toEqual({ registered: true, reason: null }); }); it('exposes reason=expired when ?reason=expired is present', async () => { const result = await load(loadEvent('?reason=expired')); expect(result).toEqual({ registered: false, reason: 'expired' }); }); }); describe('login action', () => { beforeEach(() => vi.restoreAllMocks()); it('returns 400 when email is missing', async () => { const result = await (actions as ActionsRecord).login({ request: makeRequest({ password: 'pw' }), cookies: makeCookies(), fetch: vi.fn(), url: new URL('http://localhost/login') } as never); expect((result as { status: number }).status).toBe(400); }); it('returns 401 with INVALID_CREDENTIALS when the backend rejects credentials', async () => { const mockFetch = vi.fn().mockResolvedValue( new Response(JSON.stringify({ code: 'INVALID_CREDENTIALS' }), { status: 401, headers: { 'Content-Type': 'application/json' } }) ); const result = await (actions as ActionsRecord).login({ request: makeRequest({ email: 'a@b.de', password: 'wrong' }), cookies: makeCookies(), fetch: mockFetch, url: new URL('http://localhost/login') } as never); expect((result as { status: number }).status).toBe(401); }); it('re-emits fa_session and deletes legacy auth_token on success', async () => { const mockFetch = vi.fn().mockResolvedValue( new Response('{}', { status: 200, headers: { 'Set-Cookie': 'fa_session=opaque-id; Path=/; HttpOnly; SameSite=Strict' } }) ); const cookies = makeCookies(); // redirect() throws a Redirect instance — assert via rejects. const redirected = (actions as ActionsRecord).login({ request: makeRequest({ email: 'a@b.de', password: 'pw' }), cookies, fetch: mockFetch, url: new URL('http://localhost/login') } as never); await expect(redirected).rejects.toMatchObject({ status: 303, location: '/' }); expect(cookies.set).toHaveBeenCalledWith( 'fa_session', 'opaque-id', expect.objectContaining({ httpOnly: true, sameSite: 'strict', maxAge: 60 * 60 * 8 }) ); expect(cookies.delete).toHaveBeenCalledWith('auth_token', { path: '/' }); }); it('returns 429 with rateLimited=true when the backend rate-limits the request', async () => { const mockFetch = vi.fn().mockResolvedValue( new Response(JSON.stringify({ code: 'TOO_MANY_LOGIN_ATTEMPTS' }), { status: 429, headers: { 'Content-Type': 'application/json' } }) ); const result = await (actions as ActionsRecord).login({ request: makeRequest({ email: 'a@b.de', password: 'pw' }), cookies: makeCookies(), fetch: mockFetch, url: new URL('http://localhost/login') } as never); expect((result as { status: number }).status).toBe(429); expect((result as { data: { rateLimited: boolean } }).data.rateLimited).toBe(true); }); it('returns 500 when backend response omits fa_session cookie', async () => { const mockFetch = vi.fn().mockResolvedValue(new Response('{}', { status: 200 })); const cookies = makeCookies(); const result = await (actions as ActionsRecord).login({ request: makeRequest({ email: 'a@b.de', password: 'pw' }), cookies, fetch: mockFetch, url: new URL('http://localhost/login') } as never); expect((result as { status: number }).status).toBe(500); expect(cookies.set).not.toHaveBeenCalled(); }); });