import { describe, it, expect, vi, beforeEach } from 'vitest'; // Mock the API client *before* importing the load function so the // module under test picks up the mock when it resolves its imports. vi.mock('$lib/api/auth', () => ({ getAuthConfig: vi.fn(), me: vi.fn() })); import { load } from './+layout'; import { getAuthConfig, me, type AuthConfig } from '$lib/api/auth'; type MinimalLoadEvent = { url: { pathname: string; search: string } }; function event(pathname: string, search = ''): MinimalLoadEvent { return { url: { pathname, search } }; } // `LayoutLoad`'s declared return type is `void | …`. Our `load` // always returns `{ authConfig }`, but TypeScript can't narrow on // that at the call site. Wrap to remove the `void` arm so the // assertions stay terse. async function callLoad(ev: MinimalLoadEvent): Promise<{ authConfig: AuthConfig }> { // eslint-disable-next-line @typescript-eslint/no-explicit-any const result = await load(ev as any); return result as { authConfig: AuthConfig }; } const PUBLIC_CFG = { self_register_enabled: true, private_mode: false }; const PRIVATE_CFG = { self_register_enabled: false, private_mode: true }; const aliceUser = { id: 'u1', username: 'alice', created_at: '2026-01-01T00:00:00Z', is_admin: false }; describe('root +layout load', () => { beforeEach(() => { vi.mocked(getAuthConfig).mockReset(); vi.mocked(me).mockReset(); }); it('public mode: returns authConfig data, never calls me()', async () => { vi.mocked(getAuthConfig).mockResolvedValue(PUBLIC_CFG); const data = await callLoad(event('/')); expect(data.authConfig).toEqual(PUBLIC_CFG); expect(me).not.toHaveBeenCalled(); }); it('private mode + anonymous on `/`: throws redirect(302) to /login with next=', async () => { vi.mocked(getAuthConfig).mockResolvedValue(PRIVATE_CFG); vi.mocked(me).mockResolvedValue(null); // eslint-disable-next-line @typescript-eslint/no-explicit-any await expect(load(event('/') as any)).rejects.toMatchObject({ status: 302, location: '/login?next=%2F' }); }); it('private mode + anonymous on `/login`: passes through without redirect', async () => { vi.mocked(getAuthConfig).mockResolvedValue(PRIVATE_CFG); const data = await callLoad(event('/login')); expect(data.authConfig.private_mode).toBe(true); // me() must not run on the login page itself, otherwise anonymous // visits make an extra round-trip every page load. expect(me).not.toHaveBeenCalled(); }); it('private mode + logged-in user: no redirect, returns authConfig', async () => { vi.mocked(getAuthConfig).mockResolvedValue(PRIVATE_CFG); vi.mocked(me).mockResolvedValue(aliceUser); const data = await callLoad(event('/')); expect(data.authConfig).toEqual(PRIVATE_CFG); }); it('private mode + anonymous: preserves pathname AND search in next=', async () => { vi.mocked(getAuthConfig).mockResolvedValue(PRIVATE_CFG); vi.mocked(me).mockResolvedValue(null); await expect( // eslint-disable-next-line @typescript-eslint/no-explicit-any load(event('/manga/abc', '?page=3') as any) ).rejects.toMatchObject({ status: 302, location: '/login?next=%2Fmanga%2Fabc%3Fpage%3D3' }); }); it('private mode + anonymous on /register: redirects to /login (register is never reachable in private mode)', async () => { vi.mocked(getAuthConfig).mockResolvedValue(PRIVATE_CFG); vi.mocked(me).mockResolvedValue(null); // eslint-disable-next-line @typescript-eslint/no-explicit-any await expect(load(event('/register') as any)).rejects.toMatchObject({ status: 302, location: '/login?next=%2Fregister' }); }); it('getAuthConfig failure: falls back to public-mode defaults, no redirect', async () => { // The backend middleware is the source of truth for the gate; // if the config probe blips, fail soft so a brief outage doesn't // lock everyone out of even the login page. No private data // can leak because the backend still 401s every request. vi.mocked(getAuthConfig).mockRejectedValue(new Error('network')); const data = await callLoad(event('/')); expect(data.authConfig).toEqual({ self_register_enabled: true, private_mode: false }); expect(me).not.toHaveBeenCalled(); }); });