Files

254 lines
12 KiB
JavaScript

'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('<script>alert(1)</script>')), '');
});
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 = `<!doctype html><html><head>
<title>CUSA03041: Red Dead Redemption 2 | ORBISPatches.com</title>
<meta name="twitter:image" content="https://cdn.orbispatches.com/titles/CUSA03041_${'a'.repeat(64)}/icon0.webp">
<meta property="og:description" content="An open-world western adventure.">
</head><body>
<header style="background:url(https://cdn.orbispatches.com/titles/CUSA03041_${'a'.repeat(64)}/pic0.webp)"></header>
<div id="dynpatch" data-loadparams="{ 'titleid': 'CUSA03041', 'key': '${'b'.repeat(64)}' }"></div>
</body></html>`;
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 = `<!doctype html><html><head>
<title>PPSA01411: Marvel's Spider-Man: Miles Morales | PROSPEROPatches.com</title>
<meta property="og:image" content="https://cdn.prosperopatches.com/titles/PPSA01411_${'c'.repeat(64)}/icon0.webp">
</head><body><div id="dynpatch" data-key="${'d'.repeat(64)}"></div></body></html>`;
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 = '<html><head><title>CUSA00001: Tom &amp; Jerry&#39;s Adventure | ORBISPatches.com</title><meta property="og:description" content="Action &amp; adventure"></head></html>';
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 });
}
});