Files
splatoon2.ink/test/updater/Updater.test.js
2026-09-07 17:52:50 -07:00

186 lines
6.2 KiB
JavaScript

import { test, beforeEach, afterEach, mock } from 'node:test';
import assert from 'node:assert/strict';
import Updater from '../../src/app/updater/updaters/Updater.js';
import { fakeSplatNet, buckets, json, keys, setSessionEnvironment } from './support.js';
setSessionEnvironment();
const NAMES = {
en: { thing: 'Widget', other: 'Gadget' },
ja: { thing: 'ウィジェット', other: 'ガジェット' },
};
function thingsFor(language) {
const names = NAMES[language] ?? NAMES.en;
return {
things: [
{ id: '1', name: names.thing, image: '/images/things/one.png' },
{ id: '2', name: names.other, image: '/images/things/two.png' },
],
};
}
class ThingsUpdater extends Updater {
constructor(buckets, options = {}) {
super(
{
name: 'Things',
filename: 'things.json',
calendarFilename: 'things.ics',
request: splatnet => splatnet.getResponse('things'),
imagePaths: ['$..image'],
localization: [{ name: 'things', entities: '$.things[*]', id: 'id', values: 'name' }],
...options,
},
buckets,
);
}
getCalendarEntries(data) {
return data.things.map(thing => ({
id: `thing-${thing.id}`,
title: thing.name,
start_time: 3600,
end_time: 7200,
}));
}
}
let b;
beforeEach(() => {
b = buckets();
});
afterEach(() => mock.restoreAll());
test('publishes data, locale document, and calendar with the right metadata', async () => {
const splatnet = fakeSplatNet({ '/api/things': ({ language }) => thingsFor(language) });
await new ThingsUpdater(b).update();
assert.deepEqual(await json(b.publicBucket, 'data/things.json'), thingsFor('en'));
assert.deepEqual((await b.publicBucket.head('data/things.json')).httpMetadata, {
contentType: 'application/json',
cacheControl: 'no-cache, stale-while-revalidate=5, stale-if-error=86400',
});
assert.deepEqual(await json(b.publicBucket, 'data/locale/en.json'), {
things: { 1: { name: 'Widget' }, 2: { name: 'Gadget' } },
});
assert.deepEqual(await json(b.publicBucket, 'data/locale/ja.json'), {
things: { 1: { name: 'ウィジェット' }, 2: { name: 'ガジェット' } },
});
const ics = await (await b.publicBucket.get('data/things.ics')).text();
assert.match(ics, /SUMMARY:Widget/);
assert.match(ics, /X-WR-CALNAME:Things/);
assert.equal((await b.publicBucket.head('data/things.ics')).httpMetadata.contentType, 'text/calendar');
assert.equal(splatnet.api[0].language, 'en');
assert.equal(splatnet.api[0].region, 'NA');
});
test('fetches each language with missing strings using that region\'s session, then stops needing to', async () => {
const splatnet = fakeSplatNet({ '/api/things': ({ language }) => thingsFor(language) });
await new ThingsUpdater(b).update();
assert.deepEqual(
splatnet.api.map(r => `${r.region}/${r.language}`),
['NA/en', 'EU/es', 'NA/es-MX', 'EU/fr', 'NA/fr-CA', 'EU/de', 'EU/nl', 'EU/it', 'EU/ru', 'JP/ja'],
);
const second = fakeSplatNet({ '/api/things': ({ language }) => thingsFor(language) });
const puts = mock.method(b.publicBucket, 'put');
await new ThingsUpdater(b).update();
assert.deepEqual(
second.api.map(r => r.language),
['en'],
);
assert.deepEqual(
puts.mock.calls.map(c => c.arguments[0]).filter(k => k.startsWith('data/locale/')),
[],
);
});
test('mirrors only images that are missing, using one list per directory', async () => {
await b.publicBucket.put('assets/splatnet/images/things/one.png', 'existing');
const splatnet = fakeSplatNet({ '/api/things': ({ language }) => thingsFor(language) });
const list = mock.method(b.publicBucket, 'list');
const head = mock.method(b.publicBucket, 'head');
await new ThingsUpdater(b).update();
assert.deepEqual(
splatnet.images.map(r => r.path),
['/images/things/two.png'],
);
assert.equal(splatnet.images[0].cookie, null);
assert.equal(await (await b.publicBucket.get('assets/splatnet/images/things/one.png')).text(), 'existing');
assert.deepEqual(
list.mock.calls.map(c => c.arguments[0].prefix),
['assets/splatnet/images/things/'],
);
assert.equal(head.mock.calls.length, 0);
assert.equal(
(await b.publicBucket.get('assets/splatnet/images/things/two.png')).httpMetadata.contentType,
'image/png',
);
});
test('uses the embedded backup for the image SplatNet no longer serves', async () => {
const { cdnBackup } = await import('#cdn-images');
const missing = '/images/skill/53c62995f9d2dc4a60f3850c5dbdd2323f1eef87.png';
const splatnet = fakeSplatNet({
'/api/things': () => ({ things: [{ id: '1', name: 'Widget', image: missing }] }),
});
await new ThingsUpdater(b).update();
assert.deepEqual(splatnet.images, []);
const written = new Uint8Array(await (await b.publicBucket.get(`assets/splatnet${missing}`)).arrayBuffer());
assert.deepEqual(written, await cdnBackup(missing));
assert.deepEqual(written.slice(0, 4), new Uint8Array([0x89, 0x50, 0x4e, 0x47])); // a real PNG
assert.equal(written.byteLength, 8440);
});
test('filters root keys, dropping hidden timeline items', async () => {
fakeSplatNet({
'/api/things': () => ({ keep: { importance: 1, x: 1 }, hidden: { importance: -1 }, extra: 'dropped' }),
});
await new ThingsUpdater(b, {
rootKeys: ['keep', 'hidden', 'absent'],
localization: undefined,
imagePaths: undefined,
calendarFilename: undefined,
}).update();
assert.deepEqual(await json(b.publicBucket, 'data/things.json'), {
keep: { importance: 1, x: 1 },
hidden: null,
absent: null,
});
});
test('fails the run and writes nothing when SplatNet rejects the session', async () => {
fakeSplatNet({ '/api/things': () => new Response('forbidden', { status: 403 }) });
await assert.rejects(new ThingsUpdater(b).update(), /status 403/);
assert.deepEqual(keys(b.publicBucket), []);
});
test('leaves the existing locale document untouched when the update fails', async () => {
await b.publicBucket.put('data/locale/en.json', JSON.stringify({ things: { 9: { name: 'Old' } } }));
fakeSplatNet({ '/api/things': () => new Response('down', { status: 503 }) });
await assert.rejects(new ThingsUpdater(b).update());
assert.deepEqual(await json(b.publicBucket, 'data/locale/en.json'), { things: { 9: { name: 'Old' } } });
});