feat: verifyIntegrity 리포트 확장(summary) 및 테스트 추가\n\n- VerifyResult에 summary(total/missing/integrityMismatches/sizeMismatches/bundleHashMismatch) 추가\n- verify.integrity.test.ts에 summary 단언 추가\n- 전체 테스트 그린 유지

This commit is contained in:
2025-11-16 18:33:16 +09:00
parent c519055e90
commit 19a39230be
2 changed files with 42 additions and 5 deletions
+10
View File
@@ -20,6 +20,13 @@ describe('verifyIntegrity', () => {
const res = verifyIntegrity(extras, assets)
expect(res.ok).toBe(true)
expect(res.errors.length).toBe(0)
// summary
const parsedIdx = JSON.parse(td(idx)) as { entries: Array<{ path: string }> }
expect(res.summary.total).toBe(parsedIdx.entries.length)
expect(res.summary.missing).toBe(0)
expect(res.summary.integrityMismatches).toBe(0)
expect(res.summary.sizeMismatches).toBe(0)
expect(res.summary.bundleHashMismatch).toBe(false)
// sanity: index contains both paths
const parsed = JSON.parse(td(idx)) as { entries: Array<{ path: string }> }
const paths = parsed.entries.map(e => e.path)
@@ -42,6 +49,7 @@ describe('verifyIntegrity', () => {
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)
expect(res.summary.integrityMismatches).toBeGreaterThanOrEqual(1)
})
it('detects missing entry or missing asset referenced by index', async () => {
@@ -58,6 +66,7 @@ describe('verifyIntegrity', () => {
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)
expect(res1.summary.bundleHashMismatch).toBe(true)
// reference a missing path in index
obj.entries.unshift({ path: 'assets/missing.bin', integrity: 'sha256-AAAA' })
@@ -65,6 +74,7 @@ describe('verifyIntegrity', () => {
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)
expect(res2.summary.missing).toBeGreaterThanOrEqual(1)
// keep lints happy
expect(removed).toBeTruthy()
})
+32 -5
View File
@@ -1,4 +1,14 @@
export type VerifyResult = { ok: boolean; errors: string[] }
export type VerifyResult = {
ok: boolean;
errors: string[];
summary: {
total: number;
missing: number;
integrityMismatches: number;
sizeMismatches: number;
bundleHashMismatch: boolean;
};
}
// Minimal synchronous SHA-256 implementation returning base64 string.
function sha256Base64(bytes: Uint8Array): string {
@@ -81,30 +91,36 @@ export function verifyIntegrity(
const errors: string[] = []
const key = 'assets.integrity.index.json'
if (!Object.prototype.hasOwnProperty.call(extras, key)) {
return { ok: false, errors: ['Missing assets.integrity.index.json in extras'] }
return { ok: false, errors: ['Missing assets.integrity.index.json in extras'], summary: { total: 0, missing: 0, integrityMismatches: 0, sizeMismatches: 0, bundleHashMismatch: false } }
}
let parsed: { entries: Array<{ path: string; integrity: string; size?: number }>; bundleHash: string }
try {
parsed = JSON.parse(new TextDecoder().decode(extras[key]))
} catch {
return { ok: false, errors: ['Invalid assets.integrity.index.json JSON'] }
return { ok: false, errors: ['Invalid assets.integrity.index.json JSON'], summary: { total: 0, missing: 0, integrityMismatches: 0, sizeMismatches: 0, bundleHashMismatch: false } }
}
// verify each entry exists and integrity matches
const recomputedLines: string[] = []
let missing = 0
let integrityMismatches = 0
let sizeMismatches = 0
for (const e of parsed.entries) {
const bytes = assets[e.path]
if (!bytes) {
errors.push(`Missing asset for path in integrity index: ${e.path}`)
missing++
continue
}
// optional size check when present in index
if (typeof e.size === 'number' && e.size !== bytes.length) {
errors.push(`Size mismatch for ${e.path}: expected ${e.size}, got ${bytes.length}`)
sizeMismatches++
}
const b64 = sha256Base64(bytes)
const integ = `sha256-${b64}`
if (integ !== e.integrity) {
errors.push(`Integrity mismatch for ${e.path}`)
integrityMismatches++
}
recomputedLines.push(e.path)
recomputedLines.push(integ)
@@ -112,8 +128,19 @@ export function verifyIntegrity(
// recompute bundle hash
const joined = new TextEncoder().encode(recomputedLines.join('\n'))
const bundle = `sha256-${sha256Base64(joined)}`
if (bundle !== parsed.bundleHash) {
const bundleHashMismatch = bundle !== parsed.bundleHash
if (bundleHashMismatch) {
errors.push('BundleHash mismatch')
}
return { ok: errors.length === 0, errors }
return {
ok: errors.length === 0,
errors,
summary: {
total: parsed.entries.length,
missing,
integrityMismatches,
sizeMismatches,
bundleHashMismatch,
},
}
}