import { describe, it, expect, vi, afterEach } from 'vitest'; import Fastify from 'fastify'; import type { FastifyInstance } from 'fastify'; import { registerSecretRoutes } from '../src/routes/secrets.js'; import { SecretService } from '../src/services/secret.service.js'; import { errorHandler } from '../src/middleware/error-handler.js'; import type { ISecretRepository } from '../src/repositories/interfaces.js'; let app: FastifyInstance; function mockRepo(): ISecretRepository { let lastCreated: Record | null = null; return { findAll: vi.fn(async () => [ { id: '1', name: 'ha-creds', data: { TOKEN: 'abc' }, version: 1, createdAt: new Date(), updatedAt: new Date() }, ]), findById: vi.fn(async (id: string) => { if (lastCreated && (lastCreated as { id: string }).id === id) return lastCreated as never; return null; }), findByName: vi.fn(async () => null), create: vi.fn(async (data) => { const secret = { id: 'new-id', name: data.name, data: data.data ?? {}, version: 1, createdAt: new Date(), updatedAt: new Date(), }; lastCreated = secret; return secret; }), update: vi.fn(async (id, data) => { const secret = { id, name: 'ha-creds', data: data.data, version: 2, createdAt: new Date(), updatedAt: new Date(), }; lastCreated = secret; return secret; }), delete: vi.fn(async () => {}), }; } afterEach(async () => { if (app) await app.close(); }); function createApp(repo: ISecretRepository) { app = Fastify({ logger: false }); app.setErrorHandler(errorHandler); const service = new SecretService(repo); registerSecretRoutes(app, service); return app.ready(); } describe('Secret Routes', () => { describe('GET /api/v1/secrets', () => { it('returns secret list', async () => { const repo = mockRepo(); await createApp(repo); const res = await app.inject({ method: 'GET', url: '/api/v1/secrets' }); expect(res.statusCode).toBe(200); const body = res.json>(); expect(body).toHaveLength(1); expect(body[0]?.name).toBe('ha-creds'); }); }); describe('GET /api/v1/secrets/:id', () => { it('returns 404 when not found', async () => { const repo = mockRepo(); await createApp(repo); const res = await app.inject({ method: 'GET', url: '/api/v1/secrets/missing' }); expect(res.statusCode).toBe(404); }); it('returns secret when found', async () => { const repo = mockRepo(); vi.mocked(repo.findById).mockResolvedValue({ id: '1', name: 'ha-creds', data: { TOKEN: 'abc' } } as never); await createApp(repo); const res = await app.inject({ method: 'GET', url: '/api/v1/secrets/1' }); expect(res.statusCode).toBe(200); }); }); describe('POST /api/v1/secrets', () => { it('creates a secret and returns 201', async () => { const repo = mockRepo(); await createApp(repo); const res = await app.inject({ method: 'POST', url: '/api/v1/secrets', payload: { name: 'new-secret', data: { KEY: 'val' } }, }); expect(res.statusCode).toBe(201); expect(res.json<{ name: string }>().name).toBe('new-secret'); }); it('returns 400 for invalid input', async () => { const repo = mockRepo(); await createApp(repo); const res = await app.inject({ method: 'POST', url: '/api/v1/secrets', payload: { name: '' }, }); expect(res.statusCode).toBe(400); }); it('returns 409 when name already exists', async () => { const repo = mockRepo(); vi.mocked(repo.findByName).mockResolvedValue({ id: '1' } as never); await createApp(repo); const res = await app.inject({ method: 'POST', url: '/api/v1/secrets', payload: { name: 'existing' }, }); expect(res.statusCode).toBe(409); }); }); describe('PUT /api/v1/secrets/:id', () => { it('updates a secret', async () => { const repo = mockRepo(); vi.mocked(repo.findById).mockResolvedValue({ id: '1', name: 'ha-creds' } as never); await createApp(repo); const res = await app.inject({ method: 'PUT', url: '/api/v1/secrets/1', payload: { data: { TOKEN: 'new-val' } }, }); expect(res.statusCode).toBe(200); }); it('returns 404 when not found', async () => { const repo = mockRepo(); await createApp(repo); const res = await app.inject({ method: 'PUT', url: '/api/v1/secrets/missing', payload: { data: { X: 'y' } }, }); expect(res.statusCode).toBe(404); }); }); describe('DELETE /api/v1/secrets/:id', () => { it('deletes a secret and returns 204', async () => { const repo = mockRepo(); vi.mocked(repo.findById).mockResolvedValue({ id: '1', name: 'ha-creds' } as never); await createApp(repo); const res = await app.inject({ method: 'DELETE', url: '/api/v1/secrets/1' }); expect(res.statusCode).toBe(204); }); it('returns 404 when not found', async () => { const repo = mockRepo(); await createApp(repo); const res = await app.inject({ method: 'DELETE', url: '/api/v1/secrets/missing' }); expect(res.statusCode).toBe(404); }); }); });