import { describe, test, expect } from 'bun:test'; import { canonicalRpcBytes, canonicalJsonStringify, hashArgs, bytesToHex, bytesToBase64, base64ToBytes, } from '../../src/index.js'; describe('canonicalJsonStringify', () => { test('sorts object keys', () => { expect(canonicalJsonStringify({ b: 1, a: 2 })).toBe('{"a":2,"b":1}'); }); test('skips undefined values', () => { expect(canonicalJsonStringify({ a: 1, b: undefined, c: 2 })).toBe('{"a":1,"c":2}'); }); test('preserves array order', () => { expect(canonicalJsonStringify([3, 1, 2])).toBe('[3,1,2]'); }); test('recursive sorting', () => { const a = canonicalJsonStringify({ outer: { y: 1, x: 2 } }); const b = canonicalJsonStringify({ outer: { x: 2, y: 1 } }); expect(a).toBe(b); expect(a).toBe('{"outer":{"x":2,"y":1}}'); }); test('handles primitives', () => { expect(canonicalJsonStringify(null)).toBe('null'); expect(canonicalJsonStringify(true)).toBe('true'); expect(canonicalJsonStringify(42)).toBe('42'); expect(canonicalJsonStringify('hi')).toBe('"hi"'); }); test('different argument orders hash identically', () => { const h1 = hashArgs({ b: 1, a: 2 }); const h2 = hashArgs({ a: 2, b: 1 }); expect(bytesToHex(h1)).toBe(bytesToHex(h2)); }); test('different values hash differently', () => { expect(bytesToHex(hashArgs({ a: 1 }))).not.toBe(bytesToHex(hashArgs({ a: 2 }))); }); }); describe('canonicalRpcBytes', () => { test('deterministic for same input (known vector)', () => { const args = { address: 'alice', signedAt: 1730000000000, kind: 'shade.fs.list/v1', id: 'AbCdEfGhIjKlMnOpQrStUv', argsHash: new Uint8Array(32).fill(0xab), }; const a = canonicalRpcBytes(args); const b = canonicalRpcBytes(args); expect(a).toEqual(b); }); test('changes when address differs', () => { const base = { signedAt: 1, kind: 'shade.fs.list/v1', id: 'AbCdEfGhIjKlMnOpQrStUv', argsHash: new Uint8Array(32), }; const a = canonicalRpcBytes({ ...base, address: 'alice' }); const b = canonicalRpcBytes({ ...base, address: 'bob' }); expect(a).not.toEqual(b); }); test('changes when signedAt differs', () => { const base = { address: 'alice', kind: 'shade.fs.list/v1', id: 'AbCdEfGhIjKlMnOpQrStUv', argsHash: new Uint8Array(32), }; expect(canonicalRpcBytes({ ...base, signedAt: 1 })).not.toEqual( canonicalRpcBytes({ ...base, signedAt: 2 }), ); }); test('changes when kind differs', () => { const base = { address: 'alice', signedAt: 1, id: 'AbCdEfGhIjKlMnOpQrStUv', argsHash: new Uint8Array(32), }; expect(canonicalRpcBytes({ ...base, kind: 'shade.fs.list/v1' })).not.toEqual( canonicalRpcBytes({ ...base, kind: 'shade.fs.stat/v1' }), ); }); test('changes when argsHash differs', () => { const base = { address: 'alice', signedAt: 1, kind: 'shade.fs.list/v1', id: 'AbCdEfGhIjKlMnOpQrStUv', }; expect( canonicalRpcBytes({ ...base, argsHash: new Uint8Array(32) }), ).not.toEqual( canonicalRpcBytes({ ...base, argsHash: new Uint8Array(32).fill(1) }), ); }); }); describe('hashArgs', () => { test('produces 32-byte digest', () => { expect(hashArgs({ a: 1 }).length).toBe(32); }); test('null input is fine', () => { expect(hashArgs(null).length).toBe(32); }); }); describe('bytesToHex / base64', () => { test('hex roundtrip', () => { const b = new Uint8Array([0x01, 0xab, 0xff, 0x00]); expect(bytesToHex(b)).toBe('01abff00'); }); test('base64 roundtrip', () => { const b = new Uint8Array([1, 2, 3, 4]); const enc = bytesToBase64(b); expect(base64ToBytes(enc)).toEqual(b); }); });