From 2779502f3bae7f88614df6f27a72d754e8731352 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 17 May 2026 22:48:16 +0200 Subject: [PATCH] test(auth): Vitest coverage for login action Six tests covering: load() exposes ?registered and ?reason; action returns 400 on missing email; 401 with INVALID_CREDENTIALS on backend reject; success re-emits fa_session and deletes legacy auth_token; 500 when backend omits fa_session in Set-Cookie. Closes the frontend coverage gap on the credential- handling logic that moved out of the Java side. Addresses PR #612 / Sara S1. Co-Authored-By: Claude Opus 4.7 --- frontend/src/routes/login/page.server.test.ts | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 frontend/src/routes/login/page.server.test.ts diff --git a/frontend/src/routes/login/page.server.test.ts b/frontend/src/routes/login/page.server.test.ts new file mode 100644 index 00000000..fa0878dd --- /dev/null +++ b/frontend/src/routes/login/page.server.test.ts @@ -0,0 +1,117 @@ +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 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(); + }); +});