136 lines
3.8 KiB
TypeScript
136 lines
3.8 KiB
TypeScript
|
|
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);
|
||
|
|
});
|
||
|
|
});
|