Files
mealprep/frontend/src/hooks.server.test.ts
Marcel Raddatz aeaca76534 fix(auth): handle users without household — fallback to 'Kein Haushalt'
Removes non-null assertions on householdId/householdName. Users who
haven't joined a household get a fallback name in the sidebar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 13:58:37 +02:00

147 lines
3.7 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
// Mock $env/dynamic/private before importing anything
vi.mock('$env/dynamic/private', () => ({
env: { BACKEND_URL: 'http://localhost:8080' }
}));
// Mock the apiClient
const mockGet = vi.fn();
vi.mock('$lib/server/api', () => ({
apiClient: () => ({ GET: mockGet })
}));
describe('auth guard (hooks.server.ts handle)', () => {
let handle: any;
beforeEach(async () => {
mockGet.mockReset();
const mod = await import('./hooks.server');
handle = mod.handle;
});
function createEvent(pathname: string, cookie?: string) {
const resolve = vi.fn().mockResolvedValue(new Response('ok'));
const event = {
url: new URL(`http://localhost${pathname}`),
cookies: {
get: vi.fn().mockImplementation((name: string) => {
if (name === 'JSESSIONID') return cookie;
return undefined;
})
},
locals: {} as any,
fetch: vi.fn()
};
return { event, resolve };
}
it('allows public routes without auth', async () => {
const { event, resolve } = createEvent('/login');
await handle({ event, resolve });
expect(resolve).toHaveBeenCalledWith(event);
});
it.each(['/login', '/login/', '/register', '/invite/abc123'])(
'allows public route %s without auth',
async (path) => {
const { event, resolve } = createEvent(path);
await handle({ event, resolve });
expect(resolve).toHaveBeenCalledWith(event);
}
);
it.each(['/_app/immutable/chunks/app.js', '/favicon.ico'])(
'allows static asset %s without auth',
async (path) => {
const { event, resolve } = createEvent(path);
await handle({ event, resolve });
expect(resolve).toHaveBeenCalledWith(event);
}
);
it('redirects unauthenticated requests to /login with redirect param', async () => {
const { event, resolve } = createEvent('/recipes/abc');
try {
await handle({ event, resolve });
expect.unreachable();
} catch (e: any) {
expect(e.status).toBe(302);
expect(e.location).toBe('/login?redirect=%2Frecipes%2Fabc');
}
});
it('populates event.locals.benutzer on valid session', async () => {
mockGet.mockResolvedValue({
data: {
data: {
id: '123',
displayName: 'Max',
householdId: 'h1',
householdName: 'Familie Müller',
householdRole: 'planer',
email: 'max@example.com',
systemRole: 'user'
}
},
error: undefined
});
const { event, resolve } = createEvent('/planner', 'valid-session');
await handle({ event, resolve });
expect(event.locals.benutzer).toEqual({
id: '123',
name: 'Max',
rolle: 'planer'
});
expect(event.locals.haushalt).toEqual({
id: 'h1',
name: 'Familie Müller'
});
expect(resolve).toHaveBeenCalledWith(event);
});
it('handles user without household gracefully', async () => {
mockGet.mockResolvedValue({
data: {
data: {
id: '456',
displayName: 'Neu',
householdId: null,
householdName: null,
householdRole: null,
email: 'neu@example.com',
systemRole: 'user'
}
},
error: undefined
});
const { event, resolve } = createEvent('/planner', 'valid-session');
await handle({ event, resolve });
expect(event.locals.benutzer).toEqual({
id: '456',
name: 'Neu',
rolle: 'mitglied'
});
expect(event.locals.haushalt).toEqual({
id: undefined,
name: 'Kein Haushalt'
});
expect(resolve).toHaveBeenCalledWith(event);
});
it('redirects to /login with redirect param when session validation fails', async () => {
mockGet.mockResolvedValue({ data: undefined, error: { status: 401 } });
const { event, resolve } = createEvent('/planner', 'bad-session');
try {
await handle({ event, resolve });
expect.unreachable();
} catch (e: any) {
expect(e.status).toBe(302);
expect(e.location).toBe('/login?redirect=%2Fplanner');
}
});
});