import { describe, it, expect } from 'vitest' import { AssetManager } from '@/lib/assets/assetManager' import { buildAssetsIntegrityIndex } from '@/lib/exporter/assets.integrity.index' import { verifyIntegrity } from '@/lib/exporter/verify' function u8(n: number): Uint8Array { const a = new Uint8Array(n); for (let i=0;i { it('returns ok=true for valid assets and integrity index', async () => { const am = new AssetManager({ maxBytes: 1024*1024, allowedExts: ['png','txt'] }) const a = await am.add({ name: 'a.png', type: 'image/png', bytes: u8(5) }) const b = await am.add({ name: 'b.txt', type: 'text/plain', bytes: u8(7) }) const assets = am.toZipStructure() const idx = buildAssetsIntegrityIndex(am) const extras: Record = { 'assets.integrity.index.json': idx } const res = verifyIntegrity(extras, assets) expect(res.ok).toBe(true) expect(res.errors.length).toBe(0) // sanity: index contains both paths const parsed = JSON.parse(td(idx)) as { entries: Array<{ path: string }> } const paths = parsed.entries.map(e => e.path) expect(paths).toContain(a.path) expect(paths).toContain(b.path) }) it('detects byte tampering in an asset', async () => { const am = new AssetManager({ maxBytes: 1024*1024, allowedExts: ['png','txt'] }) await am.add({ name: 'a.png', type: 'image/png', bytes: u8(5) }) await am.add({ name: 'b.txt', type: 'text/plain', bytes: u8(7) }) const assets = am.toZipStructure() const idx = buildAssetsIntegrityIndex(am) // tamper one asset byte const somePath = Object.keys(assets)[0] const tampered = assets[somePath].slice() tampered[0] = (tampered[0] ^ 0xff) & 0xff assets[somePath] = tampered const res = verifyIntegrity({ 'assets.integrity.index.json': idx }, assets) expect(res.ok).toBe(false) expect(res.errors.some((e: string) => e.includes(somePath) && e.toLowerCase().includes('mismatch'))).toBe(true) }) it('detects missing entry or missing asset referenced by index', async () => { const am = new AssetManager({ maxBytes: 1024*1024, allowedExts: ['png','txt'] }) await am.add({ name: 'a.png', type: 'image/png', bytes: u8(5) }) await am.add({ name: 'b.txt', type: 'text/plain', bytes: u8(7) }) const assets = am.toZipStructure() const idxRaw = buildAssetsIntegrityIndex(am) const obj = JSON.parse(td(idxRaw)) as { entries: Array<{ path: string; integrity: string }>, bundleHash: string } // remove one entry const removed = obj.entries.shift()! const idx2 = te(JSON.stringify(obj)) const res1 = verifyIntegrity({ 'assets.integrity.index.json': idx2 }, assets) expect(res1.ok).toBe(false) expect(res1.errors.some((e: string) => e.toLowerCase().includes('bundlehash'))).toBe(true) // reference a missing path in index obj.entries.unshift({ path: 'assets/missing.bin', integrity: 'sha256-AAAA' }) const idx3 = te(JSON.stringify(obj)) const res2 = verifyIntegrity({ 'assets.integrity.index.json': idx3 }, assets) expect(res2.ok).toBe(false) expect(res2.errors.some((e: string) => e.includes('assets/missing.bin') && e.toLowerCase().includes('missing'))).toBe(true) // keep lints happy expect(removed).toBeTruthy() }) })