init
This commit is contained in:
52
server/tests/services/auth.service.test.js
Normal file
52
server/tests/services/auth.service.test.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { loginUser, logoutUser } from '../../src/services/auth.service.js';
|
||||
import { User } from '../../src/models/User.js';
|
||||
|
||||
vi.mock('../../src/models/User.js');
|
||||
global.fetch = vi.fn();
|
||||
|
||||
describe('Auth Service', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('loginUser', () => {
|
||||
it('should throw error for invalid API Key', async () => {
|
||||
fetch.mockResolvedValue({ status: 401 });
|
||||
await expect(loginUser('bad_key')).rejects.toThrow('Invalid API Key');
|
||||
});
|
||||
|
||||
it('should return existing user if found', async () => {
|
||||
fetch.mockResolvedValue({ status: 200 });
|
||||
const mockUser = { wkApiKey: 'valid_key', _id: '123' };
|
||||
User.findOne.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await loginUser('valid_key');
|
||||
expect(result).toEqual(mockUser);
|
||||
expect(User.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create new user if not found', async () => {
|
||||
fetch.mockResolvedValue({ status: 200 });
|
||||
User.findOne.mockResolvedValue(null);
|
||||
const newUser = { wkApiKey: 'valid_key', _id: 'new_id' };
|
||||
User.create.mockResolvedValue(newUser);
|
||||
|
||||
const result = await loginUser('valid_key');
|
||||
expect(result).toEqual(newUser);
|
||||
expect(User.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
wkApiKey: 'valid_key',
|
||||
tokenVersion: 0
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('logoutUser', () => {
|
||||
it('should increment token version', async () => {
|
||||
User.findByIdAndUpdate.mockResolvedValue(true);
|
||||
const result = await logoutUser('userId');
|
||||
expect(User.findByIdAndUpdate).toHaveBeenCalledWith('userId', { $inc: { tokenVersion: 1 } });
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
142
server/tests/services/controllers.test.js
Normal file
142
server/tests/services/controllers.test.js
Normal file
@@ -0,0 +1,142 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import * as AuthController from '../../src/controllers/auth.controller.js';
|
||||
import * as ReviewController from '../../src/controllers/review.controller.js';
|
||||
import * as SyncController from '../../src/controllers/sync.controller.js';
|
||||
import * as CollectionController from '../../src/controllers/collection.controller.js';
|
||||
|
||||
import * as AuthService from '../../src/services/auth.service.js';
|
||||
import * as ReviewService from '../../src/services/review.service.js';
|
||||
import * as SyncService from '../../src/services/sync.service.js';
|
||||
import * as StatsService from '../../src/services/stats.service.js';
|
||||
import { StudyItem } from '../../src/models/StudyItem.js';
|
||||
|
||||
vi.mock('../../src/services/auth.service.js');
|
||||
vi.mock('../../src/services/review.service.js');
|
||||
vi.mock('../../src/services/sync.service.js');
|
||||
vi.mock('../../src/services/stats.service.js');
|
||||
vi.mock('../../src/models/StudyItem.js');
|
||||
|
||||
const mockReq = (body = {}, user = {}, query = {}) => ({ body, user, query });
|
||||
const mockReply = () => {
|
||||
const res = {};
|
||||
res.code = vi.fn().mockReturnValue(res);
|
||||
res.send = vi.fn().mockReturnValue(res);
|
||||
res.jwtSign = vi.fn().mockResolvedValue('token');
|
||||
return res;
|
||||
};
|
||||
|
||||
describe('Controllers', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Auth Controller', () => {
|
||||
it('login should fail without apiKey', async () => {
|
||||
const reply = mockReply();
|
||||
await AuthController.login(mockReq({}), reply);
|
||||
expect(reply.code).toHaveBeenCalledWith(400);
|
||||
});
|
||||
|
||||
it('login should succeed', async () => {
|
||||
const reply = mockReply();
|
||||
AuthService.loginUser.mockResolvedValue({ _id: 1, tokenVersion: 1 });
|
||||
await AuthController.login(mockReq({ apiKey: 'key' }), reply);
|
||||
expect(reply.jwtSign).toHaveBeenCalled();
|
||||
expect(reply.code).not.toHaveBeenCalledWith(401);
|
||||
});
|
||||
|
||||
it('login should catch errors', async () => {
|
||||
const reply = mockReply();
|
||||
AuthService.loginUser.mockRejectedValue(new Error('fail'));
|
||||
await AuthController.login(mockReq({ apiKey: 'k' }), reply);
|
||||
expect(reply.code).toHaveBeenCalledWith(401);
|
||||
});
|
||||
|
||||
it('logout should succeed', async () => {
|
||||
const reply = mockReply();
|
||||
await AuthController.logout(mockReq({}, { _id: 1 }), reply);
|
||||
expect(AuthService.logoutUser).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Review Controller', () => {
|
||||
it('submitReview should succeed', async () => {
|
||||
const reply = mockReply();
|
||||
ReviewService.processReview.mockResolvedValue({});
|
||||
await ReviewController.submitReview(mockReq({}), reply);
|
||||
expect(reply.send).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('submitReview should handle error', async () => {
|
||||
const reply = mockReply();
|
||||
ReviewService.processReview.mockRejectedValue(new Error('err'));
|
||||
await ReviewController.submitReview(mockReq({}), reply);
|
||||
expect(reply.code).toHaveBeenCalledWith(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sync Controller', () => {
|
||||
it('sync should succeed', async () => {
|
||||
const reply = mockReply();
|
||||
SyncService.syncWithWaniKani.mockResolvedValue({});
|
||||
await SyncController.sync(mockReq({}, {}), reply);
|
||||
expect(reply.send).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sync should handle error', async () => {
|
||||
const reply = mockReply();
|
||||
SyncService.syncWithWaniKani.mockRejectedValue(new Error('err'));
|
||||
await SyncController.sync(mockReq({}, {}), reply);
|
||||
expect(reply.code).toHaveBeenCalledWith(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Collection Controller', () => {
|
||||
it('getCollection should return items', async () => {
|
||||
const reply = mockReply();
|
||||
StudyItem.find.mockResolvedValue([]);
|
||||
await CollectionController.getCollection(mockReq({}, { _id: 1 }), reply);
|
||||
expect(reply.send).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('getQueue should call service with default limit', async () => {
|
||||
const reply = mockReply();
|
||||
ReviewService.getQueue.mockResolvedValue([]);
|
||||
await CollectionController.getQueue(mockReq({}, {}, {}), reply);
|
||||
expect(ReviewService.getQueue).toHaveBeenCalledWith(expect.anything(), 20, undefined);
|
||||
});
|
||||
|
||||
it('getQueue should call service with provided limit', async () => {
|
||||
const reply = mockReply();
|
||||
ReviewService.getQueue.mockResolvedValue([]);
|
||||
await CollectionController.getQueue(mockReq({}, {}, { limit: '50' }), reply);
|
||||
expect(ReviewService.getQueue).toHaveBeenCalledWith(expect.anything(), 50, undefined);
|
||||
});
|
||||
|
||||
it('getStats should call service', async () => {
|
||||
const reply = mockReply();
|
||||
StatsService.getUserStats.mockResolvedValue({});
|
||||
await CollectionController.getStats(mockReq({}, {}), reply);
|
||||
expect(StatsService.getUserStats).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updateSettings should initialize settings if missing', async () => {
|
||||
const reply = mockReply();
|
||||
const save = vi.fn();
|
||||
const user = { save };
|
||||
await CollectionController.updateSettings(mockReq({ batchSize: 50 }, user), reply);
|
||||
expect(user.settings).toBeDefined();
|
||||
expect(user.settings.batchSize).toBe(50);
|
||||
expect(save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updateSettings should update drawingAccuracy', async () => {
|
||||
const reply = mockReply();
|
||||
const save = vi.fn();
|
||||
const user = { settings: {}, save };
|
||||
await CollectionController.updateSettings(mockReq({ drawingAccuracy: 5 }, user), reply);
|
||||
expect(user.settings.drawingAccuracy).toBe(5);
|
||||
expect(save).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
144
server/tests/services/review.service.test.js
Normal file
144
server/tests/services/review.service.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
147
server/tests/services/stats.service.test.js
Normal file
147
server/tests/services/stats.service.test.js
Normal file
@@ -0,0 +1,147 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { getUserStats } from '../../src/services/stats.service.js';
|
||||
import { StudyItem } from '../../src/models/StudyItem.js';
|
||||
import { ReviewLog } from '../../src/models/ReviewLog.js';
|
||||
|
||||
vi.mock('../../src/models/StudyItem.js');
|
||||
vi.mock('../../src/models/ReviewLog.js');
|
||||
|
||||
describe('Stats Service', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2023-01-10T12:00:00Z'));
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockUser = {
|
||||
_id: 'u1',
|
||||
stats: {
|
||||
totalReviews: 10,
|
||||
correctReviews: 8,
|
||||
currentStreak: 5,
|
||||
lastStudyDate: '2023-01-10',
|
||||
lastFreezeDate: '2022-01-01'
|
||||
}
|
||||
};
|
||||
|
||||
it('should calculate stats correctly including ghosts, forecast, and unknown SRS levels', async () => {
|
||||
StudyItem.aggregate.mockResolvedValue([
|
||||
{ _id: 1, count: 5 },
|
||||
{ _id: 8, count: 2 }
|
||||
]);
|
||||
|
||||
StudyItem.find.mockImplementation(() => ({
|
||||
select: vi.fn().mockImplementation((fields) => {
|
||||
if (fields.includes('nextReview')) {
|
||||
const now = new Date();
|
||||
return Promise.resolve([
|
||||
{ nextReview: new Date(now.getTime() - 1000) },
|
||||
{ nextReview: new Date(now.getTime() + 3600000) },
|
||||
{ nextReview: new Date(now.getTime() + 24 * 3600000) }
|
||||
]);
|
||||
}
|
||||
if (fields.includes('stats')) {
|
||||
const mkGhost = (acc) => ({
|
||||
char: 'A', meaning: 'B', stats: { correct: acc, total: 100 }, srsLevel: 1,
|
||||
toObject: () => ({ char: 'A', meaning: 'B', stats: { correct: acc, total: 100 }, srsLevel: 1 })
|
||||
});
|
||||
return Promise.resolve([mkGhost(60), mkGhost(50), mkGhost(90)]);
|
||||
}
|
||||
if (fields.includes('srsLevel')) {
|
||||
return Promise.resolve([{ srsLevel: 1 }, { srsLevel: 4 }]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
})
|
||||
}));
|
||||
|
||||
ReviewLog.find.mockResolvedValue([
|
||||
{ date: '2023-01-09', count: 5 }
|
||||
]);
|
||||
|
||||
const stats = await getUserStats(mockUser);
|
||||
|
||||
expect(stats.forecast[0]).toBe(1);
|
||||
expect(stats.forecast[1]).toBe(1);
|
||||
|
||||
expect(stats.ghosts.length).toBe(2);
|
||||
expect(stats.ghosts[0].accuracy).toBe(50);
|
||||
expect(stats.ghosts[1].accuracy).toBe(60);
|
||||
|
||||
expect(stats.distribution[1]).toBe(5);
|
||||
expect(stats.distribution[8]).toBeUndefined();
|
||||
|
||||
expect(stats.heatmap['2023-01-09']).toBe(5);
|
||||
});
|
||||
|
||||
it('should handle queue with same levels (hasLowerLevels = false)', async () => {
|
||||
StudyItem.aggregate.mockResolvedValue([]);
|
||||
StudyItem.find.mockImplementation(() => ({
|
||||
select: vi.fn().mockImplementation((fields) => {
|
||||
if (fields.includes('stats')) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
if (fields.includes('srsLevel')) {
|
||||
return Promise.resolve([{ srsLevel: 2 }, { srsLevel: 2 }]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
})
|
||||
}));
|
||||
ReviewLog.find.mockResolvedValue([]);
|
||||
|
||||
const stats = await getUserStats(mockUser);
|
||||
expect(stats.hasLowerLevels).toBe(false);
|
||||
});
|
||||
|
||||
it('should calculate shield cooldown', async () => {
|
||||
StudyItem.aggregate.mockResolvedValue([]);
|
||||
StudyItem.find.mockReturnValue({ select: vi.fn().mockResolvedValue([]) });
|
||||
ReviewLog.find.mockResolvedValue([]);
|
||||
|
||||
const frozenUser = {
|
||||
...mockUser,
|
||||
stats: { ...mockUser.stats, lastFreezeDate: '2023-01-07' }
|
||||
};
|
||||
|
||||
const stats = await getUserStats(frozenUser);
|
||||
expect(stats.streak.shield.ready).toBe(false);
|
||||
expect(stats.streak.shield.cooldown).toBe(4);
|
||||
});
|
||||
|
||||
it('should handle streak logic when lastStudyDate is missing or old', async () => {
|
||||
const userNoDate = { ...mockUser, stats: { ...mockUser.stats, lastStudyDate: null, currentStreak: 5 } };
|
||||
StudyItem.aggregate.mockResolvedValue([]);
|
||||
StudyItem.find.mockReturnValue({ select: vi.fn().mockResolvedValue([]) });
|
||||
ReviewLog.find.mockResolvedValue([]);
|
||||
|
||||
const res1 = await getUserStats(userNoDate);
|
||||
expect(res1.streak.current).toBe(5);
|
||||
|
||||
const userMissed = { ...mockUser, stats: { ...mockUser.stats, lastStudyDate: '2023-01-08' } };
|
||||
const res2 = await getUserStats(userMissed);
|
||||
expect(res2.streak.current).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle missing user stats fields (null checks)', async () => {
|
||||
const emptyUser = {
|
||||
_id: 'u2',
|
||||
stats: {}
|
||||
};
|
||||
|
||||
StudyItem.aggregate.mockResolvedValue([]);
|
||||
StudyItem.find.mockReturnValue({ select: vi.fn().mockResolvedValue([]) });
|
||||
ReviewLog.find.mockResolvedValue([]);
|
||||
|
||||
const stats = await getUserStats(emptyUser);
|
||||
|
||||
expect(stats.streak.current).toBe(0);
|
||||
expect(stats.streak.best).toBe(0);
|
||||
expect(stats.accuracy.total).toBe(0);
|
||||
expect(stats.accuracy.correct).toBe(0);
|
||||
|
||||
expect(stats.streak.shield.ready).toBe(true);
|
||||
});
|
||||
});
|
||||
212
server/tests/services/sync.service.test.js
Normal file
212
server/tests/services/sync.service.test.js
Normal file
@@ -0,0 +1,212 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { syncWithWaniKani } from '../../src/services/sync.service.js';
|
||||
import { StudyItem } from '../../src/models/StudyItem.js';
|
||||
|
||||
vi.mock('../../src/models/StudyItem.js');
|
||||
global.fetch = vi.fn();
|
||||
|
||||
describe('Sync Service', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockUser = { _id: 'u1', wkApiKey: 'key', settings: {} };
|
||||
|
||||
it('should throw if no API key', async () => {
|
||||
await expect(syncWithWaniKani({}))
|
||||
.rejects.toThrow('User has no WaniKani API Key');
|
||||
});
|
||||
|
||||
it('should handle API fetch error', async () => {
|
||||
fetch.mockResolvedValue({ ok: false, statusText: 'Unauthorized' });
|
||||
await expect(syncWithWaniKani(mockUser))
|
||||
.rejects.toThrow('Failed to connect to WaniKani');
|
||||
});
|
||||
|
||||
it('should return 0 if no unlocked kanji', async () => {
|
||||
fetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: [], pages: { next_url: null } })
|
||||
});
|
||||
|
||||
const res = await syncWithWaniKani(mockUser);
|
||||
expect(res.count).toBe(0);
|
||||
expect(res.message).toContain('No unlocked kanji');
|
||||
});
|
||||
|
||||
it('should sync new items with full reading data', async () => {
|
||||
fetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: [{ data: { subject_id: 101 } }],
|
||||
pages: { next_url: null }
|
||||
})
|
||||
});
|
||||
|
||||
StudyItem.find.mockReturnValue({ select: vi.fn().mockResolvedValue([]) });
|
||||
|
||||
fetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: [{
|
||||
id: 101,
|
||||
data: {
|
||||
characters: 'A',
|
||||
meanings: [{ primary: true, meaning: 'A_Meaning' }],
|
||||
level: 5,
|
||||
readings: [
|
||||
{ type: 'onyomi', reading: 'on' },
|
||||
{ type: 'kunyomi', reading: 'kun' },
|
||||
{ type: 'nanori', reading: 'nan' }
|
||||
]
|
||||
}
|
||||
}]
|
||||
})
|
||||
});
|
||||
|
||||
StudyItem.insertMany.mockResolvedValue(true);
|
||||
StudyItem.countDocuments.mockResolvedValue(1);
|
||||
|
||||
await syncWithWaniKani(mockUser);
|
||||
|
||||
expect(StudyItem.insertMany).toHaveBeenCalledWith(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
wkSubjectId: 101,
|
||||
onyomi: ['on'],
|
||||
kunyomi: ['kun'],
|
||||
nanori: ['nan']
|
||||
})
|
||||
]));
|
||||
});
|
||||
|
||||
it('should filter out existing items', async () => {
|
||||
fetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: [
|
||||
{ data: { subject_id: 101 } },
|
||||
{ data: { subject_id: 102 } }
|
||||
],
|
||||
pages: { next_url: null }
|
||||
})
|
||||
});
|
||||
|
||||
StudyItem.find.mockReturnValue({
|
||||
select: vi.fn().mockResolvedValue([{ wkSubjectId: 102 }])
|
||||
});
|
||||
|
||||
fetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: [{
|
||||
id: 101,
|
||||
data: {
|
||||
characters: 'New',
|
||||
meanings: [],
|
||||
level: 1,
|
||||
readings: []
|
||||
}
|
||||
}]
|
||||
})
|
||||
});
|
||||
|
||||
StudyItem.insertMany.mockResolvedValue(true);
|
||||
StudyItem.countDocuments.mockResolvedValue(2);
|
||||
|
||||
await syncWithWaniKani(mockUser);
|
||||
|
||||
expect(StudyItem.insertMany).toHaveBeenCalledTimes(1);
|
||||
expect(StudyItem.insertMany).toHaveBeenCalledWith(expect.arrayContaining([
|
||||
expect.objectContaining({ wkSubjectId: 101 })
|
||||
]));
|
||||
expect(StudyItem.insertMany).not.toHaveBeenCalledWith(expect.arrayContaining([
|
||||
expect.objectContaining({ wkSubjectId: 102 })
|
||||
]));
|
||||
});
|
||||
|
||||
it('should handle assignment pagination', async () => {
|
||||
fetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: [{ data: { subject_id: 1 } }],
|
||||
pages: { next_url: 'http://next-page' }
|
||||
})
|
||||
});
|
||||
fetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: [{ data: { subject_id: 2 } }],
|
||||
pages: { next_url: null }
|
||||
})
|
||||
});
|
||||
|
||||
StudyItem.find.mockReturnValue({ select: vi.fn().mockResolvedValue([]) });
|
||||
|
||||
fetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: [] })
|
||||
});
|
||||
|
||||
StudyItem.countDocuments.mockResolvedValue(2);
|
||||
|
||||
await syncWithWaniKani(mockUser);
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should skip insert if operations are empty (e.g. subject data missing)', async () => {
|
||||
fetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: [{ data: { subject_id: 101 } }],
|
||||
pages: { next_url: null }
|
||||
})
|
||||
});
|
||||
|
||||
StudyItem.find.mockReturnValue({ select: vi.fn().mockResolvedValue([]) });
|
||||
|
||||
fetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: [] })
|
||||
});
|
||||
|
||||
StudyItem.countDocuments.mockResolvedValue(0);
|
||||
|
||||
await syncWithWaniKani(mockUser);
|
||||
|
||||
expect(StudyItem.insertMany).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should sync items in chunks', async () => {
|
||||
const manyIds = Array.from({ length: 150 }, (_, i) => i + 1);
|
||||
const subjectData = manyIds.map(id => ({ data: { subject_id: id } }));
|
||||
|
||||
fetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: subjectData,
|
||||
pages: { next_url: null }
|
||||
})
|
||||
});
|
||||
|
||||
StudyItem.find.mockReturnValue({ select: vi.fn().mockResolvedValue([]) });
|
||||
|
||||
fetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: [{ id: 1, data: { characters: 'C', meanings: [], level: 1 } }] })
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: [{ id: 101, data: { characters: 'D', meanings: [], level: 1 } }] })
|
||||
});
|
||||
|
||||
StudyItem.insertMany.mockResolvedValue(true);
|
||||
StudyItem.countDocuments.mockResolvedValue(150);
|
||||
|
||||
await syncWithWaniKani(mockUser);
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(3);
|
||||
expect(StudyItem.insertMany).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user