This commit is contained in:
Rene Kievits
2025-12-18 01:30:52 +01:00
commit 6438660b03
78 changed files with 14230 additions and 0 deletions

View File

@@ -0,0 +1,144 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as ReviewService from '../../src/services/review.service.js';
import { User } from '../../src/models/User.js';
import { ReviewLog } from '../../src/models/ReviewLog.js';
import { StudyItem } from '../../src/models/StudyItem.js';
vi.mock('../../src/models/ReviewLog.js');
vi.mock('../../src/models/StudyItem.js');
describe('Review Service', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2023-01-10T00:00:00Z'));
});
afterEach(() => {
vi.useRealTimers();
vi.resetAllMocks();
});
describe('processReview', () => {
const mockUser = (stats = {}) => ({
_id: 'u1',
stats: { ...stats },
save: vi.fn()
});
const mockItem = (srs = 1) => ({
userId: 'u1',
wkSubjectId: 100,
srsLevel: srs,
stats: { correct: 0, total: 0 },
save: vi.fn(),
nextReview: null
});
it('should throw error if item not found', async () => {
const user = mockUser();
StudyItem.findOne.mockResolvedValue(null);
await expect(ReviewService.processReview(user, 999, true))
.rejects.toThrow('Item not found');
expect(StudyItem.findOne).toHaveBeenCalledWith({
userId: user._id,
wkSubjectId: 999
});
});
it('should handle standard success flow', async () => {
const user = mockUser();
delete user.stats;
StudyItem.findOne.mockResolvedValue(mockItem());
const res = await ReviewService.processReview(user, 100, true);
expect(user.save).toHaveBeenCalled();
expect(res.srsLevel).toBe(2);
});
it('should handle failure flow', async () => {
const user = mockUser();
const item = mockItem(2);
StudyItem.findOne.mockResolvedValue(item);
await ReviewService.processReview(user, 100, false);
expect(item.srsLevel).toBe(1);
});
it('should increment streak (diff = 1)', async () => {
const user = mockUser({ lastStudyDate: '2023-01-09', currentStreak: 5 });
StudyItem.findOne.mockResolvedValue(mockItem());
await ReviewService.processReview(user, 100, true);
expect(user.stats.currentStreak).toBe(6);
});
it('should maintain streak (diff = 0)', async () => {
const user = mockUser({ lastStudyDate: '2023-01-10', currentStreak: 5 });
StudyItem.findOne.mockResolvedValue(mockItem());
await ReviewService.processReview(user, 100, true);
expect(user.stats.currentStreak).toBe(5);
});
it('should reset streak (diff > 1, no shield)', async () => {
const user = mockUser({ lastStudyDate: '2023-01-08', currentStreak: 5, lastFreezeDate: '2023-01-09' });
StudyItem.findOne.mockResolvedValue(mockItem());
await ReviewService.processReview(user, 100, true);
expect(user.stats.currentStreak).toBe(1);
});
it('should use shield (diff = 2, shield ready)', async () => {
const user = mockUser({ lastStudyDate: '2023-01-08', currentStreak: 5, lastFreezeDate: '2022-01-01' });
StudyItem.findOne.mockResolvedValue(mockItem());
await ReviewService.processReview(user, 100, true);
expect(user.stats.currentStreak).toBe(6);
expect(user.stats.lastFreezeDate).toBeDefined();
});
it('should use shield (diff = 2) when lastFreezeDate is undefined', async () => {
const user = mockUser({ lastStudyDate: '2023-01-08', currentStreak: 5 });
user.stats.lastFreezeDate = null;
StudyItem.findOne.mockResolvedValue(mockItem());
await ReviewService.processReview(user, 100, true);
expect(user.stats.currentStreak).toBe(6);
expect(user.stats.lastFreezeDate).toBeDefined();
});
it('should initialize item stats if missing', async () => {
const user = mockUser();
const item = mockItem();
delete item.stats;
StudyItem.findOne.mockResolvedValue(item);
await ReviewService.processReview(user, 100, true);
expect(item.stats).toEqual(expect.objectContaining({ correct: 1, total: 1 }));
});
it('should not break on time travel (diff < 0)', async () => {
const user = mockUser({ lastStudyDate: '2023-01-11', currentStreak: 5 });
StudyItem.findOne.mockResolvedValue(mockItem());
await ReviewService.processReview(user, 100, true);
expect(user.stats.lastStudyDate).toBe('2023-01-10');
expect(user.stats.currentStreak).toBe(5);
});
});
describe('getQueue', () => {
it('should sort by priority', async () => {
const mockFind = { sort: vi.fn().mockReturnThis(), limit: vi.fn().mockResolvedValue([]) };
StudyItem.find.mockReturnValue(mockFind);
await ReviewService.getQueue({ _id: 'u1' }, 10, 'priority');
expect(mockFind.sort).toHaveBeenCalled();
});
it('should shuffle (default)', async () => {
const mockFind = { limit: vi.fn().mockResolvedValue([1, 2]) };
StudyItem.find.mockReturnValue(mockFind);
await ReviewService.getQueue({ _id: 'u1' }, 10, 'random');
expect(mockFind.limit).toHaveBeenCalled();
});
});
});