Setup initial project
This commit is contained in:
commit
db79af06f7
119 changed files with 20761 additions and 0 deletions
188
packages/api/src/main.test.ts
Normal file
188
packages/api/src/main.test.ts
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock mongoose to prevent real DB connections; provide class-based models
|
||||
vi.mock('mongoose', () => {
|
||||
class FakeSchema {
|
||||
paths: Record<string, unknown> = {};
|
||||
constructor(def: Record<string, unknown>, _opts?: unknown) {
|
||||
for (const key of Object.keys(def)) {
|
||||
this.paths[key] = { path: key };
|
||||
}
|
||||
this.paths['createdAt'] = { path: 'createdAt' };
|
||||
this.paths['updatedAt'] = { path: 'updatedAt' };
|
||||
}
|
||||
}
|
||||
|
||||
const models: Record<string, unknown> = {};
|
||||
|
||||
function createFakeModel(name: string) {
|
||||
const mockExec = vi.fn().mockResolvedValue(null);
|
||||
const mockLean = vi.fn(() => ({ exec: mockExec }));
|
||||
|
||||
class Model {
|
||||
_data: Record<string, unknown>;
|
||||
constructor(data: Record<string, unknown>) {
|
||||
this._data = data;
|
||||
Object.assign(this, data);
|
||||
}
|
||||
save() {
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
toObject() {
|
||||
return { _id: `${name}-id`, ...this._data };
|
||||
}
|
||||
static modelName = name;
|
||||
static schema = new FakeSchema({});
|
||||
static findOne = vi.fn(() => ({ lean: mockLean }));
|
||||
static findById = vi.fn(() => ({ lean: mockLean }));
|
||||
static findOneAndUpdate = vi.fn(() => ({ exec: mockExec }));
|
||||
static findByIdAndUpdate = vi.fn(() => ({ exec: mockExec }));
|
||||
}
|
||||
|
||||
return Model;
|
||||
}
|
||||
|
||||
return {
|
||||
default: {
|
||||
Schema: FakeSchema,
|
||||
model: vi.fn((name: string, _schema?: unknown) => {
|
||||
if (!models[name]) models[name] = createFakeModel(name);
|
||||
return models[name];
|
||||
}),
|
||||
connect: vi.fn().mockResolvedValue(undefined),
|
||||
disconnect: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('jose', () => ({
|
||||
createRemoteJWKSet: vi.fn(() => 'mock-jwks'),
|
||||
jwtVerify: vi.fn(),
|
||||
}));
|
||||
|
||||
import { buildApp } from './main.js';
|
||||
import * as jose from 'jose';
|
||||
import { NotFoundError } from './common/errors.js';
|
||||
|
||||
describe('buildApp', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('creates a Fastify app that is ready', async () => {
|
||||
const app = await buildApp({ logger: false });
|
||||
await app.ready();
|
||||
expect(app).toBeDefined();
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it('has the health route available', async () => {
|
||||
const app = await buildApp({ logger: false });
|
||||
await app.ready();
|
||||
|
||||
const res = await app.inject({ method: 'GET', url: '/api/v1/health' });
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.json().status).toBe('ok');
|
||||
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handler', () => {
|
||||
async function getApp() {
|
||||
// Set up a valid JWT mock for authenticated routes
|
||||
vi.mocked(jose.jwtVerify).mockResolvedValue({
|
||||
payload: {
|
||||
sub: 'kc-1',
|
||||
email: 'test@example.com',
|
||||
preferred_username: 'testuser',
|
||||
realm_access: { roles: ['member'] },
|
||||
householdIds: ['hh1'],
|
||||
},
|
||||
protectedHeader: { alg: 'RS256' },
|
||||
key: {} as never,
|
||||
} as never);
|
||||
|
||||
const app = await buildApp({ logger: false });
|
||||
|
||||
// Register test routes that throw various errors
|
||||
app.get('/test/app-error', { config: { public: true } as never }, async () => {
|
||||
throw new NotFoundError('Test not found');
|
||||
});
|
||||
|
||||
app.get('/test/generic-error', { config: { public: true } as never }, async () => {
|
||||
const err = new Error('Something broke');
|
||||
(err as unknown as Record<string, unknown>).statusCode = 422;
|
||||
throw err;
|
||||
});
|
||||
|
||||
app.get('/test/unknown-error', { config: { public: true } as never }, async () => {
|
||||
throw new Error('Unexpected');
|
||||
});
|
||||
|
||||
await app.ready();
|
||||
return app;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('handles AppError with correct status and body', async () => {
|
||||
const app = await getApp();
|
||||
|
||||
const res = await app.inject({ method: 'GET', url: '/test/app-error' });
|
||||
|
||||
expect(res.statusCode).toBe(404);
|
||||
const body = res.json();
|
||||
expect(body.error).toBe('Not Found');
|
||||
expect(body.message).toBe('Test not found');
|
||||
expect(body.timestamp).toBeDefined();
|
||||
expect(body.path).toBe('/test/app-error');
|
||||
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it('handles generic errors with statusCode', async () => {
|
||||
const app = await getApp();
|
||||
|
||||
const res = await app.inject({ method: 'GET', url: '/test/generic-error' });
|
||||
|
||||
expect(res.statusCode).toBe(422);
|
||||
const body = res.json();
|
||||
expect(body.error).toBe('Error');
|
||||
expect(body.message).toBe('Something broke');
|
||||
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it('handles unknown 500 errors without leaking messages', async () => {
|
||||
const app = await getApp();
|
||||
|
||||
const res = await app.inject({ method: 'GET', url: '/test/unknown-error' });
|
||||
|
||||
expect(res.statusCode).toBe(500);
|
||||
const body = res.json();
|
||||
expect(body.error).toBe('Internal Server Error');
|
||||
expect(body.message).toBe('An unexpected error occurred');
|
||||
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it('returns 404 for unknown routes', async () => {
|
||||
const app = await getApp();
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/nonexistent',
|
||||
headers: {
|
||||
authorization: 'Bearer test-token',
|
||||
'x-household-id': 'hh1',
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(404);
|
||||
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue