feat: verifyIntegrity 리포트 확장(summary) 및 테스트 추가\n\n- VerifyResult에 summary(total/missing/integrityMismatches/sizeMismatches/bundleHashMismatch) 추가\n- verify.integrity.test.ts에 summary 단언 추가\n- 전체 테스트 그린 유지
This commit is contained in:
@@ -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
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user