83 lines
4.0 KiB
TypeScript
Executable File
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);
|
|
});
|