'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('fs'); const os = require('os'); const path = require('path'); const { parseGamePage, providerForTitleId, } = require('../../src/prospero'); const { createGameMetadataService, detectImageExtension } = require('../../src/services/game-metadata-service'); const { candidateScore, titleNumbersMatch, titlesStronglyMatch, isUsablePortrait, selectPortraitCover, fetchPortraitCover, } = require('../../src/services/portrait-cover-provider'); test('metadata provider routes PS4 and PS5 title IDs to the correct catalogue', () => { assert.equal(providerForTitleId('CUSA03041').baseUrl, 'https://orbispatches.com'); assert.equal(providerForTitleId('ppsa01411').baseUrl, 'https://prosperopatches.com'); assert.throws(() => providerForTitleId('NPXS12345'), /Unsupported title ID/); }); test('artwork type detection safely accepts CDN images without a MIME header', () => { const webp = Buffer.concat([Buffer.from('RIFF'), Buffer.alloc(4), Buffer.from('WEBP'), Buffer.alloc(8)]); assert.equal(detectImageExtension(webp), 'webp'); assert.equal(detectImageExtension(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])), 'png'); assert.equal(detectImageExtension(Buffer.from([0xff, 0xd8, 0xff, 0xdb])), 'jpg'); assert.equal(detectImageExtension(Buffer.from('')), ''); }); test('portrait cover provider accepts actual box art and rejects square store tiles', () => { const portrait = { source: 'https://upload.wikimedia.org/wikipedia/en/a/aa/Game.jpg', width: 600, height: 900 }; assert.equal(isUsablePortrait(portrait), true); assert.equal(isUsablePortrait({ ...portrait, source: 'https://cdn2.steamgriddb.com/grid/cover.png' }), false); assert.equal(isUsablePortrait({ ...portrait, width: 900, height: 900 }), false); assert.equal(isUsablePortrait({ ...portrait, source: 'https://example.com/game.jpg' }), false); assert.ok(candidateScore('Returnal', 'Returnal (video game)', 1) > candidateScore('Returnal', 'Unrelated game', 1)); assert.equal(titleNumbersMatch('Evil Genius 2', 'Evil Genius'), false); assert.equal(titleNumbersMatch('EA Sports FC 26', 'EA Sports FC 26'), true); assert.equal(titleNumbersMatch('Red Dead Redemption 2', 'Red Dead Redemption II'), true); assert.equal(titlesStronglyMatch('Disney Epic Mickey Rebrushed', 'Epic Mickey'), false); assert.equal(titlesStronglyMatch('Marvels Spider Man Remastered', 'Marvels Spider Man video game'), false); assert.equal(titlesStronglyMatch('Marvels Spider Man Remastered', 'Marvels Spider Man Remastered video game'), true); assert.equal(selectPortraitCover([ { title: 'Returnal soundtrack', index: 2, original: { ...portrait, width: 900, height: 900 } }, { title: 'Returnal (video game)', index: 1, original: portrait }, ], 'Returnal').title, 'Returnal (video game)'); }); test('portrait provider never falls back to Steam artwork', async () => { const calls = []; const fetchImpl = async url => { const target = String(url); calls.push(target); return { ok: true, status: 200, json: async () => ({ query: { pages: [] } }) }; }; const result = await fetchPortraitCover({ title: 'EA SPORTS FC 26', platform: 'PS5', fetchImpl }); assert.equal(result, null); assert.ok(calls.length >= 1); assert.ok(calls.every(url => url.startsWith('https://en.wikipedia.org/'))); }); test('portrait cover provider uses the public catalog and preserves attribution', async () => { const fetchImpl = async url => { assert.match(String(url), /^https:\/\/en\.wikipedia\.org\/w\/api\.php\?/); return { ok: true, status: 200, json: async () => ({ query: { pages: [{ title: "Marvel's Spider-Man (video game)", index: 1, fullurl: 'https://en.wikipedia.org/wiki/Marvel%27s_Spider-Man_(video_game)', original: { source: 'https://upload.wikimedia.org/wikipedia/en/e/e1/Spider-Man_PS4_cover.jpg', width: 280, height: 356, }, }], }, }), }; }; const result = await fetchPortraitCover({ title: "Marvel's Spider-Man", platform: 'PS4', fetchImpl }); assert.equal(result.coverSource, 'wikipedia-portrait-v3'); assert.equal(result.coverWidth, 280); assert.match(result.coverUrl, /^https:\/\/upload\.wikimedia\.org\//); assert.match(result.coverSourceUrl, /^https:\/\/en\.wikipedia\.org\//); }); test('PS4 metadata parser keeps complete titles and extracts Orbis artwork', () => { const html = ` CUSA03041: Red Dead Redemption 2 | ORBISPatches.com
`; const parsed = parseGamePage(html, 'CUSA03041'); assert.equal(parsed.name, 'Red Dead Redemption 2'); assert.equal(parsed.dataKey, 'b'.repeat(64)); assert.match(parsed.iconUrl, /^https:\/\/cdn\.orbispatches\.com\//); assert.match(parsed.bannerUrl, /pic0\.webp$/); }); test('PS5 metadata parser does not truncate hyphenated game titles', () => { const html = ` PPSA01411: Marvel's Spider-Man: Miles Morales | PROSPEROPatches.com
`; const parsed = parseGamePage(html, 'PPSA01411'); assert.equal(parsed.name, "Marvel's Spider-Man: Miles Morales"); assert.equal(parsed.dataKey, 'd'.repeat(64)); }); test('metadata parser decodes HTML entities in full titles and descriptions', () => { const html = 'CUSA00001: Tom & Jerry's Adventure | ORBISPatches.com'; const parsed = parseGamePage(html, 'CUSA00001'); assert.equal(parsed.name, "Tom & Jerry's Adventure"); assert.equal(parsed.description, 'Action & adventure'); }); test('server metadata service caches trusted artwork under the authenticated cache route', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-artwork-')); const writes = []; const image = Buffer.concat([Buffer.from('RIFF'), Buffer.alloc(4), Buffer.from('WEBP'), Buffer.alloc(8)]); const response = { ok: true, status: 200, headers: { get: name => name === 'content-type' ? 'image/webp' : (name === 'content-length' ? String(image.length) : null) }, buffer: async () => image, json: async () => ({ query: { pages: [] } }), }; const db = { updateProsperoData: (gameId, data) => writes.push({ gameId, data }), listGames: () => [], }; const service = createGameMetadataService({ db, cacheDir: root, fetchImpl: async () => response }); const original = require('../../src/prospero').fetchGameMetadata; require('../../src/prospero').fetchGameMetadata = async () => ({ name: 'Game', iconUrl: 'https://cdn.orbispatches.com/titles/CUSA03041_hash/icon0.webp', bannerUrl: '', patches: [], }); try { const data = await service.fetchOne('CUSA03041'); assert.equal(data.iconUrl, '/pork-cache/game-metadata/CUSA03041/icon.webp'); assert.equal(writes[0].data.iconUrl, data.iconUrl); assert.equal(writes[0].data.coverSource, 'catalog-no-match'); assert.ok(Number(writes[0].data.coverFetchedAt) > 0); assert.deepEqual(fs.readFileSync(path.join(root, 'game-metadata', 'CUSA03041', 'icon.webp')), image); } finally { require('../../src/prospero').fetchGameMetadata = original; fs.rmSync(root, { recursive: true, force: true }); } }); test('temporary cover catalog failures remain eligible for a later retry', async () => { const writes = []; const image = Buffer.concat([Buffer.from('RIFF'), Buffer.alloc(4), Buffer.from('WEBP'), Buffer.alloc(8)]); const fetchImpl = async url => { if (String(url).startsWith('https://en.wikipedia.org/')) return { ok: false, status: 429, headers: { get: () => null } }; return { ok: true, status: 200, url: String(url), headers: { get: name => name === 'content-type' ? 'image/webp' : String(image.length) }, buffer: async () => image, }; }; const db = { updateProsperoData: (gameId, data) => writes.push({ gameId, data }), listGames: () => [] }; const service = createGameMetadataService({ db, fetchImpl }); const original = require('../../src/prospero').fetchGameMetadata; require('../../src/prospero').fetchGameMetadata = async () => ({ name: 'Retry Game', iconUrl: '', bannerUrl: '', patches: [] }); try { await service.fetchOne('PPSA00001'); assert.equal(writes[0].data.coverFetchedAt, null); assert.equal(writes[0].data.coverSource, undefined); } finally { require('../../src/prospero').fetchGameMetadata = original; } }); test('metadata enrichment caches a validated portrait cover separately from the square icon', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-cover-')); const writes = []; const jpeg = Buffer.from([0xff, 0xd8, 0xff, 0xdb, 0x00, 0x01]); const webp = Buffer.concat([Buffer.from('RIFF'), Buffer.alloc(4), Buffer.from('WEBP'), Buffer.alloc(8)]); const fetchImpl = async url => { const target = String(url); if (target.startsWith('https://en.wikipedia.org/w/api.php?')) { return { ok: true, status: 200, url: target, json: async () => ({ query: { pages: [{ title: 'Returnal', index: 1, fullurl: 'https://en.wikipedia.org/wiki/Returnal', original: { source: 'https://upload.wikimedia.org/wikipedia/en/9/91/Returnal_cover_art.jpg', width: 273, height: 365, }, }], }, }), }; } const isCover = target.startsWith('https://upload.wikimedia.org/'); const body = isCover ? jpeg : webp; return { ok: true, status: 200, url: target, headers: { get: name => name === 'content-type' ? (isCover ? 'image/jpeg' : 'image/webp') : (name === 'content-length' ? String(body.length) : null) }, buffer: async () => body, }; }; const db = { updateProsperoData: (gameId, data) => writes.push({ gameId, data }), listGames: () => [], }; const service = createGameMetadataService({ db, cacheDir: root, fetchImpl }); const original = require('../../src/prospero').fetchGameMetadata; require('../../src/prospero').fetchGameMetadata = async () => ({ name: 'Returnal', iconUrl: 'https://cdn.prosperopatches.com/titles/PPSA01284_hash/icon0.webp', bannerUrl: '', patches: [], }); try { const data = await service.fetchOne('PPSA01284'); assert.equal(data.iconUrl, '/pork-cache/game-metadata/PPSA01284/icon.webp'); assert.equal(data.coverUrl, '/pork-cache/game-metadata/PPSA01284/cover.jpg'); assert.equal(data.coverSource, 'wikipedia-portrait-v3'); assert.equal(writes[0].data.coverUrl, data.coverUrl); assert.deepEqual(fs.readFileSync(path.join(root, 'game-metadata', 'PPSA01284', 'cover.jpg')), jpeg); } finally { require('../../src/prospero').fetchGameMetadata = original; fs.rmSync(root, { recursive: true, force: true }); } });