292 lines
7.1 KiB
JavaScript
292 lines
7.1 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: 100 }, (_, 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(100);
|
|
|
|
await syncWithWaniKani(mockUser);
|
|
|
|
expect(fetch).toHaveBeenCalledTimes(3);
|
|
expect(StudyItem.insertMany).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('should fetch and map radicals for items, handling missing meanings', async () => {
|
|
fetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
data: [{ data: { subject_id: 500 } }],
|
|
pages: { next_url: null }
|
|
})
|
|
});
|
|
|
|
StudyItem.find.mockReturnValue({ select: vi.fn().mockResolvedValue([]) });
|
|
|
|
fetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
data: [{
|
|
id: 500,
|
|
data: {
|
|
characters: 'Kanji',
|
|
meanings: [{ primary: true, meaning: 'K_Mean' }],
|
|
level: 5,
|
|
readings: [],
|
|
component_subject_ids: [10, 11, 12]
|
|
}
|
|
}]
|
|
})
|
|
});
|
|
|
|
fetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
data: [
|
|
{
|
|
id: 10,
|
|
data: {
|
|
characters: 'R1',
|
|
meanings: [{ primary: true, meaning: 'Rad1' }],
|
|
character_images: []
|
|
}
|
|
},
|
|
{
|
|
id: 11,
|
|
data: {
|
|
characters: null,
|
|
meanings: [{ primary: true, meaning: 'Rad2' }],
|
|
character_images: [
|
|
{ content_type: 'image/png', url: 'bad.png', metadata: {} },
|
|
{ content_type: 'image/svg+xml', url: 'good.svg', metadata: { inline_styles: false } }
|
|
]
|
|
}
|
|
},
|
|
{
|
|
id: 12,
|
|
data: {
|
|
characters: 'R2',
|
|
meanings: [{ primary: false, meaning: 'Secondary' }],
|
|
character_images: []
|
|
}
|
|
}
|
|
]
|
|
})
|
|
});
|
|
|
|
StudyItem.insertMany.mockResolvedValue(true);
|
|
StudyItem.countDocuments.mockResolvedValue(1);
|
|
|
|
await syncWithWaniKani(mockUser);
|
|
|
|
expect(StudyItem.insertMany).toHaveBeenCalledWith(expect.arrayContaining([
|
|
expect.objectContaining({
|
|
wkSubjectId: 500,
|
|
radicals: expect.arrayContaining([
|
|
expect.objectContaining({ meaning: 'Rad1', char: 'R1' }),
|
|
expect.objectContaining({ meaning: 'Rad2', image: 'good.svg' }),
|
|
expect.objectContaining({ meaning: 'Unknown', char: 'R2' })
|
|
])
|
|
})
|
|
]));
|
|
});
|
|
});
|