From d64139d9d14d4d4ee043ef01be89261027c4ecc5 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 17 May 2026 22:49:06 +0200 Subject: [PATCH] test(auth): Vitest coverage for logout action Three tests: happy path POSTs to backend with the session cookie and clears both fa_session and legacy auth_token; cookies are cleared even when the backend call rejects (best-effort logout); skips the backend call when no session cookie is present. Addresses PR #612 / Sara S1. Co-Authored-By: Claude Opus 4.7 --- .../src/routes/logout/page.server.test.ts | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 frontend/src/routes/logout/page.server.test.ts diff --git a/frontend/src/routes/logout/page.server.test.ts b/frontend/src/routes/logout/page.server.test.ts new file mode 100644 index 00000000..2366bbd7 --- /dev/null +++ b/frontend/src/routes/logout/page.server.test.ts @@ -0,0 +1,63 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('$env/dynamic/private', () => ({ + env: { API_INTERNAL_URL: 'http://backend:8080' } +})); + +import { actions } from './+page.server'; + +type ActionsRecord = Record unknown>; + +function makeCookies(sessionId?: string) { + return { + get: vi.fn((name: string) => (name === 'fa_session' ? sessionId : undefined)), + set: vi.fn(), + delete: vi.fn() + }; +} + +describe('logout action', () => { + beforeEach(() => vi.restoreAllMocks()); + + it('calls backend /api/auth/logout with the session cookie and redirects to /login', async () => { + const mockFetch = vi.fn().mockResolvedValue(new Response(null, { status: 204 })); + const cookies = makeCookies('opaque-id'); + + await expect( + (actions as ActionsRecord).default({ cookies, fetch: mockFetch } as never) + ).rejects.toMatchObject({ status: 303, location: '/login' }); + + expect(mockFetch).toHaveBeenCalledWith( + 'http://backend:8080/api/auth/logout', + expect.objectContaining({ + method: 'POST', + headers: { Cookie: 'fa_session=opaque-id' } + }) + ); + expect(cookies.delete).toHaveBeenCalledWith('fa_session', { path: '/' }); + expect(cookies.delete).toHaveBeenCalledWith('auth_token', { path: '/' }); + }); + + it('clears cookies even when the backend logout call fails', async () => { + const mockFetch = vi.fn().mockRejectedValue(new Error('connection refused')); + const cookies = makeCookies('opaque-id'); + + await expect( + (actions as ActionsRecord).default({ cookies, fetch: mockFetch } as never) + ).rejects.toMatchObject({ status: 303, location: '/login' }); + + expect(cookies.delete).toHaveBeenCalledWith('fa_session', { path: '/' }); + }); + + it('skips the backend call when no session cookie is present', async () => { + const mockFetch = vi.fn(); + const cookies = makeCookies(undefined); + + await expect( + (actions as ActionsRecord).default({ cookies, fetch: mockFetch } as never) + ).rejects.toMatchObject({ status: 303, location: '/login' }); + + expect(mockFetch).not.toHaveBeenCalled(); + expect(cookies.delete).toHaveBeenCalledWith('fa_session', { path: '/' }); + }); +});