215 lines
6.3 KiB
TypeScript
215 lines
6.3 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { MedicineForm, StrengthUnit, MedicineCategory } from '@meshitrack/shared';
|
|
|
|
const {
|
|
mockExec,
|
|
_mockLean,
|
|
mockFind,
|
|
mockFindOne,
|
|
mockFindOneAndUpdate,
|
|
mockSave,
|
|
_mockSort,
|
|
_mockLimit,
|
|
} = vi.hoisted(() => {
|
|
const mockExec = vi.fn();
|
|
const mockLean = vi.fn(() => ({ exec: mockExec }));
|
|
const mockLimit = vi.fn(() => ({ lean: mockLean }));
|
|
const mockSort = vi.fn(() => ({ limit: mockLimit }));
|
|
return {
|
|
mockExec,
|
|
mockLean,
|
|
mockFind: vi.fn(() => ({ sort: mockSort })),
|
|
mockFindOne: vi.fn(() => ({ lean: mockLean })),
|
|
mockFindOneAndUpdate: vi.fn(() => ({ exec: mockExec })),
|
|
mockSave: vi.fn(),
|
|
mockSort,
|
|
mockLimit,
|
|
};
|
|
});
|
|
|
|
vi.mock('../../../src/schemas/medicine.schema.js', () => {
|
|
class MockMedicineModel {
|
|
_data: Record<string, unknown>;
|
|
constructor(data: Record<string, unknown>) {
|
|
this._data = data;
|
|
Object.assign(this, data);
|
|
}
|
|
save() {
|
|
mockSave();
|
|
return Promise.resolve(this);
|
|
}
|
|
toObject() {
|
|
return { _id: 'med-new', ...this._data };
|
|
}
|
|
static find = mockFind;
|
|
static findOne = mockFindOne;
|
|
static findOneAndUpdate = mockFindOneAndUpdate;
|
|
}
|
|
return { MedicineModel: MockMedicineModel };
|
|
});
|
|
|
|
import { MedicinesRepository } from '../../../src/modules/medicines/medicines.repository.js';
|
|
|
|
describe(MedicinesRepository.name, () => {
|
|
let repo: MedicinesRepository;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
repo = new MedicinesRepository();
|
|
});
|
|
|
|
describe('findByHousehold', () => {
|
|
it('returns paginated results', async () => {
|
|
const items = [
|
|
{ _id: 'med-1', name: 'Aspirin' },
|
|
{ _id: 'med-2', name: 'Ibuprofen' },
|
|
];
|
|
mockExec.mockResolvedValue(items);
|
|
|
|
const result = await repo.findByHousehold('hh1', { limit: 20 });
|
|
|
|
expect(mockFind).toHaveBeenCalledWith({ householdId: 'hh1', isDeleted: false });
|
|
expect(result.data).toHaveLength(2);
|
|
expect(result.pagination.hasMore).toBe(false);
|
|
});
|
|
|
|
it('applies partial name search when q is provided', async () => {
|
|
mockExec.mockResolvedValue([]);
|
|
|
|
await repo.findByHousehold('hh1', { q: 'asp', limit: 20 });
|
|
|
|
expect(mockFind).toHaveBeenCalledWith(
|
|
expect.objectContaining({ name: { $regex: 'asp', $options: 'i' } }),
|
|
);
|
|
});
|
|
|
|
it('applies category filter', async () => {
|
|
mockExec.mockResolvedValue([]);
|
|
|
|
await repo.findByHousehold('hh1', { category: MedicineCategory.PRESCRIPTION, limit: 20 });
|
|
|
|
expect(mockFind).toHaveBeenCalledWith(
|
|
expect.objectContaining({ category: MedicineCategory.PRESCRIPTION }),
|
|
);
|
|
});
|
|
|
|
it('applies form filter', async () => {
|
|
mockExec.mockResolvedValue([]);
|
|
|
|
await repo.findByHousehold('hh1', { form: MedicineForm.TABLET, limit: 20 });
|
|
|
|
expect(mockFind).toHaveBeenCalledWith(expect.objectContaining({ form: MedicineForm.TABLET }));
|
|
});
|
|
|
|
it('detects hasMore when extra item returned', async () => {
|
|
const items = Array.from({ length: 3 }, (_, i) => ({ _id: `med-${i}`, name: `Med ${i}` }));
|
|
mockExec.mockResolvedValue(items);
|
|
|
|
const result = await repo.findByHousehold('hh1', { limit: 2 });
|
|
|
|
expect(result.data).toHaveLength(2);
|
|
expect(result.pagination.hasMore).toBe(true);
|
|
expect(result.pagination.cursor).toBeTruthy();
|
|
});
|
|
|
|
it('decodes cursor for pagination', async () => {
|
|
mockExec.mockResolvedValue([]);
|
|
const cursor = Buffer.from('med-5').toString('base64');
|
|
|
|
await repo.findByHousehold('hh1', { cursor, limit: 20 });
|
|
|
|
expect(mockFind).toHaveBeenCalledWith(expect.objectContaining({ _id: { $gt: 'med-5' } }));
|
|
});
|
|
});
|
|
|
|
describe('findById', () => {
|
|
it('finds by id and householdId', async () => {
|
|
const medicine = { _id: 'med-1', name: 'Aspirin' };
|
|
mockExec.mockResolvedValue(medicine);
|
|
|
|
const result = await repo.findById('med-1', 'hh1');
|
|
|
|
expect(mockFindOne).toHaveBeenCalledWith({
|
|
_id: 'med-1',
|
|
householdId: 'hh1',
|
|
isDeleted: false,
|
|
});
|
|
expect(result).toEqual(medicine);
|
|
});
|
|
});
|
|
|
|
describe('findDuplicate', () => {
|
|
it('finds medicine with matching fields', async () => {
|
|
mockExec.mockResolvedValue({ _id: 'med-1' });
|
|
|
|
const result = await repo.findDuplicate('hh1', 'Aspirin', 500, 'mg', 'tablet');
|
|
|
|
expect(mockFindOne).toHaveBeenCalledWith({
|
|
householdId: 'hh1',
|
|
name: 'Aspirin',
|
|
strength: 500,
|
|
strengthUnit: 'mg',
|
|
form: 'tablet',
|
|
isDeleted: false,
|
|
});
|
|
expect(result).toBeTruthy();
|
|
});
|
|
|
|
it('excludes specified id', async () => {
|
|
mockExec.mockResolvedValue(null);
|
|
|
|
await repo.findDuplicate('hh1', 'Aspirin', 500, 'mg', 'tablet', 'med-1');
|
|
|
|
expect(mockFindOne).toHaveBeenCalledWith(expect.objectContaining({ _id: { $ne: 'med-1' } }));
|
|
});
|
|
});
|
|
|
|
describe('create', () => {
|
|
it('creates a medicine', async () => {
|
|
mockSave.mockResolvedValue({});
|
|
|
|
const data = {
|
|
name: 'Aspirin',
|
|
form: MedicineForm.TABLET,
|
|
strength: 500,
|
|
strengthUnit: StrengthUnit.MG,
|
|
category: MedicineCategory.OTC,
|
|
tags: [],
|
|
};
|
|
|
|
const result = await repo.create(data, 'hh1', 'kc-1');
|
|
|
|
expect(mockSave).toHaveBeenCalled();
|
|
expect(result).toMatchObject({ name: 'Aspirin', householdId: 'hh1', createdBy: 'kc-1' });
|
|
});
|
|
});
|
|
|
|
describe('update', () => {
|
|
it('updates a medicine', async () => {
|
|
mockExec.mockResolvedValue({ _id: 'med-1', name: 'Updated' });
|
|
|
|
const result = await repo.update('med-1', 'hh1', { name: 'Updated' });
|
|
|
|
expect(mockFindOneAndUpdate).toHaveBeenCalledWith(
|
|
{ _id: 'med-1', householdId: 'hh1', isDeleted: false },
|
|
{ $set: { name: 'Updated' } },
|
|
{ new: true, lean: true },
|
|
);
|
|
expect(result).toEqual({ _id: 'med-1', name: 'Updated' });
|
|
});
|
|
});
|
|
|
|
describe('softDelete', () => {
|
|
it('sets isDeleted to true', async () => {
|
|
mockExec.mockResolvedValue({ _id: 'med-1', isDeleted: true });
|
|
|
|
await repo.softDelete('med-1', 'hh1');
|
|
|
|
expect(mockFindOneAndUpdate).toHaveBeenCalledWith(
|
|
{ _id: 'med-1', householdId: 'hh1', isDeleted: false },
|
|
{ $set: { isDeleted: true } },
|
|
{ new: true, lean: true },
|
|
);
|
|
});
|
|
});
|
|
});
|