Files
zen-kanji/server/tests/services/sync.service.test.js
Rene Kievits 6438660b03 init
2025-12-18 01:30:52 +01:00

213 lines
5.2 KiB
JavaScript

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);
});
});