2026-05-14 14:47:23 +09:00
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
mockFind,
|
|
|
|
|
mockFindOne,
|
|
|
|
|
mockFindOneAndUpdate,
|
|
|
|
|
mockFindOneAndDelete,
|
|
|
|
|
mockSave,
|
|
|
|
|
mockFindById,
|
|
|
|
|
} = vi.hoisted(() => ({
|
|
|
|
|
mockFind: vi.fn(),
|
|
|
|
|
mockFindOne: vi.fn(),
|
|
|
|
|
mockFindOneAndUpdate: vi.fn(),
|
|
|
|
|
mockFindOneAndDelete: vi.fn(),
|
|
|
|
|
mockSave: vi.fn(),
|
|
|
|
|
mockFindById: vi.fn(),
|
|
|
|
|
}));
|
|
|
|
|
|
2026-05-19 11:06:03 +09:00
|
|
|
vi.mock('../../../src/schemas/freshness-rule.schema.js', () => {
|
2026-05-14 14:47:23 +09:00
|
|
|
const chain = () => ({
|
|
|
|
|
sort: vi.fn().mockReturnThis(),
|
|
|
|
|
limit: vi.fn().mockReturnThis(),
|
|
|
|
|
lean: vi.fn().mockReturnThis(),
|
|
|
|
|
exec: mockFind,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const findOneChain = () => ({
|
|
|
|
|
lean: vi.fn().mockReturnThis(),
|
|
|
|
|
exec: mockFindOne,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const findByIdChain = () => ({
|
|
|
|
|
lean: vi.fn().mockReturnThis(),
|
|
|
|
|
exec: mockFindById,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const updateChain = () => ({
|
|
|
|
|
lean: vi.fn().mockReturnThis(),
|
|
|
|
|
exec: mockFindOneAndUpdate,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const deleteChain = () => ({
|
|
|
|
|
exec: mockFindOneAndDelete,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
class FakeModel {
|
|
|
|
|
data: unknown;
|
|
|
|
|
constructor(data: unknown) {
|
|
|
|
|
this.data = data;
|
|
|
|
|
}
|
|
|
|
|
save() {
|
|
|
|
|
mockSave(this.data);
|
|
|
|
|
return Promise.resolve({ toObject: () => this.data });
|
|
|
|
|
}
|
|
|
|
|
static find = vi.fn(() => chain());
|
|
|
|
|
static findOne = vi.fn(() => findOneChain());
|
|
|
|
|
static findById = vi.fn(() => findByIdChain());
|
|
|
|
|
static findOneAndUpdate = vi.fn(() => updateChain());
|
|
|
|
|
static findOneAndDelete = vi.fn(() => deleteChain());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { FreshnessRuleModel: FakeModel };
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-19 11:06:03 +09:00
|
|
|
import { FreshnessRulesRepository } from '../../../src/modules/freshness-rules/freshness-rules.repository.js';
|
2026-05-14 14:47:23 +09:00
|
|
|
|
|
|
|
|
describe(FreshnessRulesRepository.name, () => {
|
|
|
|
|
let repo: FreshnessRulesRepository;
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
repo = new FreshnessRulesRepository();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('findByHousehold', () => {
|
|
|
|
|
it('returns paginated results', async () => {
|
|
|
|
|
mockFind.mockResolvedValue([{ _id: { toString: () => 'id1' } }]);
|
|
|
|
|
const result = await repo.findByHousehold('hh1', { limit: 50 });
|
|
|
|
|
expect(result.data).toHaveLength(1);
|
|
|
|
|
expect(result.pagination.hasMore).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('applies category filter', async () => {
|
|
|
|
|
mockFind.mockResolvedValue([]);
|
|
|
|
|
await repo.findByHousehold('hh1', { category: 'dairy', limit: 50 });
|
|
|
|
|
expect(mockFind).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('applies storageLocation filter', async () => {
|
|
|
|
|
mockFind.mockResolvedValue([]);
|
|
|
|
|
await repo.findByHousehold('hh1', { storageLocation: 'fridge', limit: 50 });
|
|
|
|
|
expect(mockFind).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('applies cursor', async () => {
|
|
|
|
|
const cursor = Buffer.from('abc').toString('base64');
|
|
|
|
|
mockFind.mockResolvedValue([]);
|
|
|
|
|
await repo.findByHousehold('hh1', { cursor, limit: 50 });
|
|
|
|
|
expect(mockFind).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('findById', () => {
|
|
|
|
|
it('returns rule', async () => {
|
|
|
|
|
mockFindById.mockResolvedValue({ _id: 'id1' });
|
|
|
|
|
const result = await repo.findById('id1');
|
|
|
|
|
expect(result).toEqual({ _id: 'id1' });
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('findApplicableRule', () => {
|
|
|
|
|
it('returns household rule when available', async () => {
|
|
|
|
|
const rule = { _id: 'r1', householdId: 'hh1' };
|
|
|
|
|
mockFindOne.mockResolvedValueOnce(rule);
|
|
|
|
|
const result = await repo.findApplicableRule('hh1', 'dairy', 'fridge');
|
|
|
|
|
expect(result).toEqual(rule);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('falls back to system rule', async () => {
|
|
|
|
|
const systemRule = { _id: 'r2', householdId: null };
|
|
|
|
|
mockFindOne.mockResolvedValueOnce(null).mockResolvedValueOnce(systemRule);
|
|
|
|
|
const result = await repo.findApplicableRule('hh1', 'dairy', 'fridge');
|
|
|
|
|
expect(result).toEqual(systemRule);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns null when no rule found', async () => {
|
|
|
|
|
mockFindOne.mockResolvedValue(null);
|
|
|
|
|
const result = await repo.findApplicableRule('hh1', 'dairy', 'fridge');
|
|
|
|
|
expect(result).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('create', () => {
|
|
|
|
|
it('saves and returns rule', async () => {
|
|
|
|
|
const data = {
|
|
|
|
|
category: 'dairy',
|
|
|
|
|
storageLocation: 'fridge',
|
|
|
|
|
shelfLifeDays: 14,
|
|
|
|
|
openedLifeDays: 7,
|
|
|
|
|
};
|
|
|
|
|
const result = await repo.create(data);
|
|
|
|
|
expect(mockSave).toHaveBeenCalled();
|
|
|
|
|
expect(result).toEqual(data);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('update', () => {
|
|
|
|
|
it('updates and returns rule', async () => {
|
|
|
|
|
mockFindOneAndUpdate.mockResolvedValue({ _id: 'id1' });
|
|
|
|
|
const result = await repo.update('id1', 'hh1', { shelfLifeDays: 10 });
|
|
|
|
|
expect(result).toEqual({ _id: 'id1' });
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('delete', () => {
|
|
|
|
|
it('deletes rule', async () => {
|
|
|
|
|
mockFindOneAndDelete.mockResolvedValue({ _id: 'id1' });
|
|
|
|
|
await repo.delete('id1', 'hh1');
|
|
|
|
|
expect(mockFindOneAndDelete).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|