test(auth): userGroup hook redirect + cookie cleanup coverage
Four new tests against the composed handle (with sequence stubbed to return the head function): backend 401 on a private path redirects to /login?reason=expired; backend 401 on /login does NOT redirect (no loop); missing fa_session passes through without a backend call; 200 attaches the user to event.locals. Closes the hook-coverage gap flagged by Sara S1. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -6,11 +6,22 @@ vi.mock('@sentry/sveltekit', () => ({
|
||||
lastEventId: vi.fn(() => 'sentry-event-id-abc123')
|
||||
}));
|
||||
|
||||
vi.mock('@sveltejs/kit', () => ({ redirect: vi.fn() }));
|
||||
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' },
|
||||
@@ -56,3 +67,86 @@ describe('hooks.server handleError', () => {
|
||||
expect(result.message).toBe('An unexpected error occurred');
|
||||
});
|
||||
});
|
||||
|
||||
interface UserGroupEvent {
|
||||
url: URL;
|
||||
cookies: { get: ReturnType<typeof vi.fn>; delete: ReturnType<typeof vi.fn> };
|
||||
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<unknown>)({ 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<unknown>)({ 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<unknown>)({ event, resolve });
|
||||
|
||||
expect((event.locals as { user: { email: string } }).user.email).toBe('a@b.de');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user