import { describe, test, expect } from 'bun:test'; import { StreamingSha256, sha256Once } from '../src/index.js'; function hex(bytes: Uint8Array): string { return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join(''); } async function subtleHashHex(data: Uint8Array): Promise { const buf = await globalThis.crypto.subtle.digest('SHA-256', data as unknown as ArrayBuffer); return hex(new Uint8Array(buf)); } describe('StreamingSha256', () => { test('digest of empty input matches the well-known SHA-256 zero hash', () => { const h = new StreamingSha256().digest(); expect(hex(h)).toBe('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'); }); test('matches one-shot sha256 over the same bytes', () => { const data = new TextEncoder().encode('the quick brown fox'); const streaming = new StreamingSha256().update(data).digest(); expect(hex(streaming)).toBe(hex(sha256Once(data))); }); test('matches SubtleCrypto digest over the same bytes', async () => { const data = new TextEncoder().encode('cross-impl parity check'); const streaming = new StreamingSha256().update(data).digest(); expect(hex(streaming)).toBe(await subtleHashHex(data)); }); test('chunked updates produce identical digest to a single update', () => { const buf = new Uint8Array(4096); for (let i = 0; i < buf.length; i++) buf[i] = i & 0xff; const a = new StreamingSha256().update(buf).digest(); const b = new StreamingSha256(); for (let off = 0; off < buf.length; off += 137) { b.update(buf.slice(off, Math.min(off + 137, buf.length))); } expect(hex(a)).toBe(hex(b.digest())); }); test('handles multi-megabyte inputs (memory-bounded streaming)', () => { const chunk = new Uint8Array(1024 * 1024); for (let i = 0; i < chunk.length; i++) chunk[i] = (i * 31) & 0xff; const h = new StreamingSha256(); for (let i = 0; i < 4; i++) h.update(chunk); const digest = h.digest(); expect(digest.length).toBe(32); }); test('throws on update after digest()', () => { const h = new StreamingSha256(); h.digest(); expect(() => h.update(new Uint8Array([1]))).toThrow(); }); test('isFinalized reflects digest()', () => { const h = new StreamingSha256(); expect(h.isFinalized).toBe(false); h.digest(); expect(h.isFinalized).toBe(true); }); test('skips no-op empty updates', () => { const h = new StreamingSha256(); h.update(new Uint8Array(0)); h.update(new Uint8Array(0)); expect(hex(h.digest())).toBe( 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', ); }); });