Files
eden/tests/reference-production.test.ts
T
2026-09-18 23:15:05 -04:00

113 lines
12 KiB
TypeScript

import { describe, expect, test } from 'bun:test';
import { mkdtemp, cp, readFile, rm, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { ReferenceCompiler, readReferenceManifest } from '../src/lib/server/reference';
import { ccpUniverse, collectTarMembers, parseJson, productionSources, safeMemberName } from '../src/lib/server/reference/production';
import { fixtureSources } from '../src/lib/server/reference/sources';
import { normalizeReference } from '../src/lib/server/reference/normalize';
import type { ReferenceSourceAdapter } from '../src/lib/server/reference/types';
function tarHeader(name: string, size: number, type = '0'): Uint8Array {
const header = new Uint8Array(512); const encoder = new TextEncoder(); header.set(encoder.encode(name), 0); header.set(encoder.encode(size.toString(8).padStart(11, '0')), 124); header[156] = type.charCodeAt(0); header.set(encoder.encode('ustar\0'), 257); header.fill(0x20, 148, 156); const checksum = [...header].reduce((sum, byte) => sum + byte, 0); header.set(encoder.encode(checksum.toString(8).padStart(6, '0') + '\0 '), 148); return header;
}
async function* chunks(value: Uint8Array): AsyncIterable<Uint8Array> { yield value; }
function crc32(bytes: Uint8Array): number { let crc = 0xffffffff; for (const byte of bytes) { crc ^= byte; for (let bit = 0; bit < 8; bit++) crc = (crc & 1) ? 0xedb88320 ^ (crc >>> 1) : crc >>> 1; } return (crc ^ 0xffffffff) >>> 0; }
function le32(value: number): Uint8Array { return new Uint8Array([value & 255, (value >>> 8) & 255, (value >>> 16) & 255, (value >>> 24) & 255]); }
function le16(value: number): Uint8Array { return new Uint8Array([value & 255, (value >>> 8) & 255]); }
function zipFixture(options: { duplicateSystems?: boolean; duplicateSystemField?: boolean; badCrc?: boolean; badSize?: boolean } = {}): Uint8Array {
const encoder = new TextEncoder(); const rows = {
'mapRegions.jsonl': '{"_key":1,"name":{"en":"Region"}}\n',
'mapConstellations.jsonl': '{"_key":2,"name":{"en":"Constellation"},"regionID":1}\n',
'mapSolarSystems.jsonl': options.duplicateSystemField ? '{"_key":3,"_key":3,"name":{"en":"System"},"constellationID":2,"position":{"x":1,"y":2,"z":3},"securityStatus":0.5}\n' : '{"_key":3,"name":{"en":"System"},"constellationID":2,"position":{"x":1,"y":2,"z":3},"securityStatus":0.5}\n',
'mapStargates.jsonl': '{"_key":4,"solarSystemID":3,"destination":{"solarSystemID":3,"stargateID":4}}\n',
};
const records = Object.entries(rows).map(([name, value]) => ({ name, data: encoder.encode(options.duplicateSystems && name === 'mapSolarSystems.jsonl' ? value + value : value) }));
const locals: Uint8Array[] = []; const centrals: Uint8Array[] = []; let offset = 0;
for (const record of records) {
const name = encoder.encode(record.name); const checksum = crc32(record.data); const local = new Uint8Array(30 + name.length + record.data.length); local.set([0x50,0x4b,0x03,0x04], 0); local.set(le16(20), 4); local.set(le16(0), 6); local.set(le16(0), 8); local.set(le32(options.badCrc && record.name === 'mapRegions.jsonl' ? checksum ^ 1 : checksum), 14); local.set(le32(record.data.length), 18); local.set(le32(record.data.length), 22); local.set(le16(name.length), 26); local.set(name, 30); local.set(record.data, 30 + name.length); locals.push(local);
const central = new Uint8Array(46 + name.length); central.set([0x50,0x4b,0x01,0x02], 0); central.set(le16(20), 4); central.set(le16(20), 6); central.set(le16(0), 8); central.set(le16(0), 10); central.set(le32(options.badCrc && record.name === 'mapRegions.jsonl' ? checksum ^ 1 : checksum), 16); central.set(le32(record.data.length), 20); central.set(le32(options.badSize && record.name === 'mapRegions.jsonl' ? record.data.length - 1 : record.data.length), 24); central.set(le16(name.length), 28); central.set(le32(offset), 42); central.set(name, 46); centrals.push(central); offset += local.length;
}
const centralSize = centrals.reduce((sum, item) => sum + item.length, 0); const eocd = new Uint8Array(22); eocd.set([0x50,0x4b,0x05,0x06], 0); eocd.set(le16(records.length), 8); eocd.set(le16(records.length), 10); eocd.set(le32(centralSize), 12); eocd.set(le32(offset), 16);
const output = new Uint8Array(offset + centralSize + eocd.length); let cursor = 0; for (const item of locals) { output.set(item, cursor); cursor += item.length; } for (const item of centrals) { output.set(item, cursor); cursor += item.length; } output.set(eocd, cursor); return output;
}
describe('production archive boundaries', () => {
test('rejects traversal and symlink-like archive members', () => {
expect(() => safeMemberName('../escape.json', 'test')).toThrow();
expect(() => safeMemberName('/absolute.json', 'test')).toThrow();
expect(() => safeMemberName('dir\\..\\escape.json', 'test')).toThrow();
});
test('rejects duplicate and non-regular tar members', async () => {
const data = new TextEncoder().encode('{}'); const first = tarHeader('meta.json', data.byteLength); const duplicate = tarHeader('meta.json', data.byteLength); const symlink = tarHeader('link', 0, '2');
const archive = new Uint8Array(512 * 6); archive.set(first, 0); archive.set(data, 512); archive.set(duplicate, 1024); archive.set(data, 1536); archive.set(symlink, 2048);
await expect(collectTarMembers(chunks(archive), 'test', new Set(['meta.json']), new Set(['meta.json']))).rejects.toThrow(/duplicate|non-regular/);
});
test('rejects duplicate JSON object keys in maps and JSONL records', async () => {
expect(() => parseJson(new TextEncoder().encode('{"1":{"type_id":1},"1":{"type_id":2}}'), 'duplicate-root')).toThrow(/Duplicate key/);
const directory = await mkdtemp(join(tmpdir(), 'eden-reference-jsonl-'));
try { const path = join(directory, 'duplicate-field.zip'); await writeFile(path, zipFixture({ duplicateSystemField: true })); await expect(ccpUniverse(path)).rejects.toThrow(/Duplicate key/); } finally { await rm(directory, { recursive: true, force: true }); }
});
test('rejects malformed TAR checksums and one-block end markers', async () => {
const data = new TextEncoder().encode('{}'); const header = tarHeader('meta.json', data.byteLength); header[0] ^= 1; const archive = new Uint8Array(512 * 4); archive.set(header, 0); archive.set(data, 512);
await expect(collectTarMembers(chunks(archive), 'test', new Set(['meta.json']), new Set(['meta.json']))).rejects.toThrow(/checksum/);
const validHeader = tarHeader('meta.json', data.byteLength); const oneZero = new Uint8Array(512 * 3); oneZero.set(validHeader, 0); oneZero.set(data, 512);
await expect(collectTarMembers(chunks(oneZero), 'test', new Set(['meta.json']), new Set(['meta.json']))).rejects.toThrow(/two-block/);
});
test('rejects duplicate CCP keys and ZIP CRC/size mismatches', async () => {
const directory = await mkdtemp(join(tmpdir(), 'eden-reference-zip-'));
try {
const duplicatePath = join(directory, 'duplicate.zip'); await writeFile(duplicatePath, zipFixture({ duplicateSystems: true })); await expect(ccpUniverse(duplicatePath)).rejects.toThrow(/duplicate _key/);
const crcPath = join(directory, 'crc.zip'); await writeFile(crcPath, zipFixture({ badCrc: true })); await expect(ccpUniverse(crcPath)).rejects.toThrow(/CRC/);
const sizePath = join(directory, 'size.zip'); await writeFile(sizePath, zipFixture({ badSize: true })); await expect(ccpUniverse(sizePath)).rejects.toThrow(/decompressed size|decompression limit/);
} finally { await rm(directory, { recursive: true, force: true }); }
});
test('online archives replay offline through verified content-addressed receipts', async () => {
const directory = await mkdtemp(join(tmpdir(), 'eden-reference-receipt-')); const archive = zipFixture(); let responseArchive = new Uint8Array(archive); const etag = '"receipt-test"';
const request = (async (_input: RequestInfo | URL, init?: RequestInit) => { const headers: Record<string, string> = { etag }; if (init?.method === 'HEAD') headers['content-length'] = String(archive.byteLength); if (init?.method === 'HEAD') return new Response(null, { status: 200, headers }); return new Response(new Uint8Array(responseArchive), { status: 200, headers }); }) as typeof fetch;
try {
const online = productionSources({ archiveDir: directory, fetch: request })['esi-universe']; const checked = await online.check(); const loaded = await online.load(checked); expect(loaded.archiveByteLength).toBe(archive.byteLength);
const receipts = JSON.parse(await readFile(join(directory, '.production-archive-receipts.json'), 'utf8')); const receipt = receipts.sources['esi-universe']; expect(receipt.archivePath).toMatch(/^archives\/esi-universe\/[a-f0-9]{64}\.archive$/); expect(receipt.byteLength).toBe(archive.byteLength);
responseArchive[0] ^= 1; await expect(online.load(checked)).rejects.toThrow(/different archive content/); responseArchive = new Uint8Array(archive);
const offline = productionSources({ archiveDir: directory, offline: true, fetch: (async () => { throw new Error('offline replay attempted network'); }) as unknown as typeof fetch })['esi-universe']; const replayCheck = await offline.check(); expect(replayCheck.fingerprint).toBe(checked.fingerprint); await offline.load(replayCheck);
await writeFile(join(directory, receipt.archivePath), new Uint8Array([1])); await expect(offline.check()).rejects.toThrow(/SHA-256 mismatch|byte count mismatch/);
} finally { await rm(directory, { recursive: true, force: true }); }
});
test('offline mode fails closed instead of falling back to network', async () => {
const directory = await mkdtemp(join(tmpdir(), 'eden-reference-offline-')); let networkCalled = false;
try {
const sources = productionSources({ archiveDir: directory, offline: true, fetch: (async () => { networkCalled = true; throw new Error('network must not be called'); }) as unknown as typeof fetch });
await expect(sources['reference-data'].check()).rejects.toThrow(/offline archive not found/);
expect(networkCalled).toBe(false);
} finally { await rm(directory, { recursive: true, force: true }); }
});
test('normalization rejects malformed observed record shapes instead of dropping them', async () => {
const directory = await mkdtemp(join(tmpdir(), 'eden-reference-prod-shape-'));
try {
await cp('fixtures/reference', directory, { recursive: true });
const ref = JSON.parse(await Bun.file(join(directory, 'reference-data.json')).text());
ref.types['587'].dogma_attributes = { '4': 123 };
expect(() => normalizeReference(ref)).toThrow(/dogma_attributes/);
} finally { await rm(directory, { recursive: true, force: true }); }
});
test('production profile cannot reuse fixture caches after source failure', async () => {
const root = await mkdtemp(join(tmpdir(), 'eden-reference-prod-fallback-')); const fixtureDir = join(root, 'fixtures'); await cp('fixtures/reference', fixtureDir, { recursive: true }); const directory = join(root, 'reference');
try {
const fixture = await new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(fixtureDir) }).compile();
expect(fixture.status).toBe('success'); const fixtureVersion = fixture.version;
const failing = (source: ReferenceSourceAdapter): ReferenceSourceAdapter => ({ source: source.source, async check() { throw new Error(`${source.source} unavailable`); }, async load() { throw new Error('not reached'); } });
const production = await new ReferenceCompiler({ directory, profile: 'production', sources: { 'reference-data': failing({ source: 'reference-data', check: async () => ({ source: 'reference-data', checkedAt: '', fingerprint: 'x' }), load: async () => { throw new Error(); } }), hoboleaks: failing({ source: 'hoboleaks', check: async () => ({ source: 'hoboleaks', checkedAt: '', fingerprint: 'x' }), load: async () => { throw new Error(); } }), 'esi-universe': failing({ source: 'esi-universe', check: async () => ({ source: 'esi-universe', checkedAt: '', fingerprint: 'x' }), load: async () => { throw new Error(); } }) } }).compile();
expect(production.status).toBe('failed'); expect(production.activeVersion).toBe(fixtureVersion); expect(Object.values(production.sourceResults).every((result) => result?.reused !== true)).toBe(true);
expect((await readReferenceManifest(directory)).active?.profile).toBe('fixture');
} finally { await rm(root, { recursive: true, force: true }); }
});
});