'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const { GreatestCovers } = require('../greatest/covers'); const PNG = Buffer.from('89504e470d0a1a0a0000000d49484452', 'hex'); const image = (body = PNG, type = 'image/png') => new Response(body, { status: 200, headers: { 'content-type': type } }); function setup(t, responses, options = {}) { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mc-greatest-covers-')); t.after(() => fs.rmSync(dir, { recursive: true, force: true })); const calls = []; const fetchImpl = async (url, init) => { calls.push({ url, init }); const next = responses[url]; if (next instanceof Error) throw next; return typeof next === 'function' ? next() : next || new Response('missing', { status: 404 }); }; const sleeps = []; const covers = new GreatestCovers(dir, { fetchImpl, sleep: async ms => { sleeps.push(ms); }, ...options }); return { dir, calls, sleeps, covers }; } test('warm downloads each cover once and serves it from disk afterwards', async t => { const a = 'https://img.example/a.png', b = 'https://img.example/b.jpg'; const { dir, calls, sleeps, covers } = setup(t, { [a]: () => image(), [b]: () => image(PNG, 'image/jpeg') }); const result = await covers.warm([a, b, a]); assert.deepEqual(result, { fetched: 2, skipped: 1, failed: 0, stopped: null }); assert.equal(calls.length, 2); assert.deepEqual(sleeps, [1000], 'one pause between two downloads, none before the first'); assert.ok(covers.file(a).endsWith('.png') && fs.existsSync(covers.file(a))); assert.ok(covers.file(b).endsWith('.jpg')); assert.equal(covers.file('https://img.example/other.png'), null); // A restarted reader finds the stored files and asks nobody. const reopened = new GreatestCovers(dir, { fetchImpl: async () => { throw new Error('must not fetch'); } }); assert.deepEqual(await reopened.warm([a, b]), { fetched: 0, skipped: 2, failed: 0, stopped: null }); assert.equal(reopened.file(a), covers.file(a)); }); test('a changed cover URL is a new file', async t => { const { covers } = setup(t, { 'https://img.example/v1.png': () => image(), 'https://img.example/v2.png': () => image() }); await covers.warm(['https://img.example/v1.png', 'https://img.example/v2.png']); assert.notEqual(covers.file('https://img.example/v1.png'), covers.file('https://img.example/v2.png')); assert.notEqual(covers.version('https://img.example/v1.png'), covers.version('https://img.example/v2.png')); }); test('non-images, oversized files, plain http and errors are skipped', async t => { const html = 'https://img.example/page', big = 'https://img.example/big.png', declaredBig = 'https://img.example/declared.png'; const broken = 'https://img.example/broken.png', gone = 'https://img.example/gone.png'; const { covers, calls } = setup(t, { [html]: () => new Response('', { status: 200, headers: { 'content-type': 'text/html' } }), [big]: () => image(Buffer.alloc(64)), [declaredBig]: () => new Response(PNG, { status: 200, headers: { 'content-type': 'image/png', 'content-length': '999999' } }), [broken]: new TypeError('fetch failed'), }, { maxBytes: 32 }); const result = await covers.warm([html, big, declaredBig, broken, gone, 'http://img.example/plain.png', '', null]); assert.deepEqual(result, { fetched: 0, skipped: 0, failed: 5, stopped: null }); assert.equal(calls.length, 5, 'plain http and empty URLs are never requested'); for (const url of [html, big, declaredBig, broken, gone]) assert.equal(covers.file(url), null); assert.deepEqual(fs.readdirSync(covers.dir).filter(name => !name.startsWith('.')), []); }); test('429 and 403 stop the run at once', async t => { for (const status of [429, 403]) { const first = 'https://img.example/1.png', second = 'https://img.example/2.png'; const { covers, calls } = setup(t, { [first]: () => new Response('', { status }), [second]: () => image() }); const result = await covers.warm([first, second]); assert.equal(result.stopped, `HTTP ${status}`); assert.equal(calls.length, 1); assert.equal(covers.file(second), null); } }); test('requests carry a timeout and a fixed user agent', async t => { const a = 'https://img.example/a.png'; const { covers, calls } = setup(t, { [a]: () => image() }); await covers.warm([a]); assert.ok(calls[0].init.signal instanceof AbortSignal); assert.match(calls[0].init.headers['user-agent'], /^MangaCircus/); });