import { describe, it, expect, vi, beforeEach } from 'vitest'; import Fastify from 'fastify'; import { fastifyAwilixPlugin } from '@fastify/awilix'; import { asValue } from 'awilix'; // Mock jose for the auth plugin dependency vi.mock('jose', () => ({ createRemoteJWKSet: vi.fn(() => 'mock-jwks'), jwtVerify: vi.fn().mockResolvedValue({ payload: { sub: 'kc-1', email: 'test@example.com', preferred_username: 'testuser', realm_access: { roles: ['member'] }, }, protectedHeader: { alg: 'RS256' }, key: {}, }), })); const { mockFindByKeycloakId } = vi.hoisted(() => ({ mockFindByKeycloakId: vi.fn(), })); import authPlugin from '../../src/plugins/auth.plugin.js'; import householdPlugin from '../../src/plugins/household.plugin.js'; describe('household.plugin', () => { async function buildApp() { const app = Fastify({ logger: false }); await app.register(fastifyAwilixPlugin, { disposeOnClose: true, disposeOnResponse: true, strictBooleanEnforced: true, }); app.diContainer.register({ usersRepository: asValue({ findByKeycloakId: mockFindByKeycloakId, upsertFromToken: vi.fn().mockResolvedValue({ householdIds: ['hh1'] }), }), }); await app.register(authPlugin); await app.register(householdPlugin); return app; } beforeEach(() => { vi.clearAllMocks(); mockFindByKeycloakId.mockResolvedValue({ householdIds: ['hh1'] }); }); it('skips household check for public routes', async () => { const app = await buildApp(); app.get('/public', { config: { public: true } as never }, async () => ({ ok: true })); await app.ready(); const res = await app.inject({ method: 'GET', url: '/public' }); expect(res.statusCode).toBe(200); }); it('skips household check for routes with skipHousehold', async () => { const app = await buildApp(); app.get('/skip', { config: { skipHousehold: true } as never }, async () => ({ ok: true })); await app.ready(); const res = await app.inject({ method: 'GET', url: '/skip', headers: { authorization: 'Bearer valid' }, }); expect(res.statusCode).toBe(200); }); it('throws 403 when user does not belong to household', async () => { const app = await buildApp(); app.get('/households/:householdId/data', async () => ({ ok: true })); await app.ready(); const res = await app.inject({ method: 'GET', url: '/households/hh-unknown/data', headers: { authorization: 'Bearer valid' }, }); expect(res.statusCode).toBe(403); expect(res.json().message).toContain('do not belong'); }); it('sets request.householdId when user belongs to household', async () => { const app = await buildApp(); let capturedId: string | undefined; app.get('/households/:householdId/data', async (request) => { capturedId = request.householdId; return { ok: true }; }); await app.ready(); const res = await app.inject({ method: 'GET', url: '/households/hh1/data', headers: { authorization: 'Bearer valid' }, }); expect(res.statusCode).toBe(200); expect(capturedId).toBe('hh1'); }); it('skips household check when route has no householdId param', async () => { const app = await buildApp(); app.get('/no-household', async () => ({ ok: true })); await app.ready(); const res = await app.inject({ method: 'GET', url: '/no-household', headers: { authorization: 'Bearer valid' }, }); expect(res.statusCode).toBe(200); }); });