Files
SubMiner/src/core/services/anki-jimaku-ipc.test.ts
T

267 lines
9.4 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { registerAnkiJimakuIpcHandlers } from './anki-jimaku-ipc';
import { IPC_CHANNELS } from '../../shared/ipc/contracts';
function createFakeRegistrar(): {
registrar: {
on: (channel: string, listener: (event: unknown, ...args: unknown[]) => void) => void;
handle: (channel: string, listener: (event: unknown, ...args: unknown[]) => unknown) => void;
};
onHandlers: Map<string, (event: unknown, ...args: unknown[]) => void>;
handleHandlers: Map<string, (event: unknown, ...args: unknown[]) => unknown>;
} {
const onHandlers = new Map<string, (event: unknown, ...args: unknown[]) => void>();
const handleHandlers = new Map<string, (event: unknown, ...args: unknown[]) => unknown>();
return {
registrar: {
on: (channel, listener) => {
onHandlers.set(channel, listener);
},
handle: (channel, listener) => {
handleHandlers.set(channel, listener);
},
},
onHandlers,
handleHandlers,
};
}
test('anki/jimaku IPC handlers reject malformed invoke payloads', async () => {
const { registrar, handleHandlers } = createFakeRegistrar();
let previewCalls = 0;
registerAnkiJimakuIpcHandlers(
{
setAnkiConnectEnabled: () => {},
clearAnkiHistory: () => {},
refreshKnownWords: async () => {},
respondFieldGrouping: () => {},
buildKikuMergePreview: async () => {
previewCalls += 1;
return { ok: true };
},
getJimakuMediaInfo: () => ({
title: 'x',
season: null,
episode: null,
confidence: 'high',
filename: 'x.mkv',
rawTitle: 'x',
}),
searchJimakuEntries: async () => ({ ok: true, data: [] }),
listJimakuFiles: async () => ({ ok: true, data: [] }),
resolveJimakuApiKey: async () => 'token',
getCurrentMediaPath: () => '/tmp/a.mkv',
isRemoteMediaPath: () => false,
downloadToFile: async () => ({ ok: true, path: '/tmp/sub.ass' }),
onDownloadedSubtitle: () => {},
searchTsukihimeEntries: async () => ({ ok: true, data: [] }),
listTsukihimeFiles: async () => ({ ok: true, data: [] }),
downloadTsukihimeSubtitle: async () => ({ ok: true, path: '/tmp/sub.en.ass' }),
getTsukihimeSecondaryLanguages: () => ['en'],
onDownloadedSecondarySubtitle: () => {},
},
registrar,
);
const previewHandler = handleHandlers.get(IPC_CHANNELS.request.kikuBuildMergePreview);
assert.ok(previewHandler);
const invalidPreviewResult = await previewHandler!({}, null);
assert.deepEqual(invalidPreviewResult, {
ok: false,
error: 'Invalid merge preview request payload',
});
await previewHandler!({}, { keepNoteId: 1, deleteNoteId: 2, deleteDuplicate: false });
assert.equal(previewCalls, 1);
const searchHandler = handleHandlers.get(IPC_CHANNELS.request.jimakuSearchEntries);
assert.ok(searchHandler);
const invalidSearchResult = await searchHandler!({}, { query: 12 });
assert.deepEqual(invalidSearchResult, {
ok: false,
error: { error: 'Invalid Jimaku search query payload', code: 400 },
});
const filesHandler = handleHandlers.get(IPC_CHANNELS.request.jimakuListFiles);
assert.ok(filesHandler);
const invalidFilesResult = await filesHandler!({}, { entryId: 'x' });
assert.deepEqual(invalidFilesResult, {
ok: false,
error: { error: 'Invalid Jimaku files query payload', code: 400 },
});
const downloadHandler = handleHandlers.get(IPC_CHANNELS.request.jimakuDownloadFile);
assert.ok(downloadHandler);
const invalidDownloadResult = await downloadHandler!({}, { entryId: 1, url: '/x' });
assert.deepEqual(invalidDownloadResult, {
ok: false,
error: { error: 'Invalid Jimaku download query payload', code: 400 },
});
const tsukihimeSearchHandler = handleHandlers.get(IPC_CHANNELS.request.tsukihimeSearchEntries);
assert.ok(tsukihimeSearchHandler);
const invalidTsukihimeSearch = await tsukihimeSearchHandler!({}, { query: 12 });
assert.deepEqual(invalidTsukihimeSearch, {
ok: false,
error: { error: 'Invalid TsukiHime search query payload', code: 400 },
});
const tsukihimeFilesHandler = handleHandlers.get(IPC_CHANNELS.request.tsukihimeListFiles);
assert.ok(tsukihimeFilesHandler);
const invalidTsukihimeFiles = await tsukihimeFilesHandler!({}, { entryId: 'x' });
assert.deepEqual(invalidTsukihimeFiles, {
ok: false,
error: { error: 'Invalid TsukiHime files query payload', code: 400 },
});
const tsukihimeDownloadHandler = handleHandlers.get(IPC_CHANNELS.request.tsukihimeDownloadFile);
assert.ok(tsukihimeDownloadHandler);
const invalidTsukihimeDownload = await tsukihimeDownloadHandler!({}, { entryId: 1, url: '/x' });
assert.deepEqual(invalidTsukihimeDownload, {
ok: false,
error: { error: 'Invalid TsukiHime download query payload', code: 400 },
});
const foreignUrlDownload = await tsukihimeDownloadHandler!(
{},
{ entryId: 1, url: 'https://evil.example/attach/00000001/1.xz', name: 'sub.ass' },
);
assert.deepEqual(foreignUrlDownload, {
ok: false,
error: { error: 'Refusing to download subtitle from a non-TsukiHime URL.', code: 400 },
});
});
test('tsukihime downloads always route Japanese as primary', async () => {
const { registrar, handleHandlers } = createFakeRegistrar();
const primaryLoads: string[] = [];
const secondaryLoads: string[] = [];
registerAnkiJimakuIpcHandlers(
{
setAnkiConnectEnabled: () => {},
clearAnkiHistory: () => {},
refreshKnownWords: async () => {},
respondFieldGrouping: () => {},
buildKikuMergePreview: async () => ({ ok: true }),
getJimakuMediaInfo: () => ({
title: 'x',
season: null,
episode: null,
confidence: 'high',
filename: 'x.mkv',
rawTitle: 'x',
}),
searchJimakuEntries: async () => ({ ok: true, data: [] }),
listJimakuFiles: async () => ({ ok: true, data: [] }),
resolveJimakuApiKey: async () => 'token',
getCurrentMediaPath: () => '/tmp/a.mkv',
isRemoteMediaPath: () => false,
downloadToFile: async () => ({ ok: true, path: '/tmp/sub.ass' }),
onDownloadedSubtitle: (path) => {
primaryLoads.push(path);
},
searchTsukihimeEntries: async () => ({ ok: true, data: [] }),
listTsukihimeFiles: async () => ({ ok: true, data: [] }),
downloadTsukihimeSubtitle: async (_url, destPath) => ({ ok: true, path: destPath }),
getTsukihimeSecondaryLanguages: () => ['ja'],
onDownloadedSecondarySubtitle: (path) => {
secondaryLoads.push(path);
},
},
registrar,
);
const downloadHandler = handleHandlers.get(IPC_CHANNELS.request.tsukihimeDownloadFile)!;
const engResult = (await downloadHandler!(
{},
{
entryId: 1,
url: 'https://storage.tsukihime.org/attach/00000001/1.xz',
name: 'episode.eng.ass',
lang: 'eng',
},
)) as { ok: boolean };
assert.equal(engResult.ok, true);
assert.equal(primaryLoads.length, 0);
assert.equal(secondaryLoads.length, 1);
assert.match(secondaryLoads[0]!, /\.en.*\.ass$/);
const jpnResult = (await downloadHandler!(
{},
{
entryId: 1,
url: 'https://storage.tsukihime.org/attach/00000002/2.xz',
name: 'episode.jpn.ass',
lang: 'jpn',
},
)) as { ok: boolean };
assert.equal(jpnResult.ok, true);
assert.equal(secondaryLoads.length, 1);
assert.equal(primaryLoads.length, 1);
assert.match(primaryLoads[0]!, /\.ja.*\.ass$/);
});
test('anki/jimaku IPC command handlers ignore malformed payloads', () => {
const { registrar, onHandlers } = createFakeRegistrar();
const fieldGroupingChoices: unknown[] = [];
const enabledStates: boolean[] = [];
registerAnkiJimakuIpcHandlers(
{
setAnkiConnectEnabled: (enabled) => {
enabledStates.push(enabled);
},
clearAnkiHistory: () => {},
refreshKnownWords: async () => {},
respondFieldGrouping: (choice) => {
fieldGroupingChoices.push(choice);
},
buildKikuMergePreview: async () => ({ ok: true }),
getJimakuMediaInfo: () => ({
title: 'x',
season: null,
episode: null,
confidence: 'high',
filename: 'x.mkv',
rawTitle: 'x',
}),
searchJimakuEntries: async () => ({ ok: true, data: [] }),
listJimakuFiles: async () => ({ ok: true, data: [] }),
resolveJimakuApiKey: async () => 'token',
getCurrentMediaPath: () => '/tmp/a.mkv',
isRemoteMediaPath: () => false,
downloadToFile: async () => ({ ok: true, path: '/tmp/sub.ass' }),
onDownloadedSubtitle: () => {},
searchTsukihimeEntries: async () => ({ ok: true, data: [] }),
listTsukihimeFiles: async () => ({ ok: true, data: [] }),
downloadTsukihimeSubtitle: async () => ({ ok: true, path: '/tmp/sub.en.ass' }),
getTsukihimeSecondaryLanguages: () => ['en'],
onDownloadedSecondarySubtitle: () => {},
},
registrar,
);
onHandlers.get(IPC_CHANNELS.command.setAnkiConnectEnabled)!({}, 'true');
onHandlers.get(IPC_CHANNELS.command.setAnkiConnectEnabled)!({}, true);
assert.deepEqual(enabledStates, [true]);
onHandlers.get(IPC_CHANNELS.command.kikuFieldGroupingRespond)!({}, null);
onHandlers.get(IPC_CHANNELS.command.kikuFieldGroupingRespond)!(
{},
{
keepNoteId: 1,
deleteNoteId: 2,
deleteDuplicate: false,
cancelled: false,
},
);
assert.deepEqual(fieldGroupingChoices, [
{
keepNoteId: 1,
deleteNoteId: 2,
deleteDuplicate: false,
cancelled: false,
},
]);
});