import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import Fastify from 'fastify'; import { fastifyAwilixPlugin } from '@fastify/awilix'; import { serializerCompiler, validatorCompiler } from 'fastify-type-provider-zod'; import { MedicineForm, StrengthUnit, MedicineCategory } from '@meshitrack/shared'; 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: {}, }), })); const { mockFindByHousehold, mockFindById, mockFindDuplicate, mockCreate, mockUpdate, mockSoftDelete, mockCountByMedicineId, } = vi.hoisted(() => ({ mockFindByHousehold: vi.fn(), mockFindById: vi.fn(), mockFindDuplicate: vi.fn(), mockCreate: vi.fn(), mockUpdate: vi.fn(), mockSoftDelete: vi.fn(), mockCountByMedicineId: vi.fn(), })); vi.mock('../../../src/modules/medicines/medicines.repository.js', () => ({ MedicinesRepository: class { findByHousehold = mockFindByHousehold; findById = mockFindById; findDuplicate = mockFindDuplicate; create = mockCreate; update = mockUpdate; softDelete = mockSoftDelete; }, })); vi.mock('../../../src/modules/medicine-products/medicine-products.repository.js', () => ({ MedicineProductsRepository: class { countByMedicineId = mockCountByMedicineId; }, })); vi.mock('../../../src/modules/medicine-products/medicine-products.service.js', () => ({ MedicineProductsService: class { listByMedicine = vi.fn(); getById = vi.fn(); create = vi.fn(); update = vi.fn(); delete = vi.fn(); }, })); vi.mock('../../../src/modules/users/users.repository.js', () => ({ UsersRepository: class { findByKeycloakId = vi.fn().mockResolvedValue({ householdIds: ['hh1'] }); upsertFromToken = vi.fn().mockResolvedValue({ householdIds: ['hh1'] }); }, })); import authPlugin from '../../../src/plugins/auth.plugin.js'; import householdPlugin from '../../../src/plugins/household.plugin.js'; import usersRoutes from '../../../src/modules/users/users.routes.js'; import medicinesRoutes from '../../../src/modules/medicines/medicines.routes.js'; import medicineProductsRoutes from '../../../src/modules/medicine-products/medicine-products.routes.js'; function makeFakeMedicine(overrides = {}) { return { _id: 'med-1', householdId: 'hh1', name: 'Metformin', form: MedicineForm.TABLET, strength: 500, strengthUnit: StrengthUnit.MG, category: MedicineCategory.PRESCRIPTION, tags: [], createdBy: 'kc-1', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), ...overrides, }; } describe('medicines.routes', () => { let app: Awaited>; async function buildTestApp() { const instance = Fastify({ logger: false }); instance.setValidatorCompiler(validatorCompiler); instance.setSerializerCompiler(serializerCompiler); await instance.register(fastifyAwilixPlugin, { disposeOnClose: true, disposeOnResponse: true, strictBooleanEnforced: true, }); await instance.register(authPlugin); await instance.register(householdPlugin); await instance.register(usersRoutes); await instance.register(medicineProductsRoutes); await instance.register(medicinesRoutes); await instance.ready(); return instance; } const authHeaders = { authorization: 'Bearer valid-token' }; beforeEach(async () => { vi.clearAllMocks(); mockCountByMedicineId.mockResolvedValue(0); app = await buildTestApp(); }); afterEach(async () => { if (app) await app.close(); }); describe('GET /api/v1/households/:householdId/medicines', () => { it('returns paginated list', async () => { const medicine = makeFakeMedicine(); mockFindByHousehold.mockResolvedValue({ data: [medicine], pagination: { cursor: null, hasMore: false }, }); const res = await app.inject({ method: 'GET', url: '/api/v1/households/hh1/medicines', headers: authHeaders, }); expect(res.statusCode).toBe(200); const body = res.json(); expect(body.data).toHaveLength(1); expect(body.data[0].name).toBe('Metformin'); expect(body.pagination.hasMore).toBe(false); }); it('handles ObjectId and Date objects in response', async () => { const medicine = makeFakeMedicine({ _id: { toString: () => 'med-obj' }, createdAt: { toISOString: () => '2024-01-01T00:00:00.000Z' }, updatedAt: { toISOString: () => '2024-01-02T00:00:00.000Z' }, notes: 'Take with food', }); mockFindByHousehold.mockResolvedValue({ data: [medicine], pagination: { cursor: null, hasMore: false }, }); const res = await app.inject({ method: 'GET', url: '/api/v1/households/hh1/medicines', headers: authHeaders, }); expect(res.statusCode).toBe(200); const body = res.json(); expect(body.data[0]._id).toBe('med-obj'); expect(body.data[0].createdAt).toBe('2024-01-01T00:00:00.000Z'); expect(body.data[0].notes).toBe('Take with food'); }); }); describe('GET /api/v1/households/:householdId/medicines/:id', () => { it('returns a medicine', async () => { mockFindById.mockResolvedValue(makeFakeMedicine()); const res = await app.inject({ method: 'GET', url: '/api/v1/households/hh1/medicines/med-1', headers: authHeaders, }); expect(res.statusCode).toBe(200); expect(res.json().name).toBe('Metformin'); }); }); describe('POST /api/v1/households/:householdId/medicines', () => { it('creates a medicine', async () => { mockFindDuplicate.mockResolvedValue(null); mockCreate.mockResolvedValue(makeFakeMedicine()); const res = await app.inject({ method: 'POST', url: '/api/v1/households/hh1/medicines', headers: authHeaders, payload: { name: 'Metformin', form: MedicineForm.TABLET, strength: 500, strengthUnit: StrengthUnit.MG, category: MedicineCategory.PRESCRIPTION, }, }); expect(res.statusCode).toBe(201); expect(res.json().name).toBe('Metformin'); }); }); describe('PATCH /api/v1/households/:householdId/medicines/:id', () => { it('updates a medicine', async () => { mockFindById.mockResolvedValue(makeFakeMedicine()); mockFindDuplicate.mockResolvedValue(null); mockUpdate.mockResolvedValue(makeFakeMedicine({ name: 'Updated' })); const res = await app.inject({ method: 'PATCH', url: '/api/v1/households/hh1/medicines/med-1', headers: authHeaders, payload: { name: 'Updated' }, }); expect(res.statusCode).toBe(200); expect(res.json().name).toBe('Updated'); }); }); describe('DELETE /api/v1/households/:householdId/medicines/:id', () => { it('soft deletes a medicine', async () => { mockFindById.mockResolvedValue(makeFakeMedicine()); mockSoftDelete.mockResolvedValue(makeFakeMedicine({ isDeleted: true })); const res = await app.inject({ method: 'DELETE', url: '/api/v1/households/hh1/medicines/med-1', headers: authHeaders, }); expect(res.statusCode).toBe(204); }); }); });