clone

a browser grid puzzle game where you push words around to rewrite the rules of each level

test: preserve catalog on malformed shared automata

Dami committed 4ead8c1
commit
4ead8c190ea18558ac38e1a689eedaba1a8d1d72
parent
5e2180e564717f0a8c5eb9c3c97e0f18fa79321a
author
Dami <dami.etoile@gmail.com>
1 file changed, +49 -0
Mtests/pack-service.test.js49++++++++++++++++++++++++++++++
M tests/pack-service.test.js +49 -0
@@ -4,6 +4,26 @@ import { createPackService } from '../src/pack-service.js';
44 import { createPackLibrary } from '../src/pack-library.js';55 import { exportCommonJS } from '../src/share.js';66 7+function fnv1a(text) {8+ let hash = 0x811c9dc5;9+ for (const byte of new TextEncoder().encode(text)) {10+ hash ^= byte;11+ hash = Math.imul(hash, 0x01000193) >>> 0;12+ }13+ return hash.toString(16).padStart(8, '0');14+}15+16+function tamperSharedPayload(payload, mutate) {17+ const prefix = 'miau1.';18+ assert.ok(payload.startsWith(prefix));19+ const body = payload.slice(prefix.length);20+ const split = body.lastIndexOf('.');21+ const envelope = JSON.parse(Buffer.from(body.slice(0, split), 'base64url').toString('utf8'));22+ mutate(envelope.pack);23+ const json = JSON.stringify(envelope);24+ return `${prefix}${Buffer.from(json, 'utf8').toString('base64url')}.${fnv1a(json)}`;25+}26+727 const example = {828 manifest: { id: 'gift-pack', name: 'Gift Pack', author: 'Ada', version: '1.0.0' },929 palette: { CAT: '🐈' },
@@ -57,6 +77,35 @@ test('link decoding is data-only and does not install until confirmed', () => {
5777 assert.equal(receiver.listPacks().find(pack => pack.id === 'gift-pack').origin, 'url');5878 });5979 80+test('rejects malformed shared automata atomically without changing the catalog', () => {81+ const automataPack = structuredClone(example);82+ automataPack.manifest.id = 'automata-pack';83+ automataPack.manifest.name = 'Automata Pack';84+ automataPack.levels[0].automata = [{85+ id: 'clock',86+ property: 'TICK',87+ pattern: [{ action: 'wait' }],88+ }];89+ const sender = createPackService({ baseUrl: 'https://fcbc.n.zip/' });90+ assert.equal(sender.installPack(automataPack).ok, true);91+ const shared = sender.packLink('automata-pack');92+ assert.equal(shared.ok, true);93+ const malformedPayload = tamperSharedPayload(new URL(shared.url).hash.slice('#pack='.length), pack => {94+ pack.levels[0].automata[0].pattern[0] = { action: 'move', dx: 2, dy: 0 };95+ });96+97+ const receiver = createPackService();98+ assert.equal(receiver.installPack(example).ok, true);99+ const beforePacks = receiver.catalog.getPacks();100+ const beforeLevels = receiver.catalog.getLevels();101+ const result = receiver.decodePackLink(`#pack=${malformedPayload}`);102+103+ assert.equal(result.ok, false);104+ assert.match(result.errors[0].detail, /levels\[0\]\.automata\[0\]\.pattern\[0\].*cardinal/);105+ assert.deepEqual(receiver.catalog.getPacks(), beforePacks);106+ assert.deepEqual(receiver.catalog.getLevels(), beforeLevels);107+});108+60109 test('local and shared previews expose rule warnings outside portable pack data', () => {61110 const warned = structuredClone(example);62111 warned.levels[0].objects[2].text = 'SHUT';