import test from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { gzipSync, gunzipSync } from 'node:zlib'; import { openGameDb, readGameDbText, prepareGameDbSource, sha256 } from './game-db-source.mjs'; test('snapshot reader rejects tampering, path traversal and missing tables', () => { const root = mkdtempSync(path.join(tmpdir(), 'holocalc-db-test-')); try { const version = 'a'.repeat(64), snapshot = `snapshots/${version}`; const dir = path.join(root, snapshot); mkdirSync(dir, { recursive: true }); const raw = Buffer.from('[{"data":{"id":"test"}}]'), packed = gzipSync(raw); const manifest = JSON.stringify({ schemaVersion: 1, kind: 'android-master-db', locale: 'kor', masterDataVersion: version, tables: { Card: { file: 'Card.json.gz', rows: 1, sha256: sha256(raw), gzipSha256: sha256(packed) } } }); writeFileSync(path.join(dir, 'manifest.json'), manifest); writeFileSync(path.join(dir, 'Card.json.gz'), packed); const pointer = { snapshot, manifestSha256: sha256(manifest) }; const source = openGameDb(pointer, root); assert.equal(JSON.parse(readGameDbText(source, 'Card.json'))[0].data.id, 'test'); assert.throws(() => readGameDbText(source, 'Missing.json'), /Missing game DB table/); assert.throws(() => openGameDb({ ...pointer, snapshot: '../outside' }, root), /Invalid/); assert.throws(() => openGameDb({ ...pointer, manifestSha256: 'bad' }, root), /hash mismatch/); writeFileSync(path.join(dir, 'Card.json.gz'), gzipSync('[]')); assert.throws(() => readGameDbText(source, 'Card.json'), /hash mismatch/); } finally { rmSync(root, { recursive: true, force: true }); } }); test('every captured table verifies, and unsupported locales cannot use external fallback', () => { const source = prepareGameDbSource('kor', { review: true }); assert.ok(Object.keys(source.tables).length > 0); let rows = 0; for (const name of Object.keys(source.tables)) rows += JSON.parse(readGameDbText(source, name + '.json')).length; assert.ok(rows > 0); assert.throws(() => prepareGameDbSource('eng'), /no external fallback/); const cards = JSON.parse(readFileSync(new URL('../src/lib/data/cards-summary.json', import.meta.url))); assert.equal(cards.masterDataVersion, source.masterDataVersion); assert.equal(cards.cards.length, JSON.parse(readGameDbText(source, 'Card.json')).length); }); // Independent game assets pin the two corrections to the old community reference table. test('m0337 game SUS supplies the corrected denominator and matches service provenance', async () => { const { susScoreWeight } = await import('./sus-score-weight.mjs'); for (const [difficulty, expected] of [['hard', 539500], ['expert', 884700]]) { const raw = gunzipSync(readFileSync(new URL('./fixtures/game-sus/m0337.' + difficulty + '.sus.gz', import.meta.url))); const chart = JSON.parse(readFileSync(new URL('../static/data/charts/m0337.' + difficulty + '.json', import.meta.url))); assert.equal(susScoreWeight(raw.toString('utf8')).perfectNoteWeight, expected); assert.equal(chart.source.susSha256, sha256(raw)); assert.equal(chart.scoreFormula.perfectNoteWeight, expected); } });