diff --git a/tests/bigint.test.ts b/tests/bigint.test.ts new file mode 100644 index 0000000..f9b30b9 --- /dev/null +++ b/tests/bigint.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest'; +import { toBigintOrThrow } from '../src/utils/bigint'; +import { SdkError } from '../src/errors'; + +const ctx = { code: 'CONFIG' as const, name: 'amount', detail: {} }; + +describe('toBigintOrThrow', () => { + it('passes through bigint values', () => { + expect(toBigintOrThrow(42n, ctx)).toBe(42n); + expect(toBigintOrThrow(0n, ctx)).toBe(0n); + }); + + it('converts numeric strings', () => { + expect(toBigintOrThrow('123', ctx)).toBe(123n); + expect(toBigintOrThrow('0', ctx)).toBe(0n); + }); + + it('converts finite numbers', () => { + expect(toBigintOrThrow(100, ctx)).toBe(100n); + expect(toBigintOrThrow(0, ctx)).toBe(0n); + }); + + it('throws SdkError for invalid input', () => { + expect(() => toBigintOrThrow('not-a-number', ctx)).toThrow(SdkError); + expect(() => toBigintOrThrow(null, ctx)).toThrow(SdkError); + expect(() => toBigintOrThrow(undefined, ctx)).toThrow(SdkError); + expect(() => toBigintOrThrow({}, ctx)).toThrow(SdkError); + }); + + it('includes field name in error message', () => { + try { + toBigintOrThrow('bad', ctx); + } catch (err) { + expect(err).toBeInstanceOf(SdkError); + expect((err as SdkError).message).toContain('amount'); + expect((err as SdkError).code).toBe('CONFIG'); + } + }); +}); diff --git a/tests/hex.test.ts b/tests/hex.test.ts new file mode 100644 index 0000000..2f07e93 --- /dev/null +++ b/tests/hex.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest'; +import { isHexStrict } from '../src/utils/hex'; + +describe('isHexStrict', () => { + it('accepts valid hex strings', () => { + expect(isHexStrict('0xab')).toBe(true); + expect(isHexStrict('0xABCDEF0123456789')).toBe(true); + expect(isHexStrict('0x00')).toBe(true); + }); + + it('rejects non-string input', () => { + expect(isHexStrict(42)).toBe(false); + expect(isHexStrict(null)).toBe(false); + expect(isHexStrict(undefined)).toBe(false); + expect(isHexStrict({})).toBe(false); + }); + + it('rejects strings without 0x prefix', () => { + expect(isHexStrict('abcd')).toBe(false); + }); + + it('rejects empty payload (bare 0x)', () => { + expect(isHexStrict('0x')).toBe(false); + }); + + it('rejects odd-length payload', () => { + expect(isHexStrict('0xabc')).toBe(false); + expect(isHexStrict('0x1')).toBe(false); + }); + + it('rejects non-hex characters', () => { + expect(isHexStrict('0xgh')).toBe(false); + expect(isHexStrict('0x00zz')).toBe(false); + }); + + it('enforces minBytes option', () => { + expect(isHexStrict('0xab', { minBytes: 1 })).toBe(true); + expect(isHexStrict('0xab', { minBytes: 2 })).toBe(false); + expect(isHexStrict('0xaabbccdd', { minBytes: 4 })).toBe(true); + expect(isHexStrict('0xaabb', { minBytes: 4 })).toBe(false); + }); +}); diff --git a/tests/json.test.ts b/tests/json.test.ts new file mode 100644 index 0000000..9aaf2f2 --- /dev/null +++ b/tests/json.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from 'vitest'; +import { bigintReplacer, serializeBigInt, stableStringify } from '../src/utils/json'; + +describe('bigintReplacer', () => { + it('converts bigint to string', () => { + expect(bigintReplacer('x', 42n)).toBe('42'); + }); + + it('passes non-bigint values through', () => { + expect(bigintReplacer('x', 'hello')).toBe('hello'); + expect(bigintReplacer('x', 99)).toBe(99); + expect(bigintReplacer('x', null)).toBe(null); + }); +}); + +describe('serializeBigInt', () => { + it('serializes objects with bigint fields', () => { + const result = serializeBigInt({ a: 1n, b: 'text' }); + expect(JSON.parse(result)).toEqual({ a: '1', b: 'text' }); + }); +}); + +describe('stableStringify', () => { + it('sorts object keys for deterministic output', () => { + const a = stableStringify({ z: 1, a: 2 }); + const b = stableStringify({ a: 2, z: 1 }); + expect(a).toBe(b); + expect(JSON.parse(a)).toEqual({ a: 2, z: 1 }); + }); + + it('handles nested objects with sorted keys', () => { + const result = stableStringify({ b: { d: 1, c: 2 }, a: 3 }); + const keys = Object.keys(JSON.parse(result)); + expect(keys).toEqual(['a', 'b']); + }); + + it('handles arrays without reordering', () => { + const result = stableStringify([3, 1, 2]); + expect(JSON.parse(result)).toEqual([3, 1, 2]); + }); + + it('converts bigint values', () => { + const result = stableStringify({ amount: 100n }); + expect(JSON.parse(result)).toEqual({ amount: '100' }); + }); + + it('strips undefined values', () => { + const result = stableStringify({ a: 1, b: undefined }); + expect(JSON.parse(result)).toEqual({ a: 1 }); + }); + + it('handles null and primitives', () => { + expect(stableStringify(null)).toBe('null'); + expect(stableStringify(42)).toBe('42'); + expect(stableStringify('hi')).toBe('"hi"'); + }); +}); diff --git a/tests/signal.test.ts b/tests/signal.test.ts new file mode 100644 index 0000000..509a87d --- /dev/null +++ b/tests/signal.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest'; +import { signalTimeout, signalAny } from '../src/utils/signal'; + +describe('signalTimeout', () => { + it('returns an AbortSignal', () => { + const signal = signalTimeout(5_000); + expect(signal).toBeInstanceOf(AbortSignal); + expect(signal.aborted).toBe(false); + }); + + it('aborts after the specified timeout', async () => { + const signal = signalTimeout(50); + expect(signal.aborted).toBe(false); + await new Promise((r) => setTimeout(r, 100)); + expect(signal.aborted).toBe(true); + }); +}); + +describe('signalAny', () => { + it('returns undefined for empty input', () => { + expect(signalAny([])).toBeUndefined(); + }); + + it('returns undefined when all entries are undefined', () => { + expect(signalAny([undefined, undefined])).toBeUndefined(); + }); + + it('aborts when any input signal aborts', () => { + const c1 = new AbortController(); + const c2 = new AbortController(); + const combined = signalAny([c1.signal, c2.signal]); + expect(combined).toBeDefined(); + expect(combined!.aborted).toBe(false); + + c1.abort(new Error('first')); + expect(combined!.aborted).toBe(true); + }); + + it('is already aborted if an input signal is pre-aborted', () => { + const c = new AbortController(); + c.abort(new Error('already')); + const combined = signalAny([c.signal]); + expect(combined!.aborted).toBe(true); + }); +});