120 lines
4.1 KiB
TypeScript
120 lines
4.1 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { NutritionTargetService } from '../../../src/modules/nutrition-targets/nutrition-target.service.js';
|
|
import type { NutritionTargetRepository } from '../../../src/modules/nutrition-targets/nutrition-target.repository.js';
|
|
|
|
describe(NutritionTargetService.name, () => {
|
|
let service: NutritionTargetService;
|
|
let mockRepo: { [K in keyof NutritionTargetRepository]: ReturnType<typeof vi.fn> };
|
|
|
|
beforeEach(() => {
|
|
mockRepo = {
|
|
findByUser: vi.fn(),
|
|
findAllByUser: vi.fn(),
|
|
create: vi.fn(),
|
|
deactivateAllForUser: vi.fn(),
|
|
update: vi.fn(),
|
|
} as never;
|
|
|
|
service = new NutritionTargetService({
|
|
nutritionTargetRepository: mockRepo as unknown as NutritionTargetRepository,
|
|
});
|
|
});
|
|
|
|
describe('getActiveByUser', () => {
|
|
it('delegates to repository', async () => {
|
|
const mockTarget = { dailyCalories: 2000, isActive: true };
|
|
mockRepo.findByUser.mockResolvedValue(mockTarget);
|
|
|
|
const result = await service.getActiveByUser('u1', 'h1');
|
|
expect(mockRepo.findByUser).toHaveBeenCalledWith('u1', 'h1');
|
|
expect(result).toEqual(mockTarget);
|
|
});
|
|
});
|
|
|
|
describe('getAllByUser', () => {
|
|
it('delegates to repository', async () => {
|
|
const mockTargets = [{ dailyCalories: 2000 }, { dailyCalories: 1800 }];
|
|
mockRepo.findAllByUser.mockResolvedValue(mockTargets);
|
|
|
|
const result = await service.getAllByUser('u1', 'h1');
|
|
expect(mockRepo.findAllByUser).toHaveBeenCalledWith('u1', 'h1');
|
|
expect(result).toEqual(mockTargets);
|
|
});
|
|
});
|
|
|
|
describe('setTarget', () => {
|
|
it('deactivates existing targets before creating an active one', async () => {
|
|
const mockInput = { dailyCalories: 2000, proteinG: 100, carbsG: 200, fatG: 50, isActive: true };
|
|
const createdTarget = { ...mockInput, _id: 'new-id', userId: 'u1', householdId: 'h1' };
|
|
mockRepo.create.mockResolvedValue(createdTarget);
|
|
|
|
const result = await service.setTarget('u1', 'h1', mockInput);
|
|
|
|
expect(mockRepo.deactivateAllForUser).toHaveBeenCalledWith('u1', 'h1');
|
|
expect(mockRepo.create).toHaveBeenCalledWith({
|
|
...mockInput,
|
|
userId: 'u1',
|
|
householdId: 'h1',
|
|
});
|
|
expect(result).toEqual(createdTarget);
|
|
});
|
|
|
|
it('does NOT deactivate others if isActive is explicitly false', async () => {
|
|
const mockInput = { dailyCalories: 2000, proteinG: 100, carbsG: 200, fatG: 50, isActive: false };
|
|
await service.setTarget('u1', 'h1', mockInput);
|
|
|
|
expect(mockRepo.deactivateAllForUser).not.toHaveBeenCalled();
|
|
expect(mockRepo.create).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('calculatePreset', () => {
|
|
it('calculates macros correctly for maintenance (30p / 40c / 30f)', () => {
|
|
const result = service.calculatePreset(2000, 'maintenance');
|
|
|
|
// Math:
|
|
// Protein: (2000 * 0.3) / 4 = 600 / 4 = 150
|
|
// Carbs: (2000 * 0.4) / 4 = 800 / 4 = 200
|
|
// Fat: (2000 * 0.3) / 9 = 600 / 9 = 66.66 => 67
|
|
expect(result).toEqual({
|
|
dailyCalories: 2000,
|
|
proteinG: 150,
|
|
carbsG: 200,
|
|
fatG: 67,
|
|
isActive: true,
|
|
});
|
|
});
|
|
|
|
it('calculates macros correctly for loss (40p / 30c / 30f)', () => {
|
|
const result = service.calculatePreset(2000, 'loss');
|
|
|
|
// Math:
|
|
// Protein: (2000 * 0.4) / 4 = 800 / 4 = 200
|
|
// Carbs: (2000 * 0.3) / 4 = 600 / 4 = 150
|
|
// Fat: (2000 * 0.3) / 9 = 600 / 9 = 66.66 => 67
|
|
expect(result).toEqual({
|
|
dailyCalories: 2000,
|
|
proteinG: 200,
|
|
carbsG: 150,
|
|
fatG: 67,
|
|
isActive: true,
|
|
});
|
|
});
|
|
|
|
it('calculates macros correctly for gain (25p / 50c / 25f)', () => {
|
|
const result = service.calculatePreset(2000, 'gain');
|
|
|
|
// Math:
|
|
// Protein: (2000 * 0.25) / 4 = 500 / 4 = 125
|
|
// Carbs: (2000 * 0.5) / 4 = 1000 / 4 = 250
|
|
// Fat: (2000 * 0.25) / 9 = 500 / 9 = 55.55 => 56
|
|
expect(result).toEqual({
|
|
dailyCalories: 2000,
|
|
proteinG: 125,
|
|
carbsG: 250,
|
|
fatG: 56,
|
|
isActive: true,
|
|
});
|
|
});
|
|
});
|
|
});
|