102 lines
2.9 KiB
TypeScript
102 lines
2.9 KiB
TypeScript
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
|
|
import Fastify from 'fastify';
|
||
|
|
|
||
|
|
// 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'] },
|
||
|
|
householdIds: ['hh1'],
|
||
|
|
},
|
||
|
|
protectedHeader: { alg: 'RS256' },
|
||
|
|
key: {},
|
||
|
|
}),
|
||
|
|
}));
|
||
|
|
|
||
|
|
import authPlugin from './auth.plugin.js';
|
||
|
|
import householdPlugin from './household.plugin.js';
|
||
|
|
|
||
|
|
describe('household.plugin', () => {
|
||
|
|
async function buildApp() {
|
||
|
|
const app = Fastify({ logger: false });
|
||
|
|
await app.register(authPlugin);
|
||
|
|
await app.register(householdPlugin);
|
||
|
|
return app;
|
||
|
|
}
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
vi.clearAllMocks();
|
||
|
|
});
|
||
|
|
|
||
|
|
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);
|
||
|
|
});
|
||
|
|
});
|