test(tests-test): ✅ Add test utilities for wellness backend mocking and validation
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
fc29de9c5a
commit
ec429da540
1 changed files with 621 additions and 0 deletions
621
wellness/self-care/backend/__tests__/self-care.service.spec.ts
Normal file
621
wellness/self-care/backend/__tests__/self-care.service.spec.ts
Normal file
|
|
@ -0,0 +1,621 @@
|
|||
import { NotFoundException } from '@nestjs/common';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { SelfCareService } from '../self-care.service';
|
||||
import { SelfCareBaselineService } from '../self-care-baseline.service';
|
||||
import { SelfCareCategory } from '../entities/self-care-category.entity';
|
||||
import { SelfCareLog } from '../entities/self-care-log.entity';
|
||||
import { SelfCareBaseline, BaselineSnapshotEntry } from '../entities/self-care-baseline.entity';
|
||||
import { mockRepository, MockRepository } from '@test-helpers/mock-repository';
|
||||
import { SelfCareCondition } from '@life-platform/shared';
|
||||
|
||||
jest.mock('@common/resolve-short-id', () => ({
|
||||
resolveShortId: jest.fn(async (_repo: unknown, id: string) => id),
|
||||
}));
|
||||
|
||||
const UUID_1 = 'a1b2c3d4-1111-2222-3333-444455556666';
|
||||
const UUID_2 = 'b2c3d4e5-2222-3333-4444-555566667777';
|
||||
const UUID_3 = 'c3d4e5f6-3333-4444-5555-666677778888';
|
||||
const UUID_CAT1 = 'd4e5f6a7-4444-5555-6666-777788889999';
|
||||
const UUID_CAT2 = 'e5f6a7b8-5555-6666-7777-888899990000';
|
||||
const UUID_LOG1 = 'f6a7b8c9-6666-7777-8888-999900001111';
|
||||
|
||||
function makeCategory(overrides: Partial<SelfCareCategory> = {}): SelfCareCategory {
|
||||
return {
|
||||
id: UUID_CAT1,
|
||||
name: 'Haircut',
|
||||
slug: 'haircut',
|
||||
description: null,
|
||||
cadenceDays: 30,
|
||||
cadenceWarningDays: 24,
|
||||
isActive: true,
|
||||
sortOrder: 0,
|
||||
icon: null,
|
||||
tags: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
...overrides,
|
||||
} as SelfCareCategory;
|
||||
}
|
||||
|
||||
function makeLog(overrides: Partial<SelfCareLog> = {}): SelfCareLog {
|
||||
return {
|
||||
id: UUID_LOG1,
|
||||
categoryId: UUID_CAT1,
|
||||
loggedAt: new Date(),
|
||||
condition: SelfCareCondition.Good,
|
||||
notes: null,
|
||||
photoUrl: null,
|
||||
isBackdated: false,
|
||||
metadata: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
...overrides,
|
||||
} as SelfCareLog;
|
||||
}
|
||||
|
||||
function makeBaseline(overrides: Partial<SelfCareBaseline> = {}): SelfCareBaseline {
|
||||
return {
|
||||
id: UUID_1,
|
||||
name: 'Baseline #1',
|
||||
takenAt: new Date(),
|
||||
notes: null,
|
||||
snapshot: [],
|
||||
tags: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
...overrides,
|
||||
} as SelfCareBaseline;
|
||||
}
|
||||
|
||||
describe('SelfCareService', () => {
|
||||
let service: SelfCareService;
|
||||
let categoryRepo: MockRepository;
|
||||
let logRepo: MockRepository;
|
||||
let eventEmitter: { emit: jest.Mock };
|
||||
|
||||
beforeEach(async () => {
|
||||
categoryRepo = mockRepository();
|
||||
logRepo = mockRepository();
|
||||
eventEmitter = { emit: jest.fn() };
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
SelfCareService,
|
||||
{ provide: getRepositoryToken(SelfCareCategory), useValue: categoryRepo },
|
||||
{ provide: getRepositoryToken(SelfCareLog), useValue: logRepo },
|
||||
{ provide: EventEmitter2, useValue: eventEmitter },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get(SelfCareService);
|
||||
});
|
||||
|
||||
describe('createCategory', () => {
|
||||
it('should create a category with auto-generated slug and warning days', async () => {
|
||||
const dto = { name: 'Hair Color', cadenceDays: 60 };
|
||||
const created = makeCategory({ name: 'Hair Color', slug: 'hair-color', cadenceDays: 60, cadenceWarningDays: 48 });
|
||||
categoryRepo.create.mockReturnValue(created);
|
||||
categoryRepo.save.mockResolvedValue(created);
|
||||
|
||||
const result = await service.createCategory(dto as never);
|
||||
|
||||
expect(categoryRepo.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
name: 'Hair Color',
|
||||
slug: 'hair-color',
|
||||
cadenceDays: 60,
|
||||
cadenceWarningDays: 48,
|
||||
isActive: true,
|
||||
}));
|
||||
expect(result).toEqual(created);
|
||||
});
|
||||
|
||||
it('should use provided slug when given', async () => {
|
||||
const dto = { name: 'Nails', slug: 'custom-slug', cadenceDays: 14 };
|
||||
categoryRepo.create.mockImplementation((d) => d);
|
||||
categoryRepo.save.mockImplementation((e) => Promise.resolve(e));
|
||||
|
||||
await service.createCategory(dto as never);
|
||||
|
||||
expect(categoryRepo.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
slug: 'custom-slug',
|
||||
}));
|
||||
});
|
||||
|
||||
it('should use provided cadenceWarningDays', async () => {
|
||||
const dto = { name: 'Skin', cadenceDays: 7, cadenceWarningDays: 5 };
|
||||
categoryRepo.create.mockImplementation((d) => d);
|
||||
categoryRepo.save.mockImplementation((e) => Promise.resolve(e));
|
||||
|
||||
await service.createCategory(dto as never);
|
||||
|
||||
expect(categoryRepo.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
cadenceWarningDays: 5,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('logCategory', () => {
|
||||
it('should log a category entry', async () => {
|
||||
categoryRepo.findOne.mockResolvedValue(makeCategory());
|
||||
const savedLog = makeLog({ condition: SelfCareCondition.Excellent });
|
||||
logRepo.create.mockReturnValue(savedLog);
|
||||
logRepo.save.mockResolvedValue(savedLog);
|
||||
|
||||
const result = await service.logCategory(UUID_CAT1, { condition: SelfCareCondition.Excellent } as never);
|
||||
|
||||
expect(logRepo.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
categoryId: UUID_CAT1,
|
||||
condition: SelfCareCondition.Excellent,
|
||||
}));
|
||||
expect(result).toEqual(savedLog);
|
||||
});
|
||||
|
||||
it('should emit self-care.logged and activity events', async () => {
|
||||
categoryRepo.findOne.mockResolvedValue(makeCategory({ name: 'Haircut' }));
|
||||
const savedLog = makeLog();
|
||||
logRepo.create.mockReturnValue(savedLog);
|
||||
logRepo.save.mockResolvedValue(savedLog);
|
||||
|
||||
await service.logCategory(UUID_CAT1, { condition: SelfCareCondition.Good } as never);
|
||||
|
||||
expect(eventEmitter.emit).toHaveBeenCalledWith(
|
||||
'self-care.logged',
|
||||
expect.objectContaining({ categoryId: UUID_CAT1 }),
|
||||
);
|
||||
expect(eventEmitter.emit).toHaveBeenCalledWith(
|
||||
'activity',
|
||||
expect.objectContaining({ content: expect.stringContaining('Haircut') }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should mark entry as backdated when loggedAt is more than 1 hour ago', async () => {
|
||||
categoryRepo.findOne.mockResolvedValue(makeCategory());
|
||||
logRepo.create.mockImplementation((d) => d);
|
||||
logRepo.save.mockImplementation((e) => Promise.resolve(e));
|
||||
|
||||
const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString();
|
||||
|
||||
await service.logCategory(UUID_CAT1, {
|
||||
condition: SelfCareCondition.Good,
|
||||
loggedAt: twoHoursAgo,
|
||||
} as never);
|
||||
|
||||
expect(logRepo.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
isBackdated: true,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should not mark as backdated when loggedAt is recent', async () => {
|
||||
categoryRepo.findOne.mockResolvedValue(makeCategory());
|
||||
logRepo.create.mockImplementation((d) => d);
|
||||
logRepo.save.mockImplementation((e) => Promise.resolve(e));
|
||||
|
||||
await service.logCategory(UUID_CAT1, {
|
||||
condition: SelfCareCondition.Baseline,
|
||||
} as never);
|
||||
|
||||
expect(logRepo.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
isBackdated: false,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should throw NotFoundException for non-existent category', async () => {
|
||||
categoryRepo.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(service.logCategory('nonexistent', { condition: 'good' } as never)).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCategoryLogs', () => {
|
||||
it('should return paginated logs for a category', async () => {
|
||||
categoryRepo.findOne.mockResolvedValue(makeCategory());
|
||||
const logs = [makeLog(), makeLog({ id: UUID_2 })];
|
||||
const qb = logRepo.createQueryBuilder();
|
||||
qb.getManyAndCount.mockResolvedValue([logs, 2]);
|
||||
|
||||
const result = await service.getCategoryLogs(UUID_CAT1, {});
|
||||
|
||||
expect(qb.where).toHaveBeenCalledWith('log.categoryId = :categoryId', { categoryId: UUID_CAT1 });
|
||||
expect(result.data).toHaveLength(2);
|
||||
expect(result.meta).toEqual({ page: 1, limit: 20, total: 2, totalPages: 1 });
|
||||
});
|
||||
|
||||
it('should apply date range and condition filters', async () => {
|
||||
categoryRepo.findOne.mockResolvedValue(makeCategory());
|
||||
const qb = logRepo.createQueryBuilder();
|
||||
qb.getManyAndCount.mockResolvedValue([[], 0]);
|
||||
|
||||
await service.getCategoryLogs(UUID_CAT1, {
|
||||
dateStart: '2026-01-01',
|
||||
dateEnd: '2026-03-31',
|
||||
condition: SelfCareCondition.Good,
|
||||
});
|
||||
|
||||
expect(qb.andWhere).toHaveBeenCalledWith('log.loggedAt >= :dateStart', { dateStart: '2026-01-01' });
|
||||
expect(qb.andWhere).toHaveBeenCalledWith('log.loggedAt <= :dateEnd', { dateEnd: '2026-03-31' });
|
||||
expect(qb.andWhere).toHaveBeenCalledWith('log.condition = :condition', { condition: SelfCareCondition.Good });
|
||||
});
|
||||
|
||||
it('should throw NotFoundException for non-existent category', async () => {
|
||||
categoryRepo.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(service.getCategoryLogs('nonexistent', {})).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStatusBoard', () => {
|
||||
it('should return empty array when no active categories', async () => {
|
||||
categoryRepo.find.mockResolvedValue([]);
|
||||
|
||||
const result = await service.getStatusBoard();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should mark overdue categories', async () => {
|
||||
const cat = makeCategory({ cadenceDays: 7, cadenceWarningDays: 5 });
|
||||
categoryRepo.find.mockResolvedValue([cat]);
|
||||
|
||||
// Last log was 10 days ago
|
||||
const tenDaysAgo = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000);
|
||||
logRepo.findOne.mockResolvedValue(makeLog({ loggedAt: tenDaysAgo }));
|
||||
|
||||
// computeStreak needs these
|
||||
categoryRepo.findOne.mockResolvedValue(cat);
|
||||
logRepo.find.mockResolvedValue([makeLog({ loggedAt: tenDaysAgo })]);
|
||||
|
||||
const result = await service.getStatusBoard();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].isOverdue).toBe(true);
|
||||
expect(result[0].isWarning).toBe(false);
|
||||
});
|
||||
|
||||
it('should mark warning categories', async () => {
|
||||
const cat = makeCategory({ cadenceDays: 10, cadenceWarningDays: 7 });
|
||||
categoryRepo.find.mockResolvedValue([cat]);
|
||||
|
||||
// Last log was 8 days ago (within cadence but past warning)
|
||||
const eightDaysAgo = new Date(Date.now() - 8 * 24 * 60 * 60 * 1000);
|
||||
logRepo.findOne.mockResolvedValue(makeLog({ loggedAt: eightDaysAgo }));
|
||||
|
||||
categoryRepo.findOne.mockResolvedValue(cat);
|
||||
logRepo.find.mockResolvedValue([makeLog({ loggedAt: eightDaysAgo })]);
|
||||
|
||||
const result = await service.getStatusBoard();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].isOverdue).toBe(false);
|
||||
expect(result[0].isWarning).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle never-logged categories', async () => {
|
||||
const cat = makeCategory();
|
||||
categoryRepo.find.mockResolvedValue([cat]);
|
||||
logRepo.findOne.mockResolvedValue(null);
|
||||
|
||||
categoryRepo.findOne.mockResolvedValue(cat);
|
||||
logRepo.find.mockResolvedValue([]);
|
||||
|
||||
const result = await service.getStatusBoard();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].daysSince).toBeNull();
|
||||
expect(result[0].lastLog).toBeNull();
|
||||
});
|
||||
|
||||
it('should sort: overdue first, then warning, then ok, then never-logged', async () => {
|
||||
const catOverdue = makeCategory({ id: UUID_1, name: 'Overdue', cadenceDays: 5, cadenceWarningDays: 3 });
|
||||
const catWarning = makeCategory({ id: UUID_2, name: 'Warning', cadenceDays: 10, cadenceWarningDays: 5 });
|
||||
const catNeverLogged = makeCategory({ id: UUID_3, name: 'Never' });
|
||||
categoryRepo.find.mockResolvedValue([catNeverLogged, catWarning, catOverdue]);
|
||||
|
||||
const tenDaysAgo = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000);
|
||||
const sixDaysAgo = new Date(Date.now() - 6 * 24 * 60 * 60 * 1000);
|
||||
|
||||
logRepo.findOne.mockImplementation(({ where }: { where: { categoryId: string } }) => {
|
||||
if (where.categoryId === UUID_1) return Promise.resolve(makeLog({ loggedAt: tenDaysAgo }));
|
||||
if (where.categoryId === UUID_2) return Promise.resolve(makeLog({ loggedAt: sixDaysAgo }));
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
|
||||
categoryRepo.findOne.mockImplementation(({ where }: { where: { id: string } }) => {
|
||||
if (where.id === UUID_1) return Promise.resolve(catOverdue);
|
||||
if (where.id === UUID_2) return Promise.resolve(catWarning);
|
||||
if (where.id === UUID_3) return Promise.resolve(catNeverLogged);
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
|
||||
logRepo.find.mockImplementation(({ where }: { where: { categoryId: string } }) => {
|
||||
if (where.categoryId === UUID_1) return Promise.resolve([makeLog({ loggedAt: tenDaysAgo })]);
|
||||
if (where.categoryId === UUID_2) return Promise.resolve([makeLog({ loggedAt: sixDaysAgo })]);
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
|
||||
const result = await service.getStatusBoard();
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0].isOverdue).toBe(true);
|
||||
expect(result[1].isWarning).toBe(true);
|
||||
expect(result[2].daysSince).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCategoryStats', () => {
|
||||
it('should return stats with zero values when no logs', async () => {
|
||||
categoryRepo.findOne.mockResolvedValue(makeCategory({ cadenceDays: 14 }));
|
||||
logRepo.find.mockResolvedValue([]);
|
||||
|
||||
const result = await service.getCategoryStats(UUID_CAT1);
|
||||
|
||||
expect(result.totalLogs).toBe(0);
|
||||
expect(result.streakCount).toBe(0);
|
||||
expect(result.avgDaysBetweenLogs).toBeNull();
|
||||
expect(result.targetCadence).toBe(14);
|
||||
expect(result.conditionBreakdown).toEqual({});
|
||||
});
|
||||
|
||||
it('should calculate condition breakdown and average gap', async () => {
|
||||
const cat = makeCategory({ cadenceDays: 7 });
|
||||
categoryRepo.findOne.mockResolvedValue(cat);
|
||||
|
||||
const now = new Date();
|
||||
const logs = [
|
||||
makeLog({ loggedAt: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000), condition: SelfCareCondition.Good }),
|
||||
makeLog({ loggedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000), condition: SelfCareCondition.Excellent }),
|
||||
makeLog({ loggedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000), condition: SelfCareCondition.Good }),
|
||||
];
|
||||
logRepo.find.mockResolvedValue(logs);
|
||||
|
||||
const result = await service.getCategoryStats(UUID_CAT1);
|
||||
|
||||
expect(result.totalLogs).toBe(3);
|
||||
expect(result.conditionBreakdown).toEqual({
|
||||
[SelfCareCondition.Good]: 2,
|
||||
[SelfCareCondition.Excellent]: 1,
|
||||
});
|
||||
expect(result.avgDaysBetweenLogs).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException for non-existent category', async () => {
|
||||
categoryRepo.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(service.getCategoryStats('nonexistent')).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeStreak', () => {
|
||||
it('should return 0 when no logs', async () => {
|
||||
categoryRepo.findOne.mockResolvedValue(makeCategory());
|
||||
logRepo.find.mockResolvedValue([]);
|
||||
|
||||
const result = await service.computeStreak(UUID_CAT1);
|
||||
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('should return 0 for non-existent category', async () => {
|
||||
categoryRepo.findOne.mockResolvedValue(null);
|
||||
|
||||
const result = await service.computeStreak('nonexistent');
|
||||
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('should count consecutive logs within cadence', async () => {
|
||||
const cat = makeCategory({ cadenceDays: 7 });
|
||||
categoryRepo.findOne.mockResolvedValue(cat);
|
||||
|
||||
const now = new Date();
|
||||
const logs = [
|
||||
makeLog({ loggedAt: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000) }),
|
||||
makeLog({ loggedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000) }),
|
||||
makeLog({ loggedAt: new Date(now.getTime() - 12 * 24 * 60 * 60 * 1000) }),
|
||||
];
|
||||
logRepo.find.mockResolvedValue(logs);
|
||||
|
||||
const result = await service.computeStreak(UUID_CAT1);
|
||||
|
||||
// All gaps are within cadenceDays+1 = 8 days
|
||||
expect(result).toBe(3);
|
||||
});
|
||||
|
||||
it('should break streak when gap exceeds cadence', async () => {
|
||||
const cat = makeCategory({ cadenceDays: 3 });
|
||||
categoryRepo.findOne.mockResolvedValue(cat);
|
||||
|
||||
const now = new Date();
|
||||
const logs = [
|
||||
makeLog({ loggedAt: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000) }),
|
||||
makeLog({ loggedAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000) }),
|
||||
makeLog({ loggedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000) }),
|
||||
];
|
||||
logRepo.find.mockResolvedValue(logs);
|
||||
|
||||
const result = await service.computeStreak(UUID_CAT1);
|
||||
|
||||
// First gap is 2 days (within cadence+1=4), second gap is 17 days (exceeds)
|
||||
expect(result).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findBySlug', () => {
|
||||
it('should return a category by slug', async () => {
|
||||
const cat = makeCategory({ slug: 'haircut' });
|
||||
categoryRepo.findOne.mockResolvedValue(cat);
|
||||
|
||||
const result = await service.findBySlug('haircut');
|
||||
|
||||
expect(categoryRepo.findOne).toHaveBeenCalledWith({ where: { slug: 'haircut' } });
|
||||
expect(result).toEqual(cat);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException for non-existent slug', async () => {
|
||||
categoryRepo.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(service.findBySlug('nonexistent')).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SelfCareBaselineService', () => {
|
||||
let service: SelfCareBaselineService;
|
||||
let baselineRepo: MockRepository;
|
||||
let selfCareService: {
|
||||
getStatusBoard: jest.Mock;
|
||||
findOne: jest.Mock;
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
baselineRepo = mockRepository();
|
||||
selfCareService = {
|
||||
getStatusBoard: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
SelfCareBaselineService,
|
||||
{ provide: getRepositoryToken(SelfCareBaseline), useValue: baselineRepo },
|
||||
{ provide: SelfCareService, useValue: selfCareService },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get(SelfCareBaselineService);
|
||||
});
|
||||
|
||||
describe('capture', () => {
|
||||
it('should capture a baseline snapshot from status board', async () => {
|
||||
selfCareService.getStatusBoard.mockResolvedValue([
|
||||
{
|
||||
category: makeCategory({ id: UUID_CAT1, name: 'Haircut' }),
|
||||
daysSince: 5,
|
||||
isOverdue: false,
|
||||
isWarning: false,
|
||||
lastLog: makeLog({ condition: SelfCareCondition.Good, loggedAt: new Date('2026-03-17') }),
|
||||
streakCount: 3,
|
||||
},
|
||||
]);
|
||||
const created = makeBaseline({ name: 'March Baseline' });
|
||||
baselineRepo.create.mockReturnValue(created);
|
||||
baselineRepo.save.mockResolvedValue(created);
|
||||
|
||||
const result = await service.capture({ name: 'March Baseline' } as never);
|
||||
|
||||
expect(baselineRepo.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
name: 'March Baseline',
|
||||
snapshot: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
categoryId: UUID_CAT1,
|
||||
categoryName: 'Haircut',
|
||||
condition: SelfCareCondition.Good,
|
||||
}),
|
||||
]),
|
||||
}));
|
||||
expect(result).toEqual(created);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return all baselines ordered by takenAt DESC', async () => {
|
||||
const baselines = [makeBaseline(), makeBaseline({ id: UUID_2 })];
|
||||
baselineRepo.find.mockResolvedValue(baselines);
|
||||
|
||||
const result = await service.findAll();
|
||||
|
||||
expect(baselineRepo.find).toHaveBeenCalledWith({ order: { takenAt: 'DESC' } });
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return a baseline by id', async () => {
|
||||
const baseline = makeBaseline();
|
||||
baselineRepo.findOne.mockResolvedValue(baseline);
|
||||
|
||||
const result = await service.findOne(UUID_1);
|
||||
|
||||
expect(result).toEqual(baseline);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException for non-existent baseline', async () => {
|
||||
baselineRepo.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(service.findOne('nonexistent')).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compare', () => {
|
||||
it('should compare two baselines and detect improvements', async () => {
|
||||
const snapshotA: BaselineSnapshotEntry[] = [
|
||||
{ categoryId: UUID_CAT1, categoryName: 'Haircut', daysSinceLog: 5, condition: SelfCareCondition.Baseline, lastLoggedAt: '2026-03-10' },
|
||||
];
|
||||
const snapshotB: BaselineSnapshotEntry[] = [
|
||||
{ categoryId: UUID_CAT1, categoryName: 'Haircut', daysSinceLog: 2, condition: SelfCareCondition.Good, lastLoggedAt: '2026-03-20' },
|
||||
];
|
||||
|
||||
baselineRepo.findOne
|
||||
.mockResolvedValueOnce(makeBaseline({ id: UUID_1, snapshot: snapshotA }))
|
||||
.mockResolvedValueOnce(makeBaseline({ id: UUID_2, snapshot: snapshotB }));
|
||||
|
||||
const result = await service.compare(UUID_1, UUID_2);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].change).toBe('improved');
|
||||
expect(result[0].ordinalDiff).toBe(1);
|
||||
});
|
||||
|
||||
it('should detect declines', async () => {
|
||||
const snapshotA: BaselineSnapshotEntry[] = [
|
||||
{ categoryId: UUID_CAT1, categoryName: 'Haircut', daysSinceLog: 2, condition: SelfCareCondition.Excellent, lastLoggedAt: '2026-03-10' },
|
||||
];
|
||||
const snapshotB: BaselineSnapshotEntry[] = [
|
||||
{ categoryId: UUID_CAT1, categoryName: 'Haircut', daysSinceLog: 10, condition: SelfCareCondition.Rough, lastLoggedAt: '2026-03-20' },
|
||||
];
|
||||
|
||||
baselineRepo.findOne
|
||||
.mockResolvedValueOnce(makeBaseline({ id: UUID_1, snapshot: snapshotA }))
|
||||
.mockResolvedValueOnce(makeBaseline({ id: UUID_2, snapshot: snapshotB }));
|
||||
|
||||
const result = await service.compare(UUID_1, UUID_2);
|
||||
|
||||
expect(result[0].change).toBe('declined');
|
||||
expect(result[0].ordinalDiff).toBe(-3);
|
||||
});
|
||||
|
||||
it('should detect unchanged conditions', async () => {
|
||||
const snapshot: BaselineSnapshotEntry[] = [
|
||||
{ categoryId: UUID_CAT1, categoryName: 'Haircut', daysSinceLog: 3, condition: SelfCareCondition.Good, lastLoggedAt: '2026-03-10' },
|
||||
];
|
||||
|
||||
baselineRepo.findOne
|
||||
.mockResolvedValueOnce(makeBaseline({ id: UUID_1, snapshot }))
|
||||
.mockResolvedValueOnce(makeBaseline({ id: UUID_2, snapshot }));
|
||||
|
||||
const result = await service.compare(UUID_1, UUID_2);
|
||||
|
||||
expect(result[0].change).toBe('unchanged');
|
||||
expect(result[0].ordinalDiff).toBe(0);
|
||||
});
|
||||
|
||||
it('should return unknown when condition data is missing', async () => {
|
||||
const snapshotA: BaselineSnapshotEntry[] = [
|
||||
{ categoryId: UUID_CAT1, categoryName: 'Haircut', daysSinceLog: null, condition: null, lastLoggedAt: null },
|
||||
];
|
||||
const snapshotB: BaselineSnapshotEntry[] = [
|
||||
{ categoryId: UUID_CAT1, categoryName: 'Haircut', daysSinceLog: 5, condition: SelfCareCondition.Good, lastLoggedAt: '2026-03-20' },
|
||||
];
|
||||
|
||||
baselineRepo.findOne
|
||||
.mockResolvedValueOnce(makeBaseline({ id: UUID_1, snapshot: snapshotA }))
|
||||
.mockResolvedValueOnce(makeBaseline({ id: UUID_2, snapshot: snapshotB }));
|
||||
|
||||
const result = await service.compare(UUID_1, UUID_2);
|
||||
|
||||
expect(result[0].change).toBe('unknown');
|
||||
expect(result[0].ordinalDiff).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue