Files
wikideck/tests/wiki.test.ts
T
2026-10-19 18:52:49 -04:00

83 lines
4.0 KiB
TypeScript
Executable File

import test from 'node:test';
import assert from 'node:assert/strict';
import { WikipediaArticleSource } from '../server/wiki.ts';
function page(pageid: number, patch: Record<string, unknown> = {}) {
return { pageid, ns: 0, title: `Article ${pageid}`, extract: `An encyclopedia article with enough explanatory text for a useful collectible card. ${pageid}.`,
lastrevid: pageid + 100, thumbnail: { source: `https://upload.wikimedia.org/wikipedia/commons/${pageid}.jpg` },
categories: [{ title: 'Category:Science' }], ...patch };
}
function json(value: unknown, headers: Record<string, string> = {}) {
return new Response(JSON.stringify(value), { headers: { 'Content-Type': 'application/json', ...headers } });
}
function mockFetch(handler: (url: URL, init: RequestInit) => Response | Promise<Response>) {
const calls: { url: URL; init: RequestInit }[] = [];
const fetcher: typeof fetch = async (input, init) => {
const url = new URL(String(input));
calls.push({ url, init: init! });
return handler(url, init!);
};
return { fetcher, calls };
}
test('random article lookup retries batches with no pictures and selects a pictured page', async () => {
let attempt = 0;
const { fetcher, calls } = mockFetch(() => {
attempt++;
return attempt === 1
? json({ query: { pages: [page(10, { thumbnail: undefined }), page(11, { thumbnail: undefined })] } })
: json({ query: { pages: [page(12)] } });
});
const article = await new WikipediaArticleSource(fetcher).random();
assert.equal(attempt, 2);
assert.equal(article.pageId, 12);
assert.equal(article.source, 'live');
assert.equal(article.image, 'https://upload.wikimedia.org/wikipedia/commons/12.jpg');
assert.equal(article.url, 'https://en.wikipedia.org/?curid=12');
assert.equal(calls.length, 2);
assert.equal(calls[0].url.origin, 'https://en.wikipedia.org');
assert.equal(calls[0].url.searchParams.get('generator'), 'random');
assert.equal(calls[0].url.searchParams.get('grnlimit'), '10');
assert.equal(calls[0].url.searchParams.get('piprop'), 'thumbnail');
assert.equal(calls[0].init.redirect, 'error');
});
test('only HTTPS Wikimedia pictures are eligible and random lookup has a bounded retry count', async () => {
const { fetcher, calls } = mockFetch(() => json({ query: { pages: [
page(20, { thumbnail: { source: 'https://evil.example/tracker.jpg' } }),
page(21, { thumbnail: { source: 'http://upload.wikimedia.org/image.jpg' } }),
] } }));
await assert.rejects(new WikipediaArticleSource(fetcher).random(), /three attempts/);
assert.equal(calls.length, 3);
});
test('random article fetch rejects unsafe response types, redirects, and oversized payloads without fallback', async () => {
const failures = [
() => new Response('{}', { headers: { 'Content-Type': 'text/html' } }),
() => new Response(null, { status: 302, headers: { Location: 'https://evil.example' } }),
() => new Response(' '.repeat(512 * 1024 + 1), { headers: { 'Content-Type': 'application/json' } }),
() => { throw new Error('offline'); },
];
for (const fail of failures) {
const { fetcher, calls } = mockFetch(() => fail());
await assert.rejects(new WikipediaArticleSource(fetcher).random());
assert.equal(calls.length, 3);
}
});
test('bad page records are skipped while later valid candidates are considered', async () => {
const { fetcher } = mockFetch(() => json({ query: { pages: [
page(30, { pageid: Number.MAX_SAFE_INTEGER + 1 }), page(31, { extract: 'short' }), page(32),
] } }));
const article = await new WikipediaArticleSource(fetcher).random();
assert.equal(article.pageId, 32);
});
test('a previously selected Wikipedia page may be selected again', async () => {
const { fetcher } = mockFetch(() => json({ query: { pages: [page(40)] } }));
const source = new WikipediaArticleSource(fetcher);
const first = await source.random();
const second = await source.random();
assert.equal(first.pageId, second.pageId);
});