strictencode
Version:
Deterministic binary encoding for RGB protocol compliance - JavaScript implementation of StrictEncode
369 lines (309 loc) • 13.8 kB
JavaScript
/**
* @fileoverview Tests for StrictEncoder class
*/
import { StrictEncoder } from '../index.js';
describe('StrictEncoder', () => {
let encoder;
beforeEach(() => {
encoder = new StrictEncoder();
});
describe('constructor', () => {
test('creates empty encoder', () => {
expect(encoder.length()).toBe(0);
expect(encoder.toHex()).toBe('');
expect(encoder.toBytes()).toEqual(new Uint8Array(0));
});
});
describe('encodeU8', () => {
test('encodes valid u8 values', () => {
expect(encoder.encodeU8(0).toHex()).toBe('00');
encoder.reset();
expect(encoder.encodeU8(255).toHex()).toBe('ff');
encoder.reset();
expect(encoder.encodeU8(123).toHex()).toBe('7b');
});
test('throws on invalid values', () => {
expect(() => encoder.encodeU8(-1)).toThrow('Invalid u8 value');
expect(() => encoder.encodeU8(256)).toThrow('Invalid u8 value');
expect(() => encoder.encodeU8(1.5)).toThrow('Invalid u8 value');
expect(() => encoder.encodeU8('123')).toThrow('Invalid u8 value');
});
test('chains correctly', () => {
const result = encoder.encodeU8(1).encodeU8(2).encodeU8(3);
expect(result).toBe(encoder);
expect(encoder.toHex()).toBe('010203');
});
});
describe('encodeU16', () => {
test('encodes in little-endian format', () => {
expect(encoder.encodeU16(0).toHex()).toBe('0000');
encoder.reset();
expect(encoder.encodeU16(65535).toHex()).toBe('ffff');
encoder.reset();
expect(encoder.encodeU16(256).toHex()).toBe('0001'); // 0x0100 in little-endian
encoder.reset();
expect(encoder.encodeU16(1000).toHex()).toBe('e803'); // 0x03e8 in little-endian
});
test('throws on invalid values', () => {
expect(() => encoder.encodeU16(-1)).toThrow('Invalid u16 value');
expect(() => encoder.encodeU16(65536)).toThrow('Invalid u16 value');
expect(() => encoder.encodeU16(1.5)).toThrow('Invalid u16 value');
});
});
describe('encodeU32', () => {
test('encodes in little-endian format', () => {
expect(encoder.encodeU32(0).toHex()).toBe('00000000');
encoder.reset();
expect(encoder.encodeU32(4294967295).toHex()).toBe('ffffffff');
encoder.reset();
expect(encoder.encodeU32(1000000).toHex()).toBe('40420f00'); // Test vector from spec
});
test('throws on invalid values', () => {
expect(() => encoder.encodeU32(-1)).toThrow('Invalid u32 value');
expect(() => encoder.encodeU32(4294967296)).toThrow('Invalid u32 value');
});
});
describe('encodeU64', () => {
test('encodes in little-endian format', () => {
expect(encoder.encodeU64(0n).toHex()).toBe('0000000000000000');
encoder.reset();
expect(encoder.encodeU64(1000000).toHex()).toBe('40420f0000000000'); // Test vector from spec
encoder.reset();
expect(encoder.encodeU64('1000000').toHex()).toBe('40420f0000000000');
encoder.reset();
expect(encoder.encodeU64(1000000n).toHex()).toBe('40420f0000000000');
});
test('handles large numbers', () => {
const maxU64 = 18446744073709551615n;
expect(encoder.encodeU64(maxU64).toHex()).toBe('ffffffffffffffff');
});
test('throws on invalid values', () => {
expect(() => encoder.encodeU64(-1)).toThrow('Invalid u64 value');
expect(() => encoder.encodeU64('18446744073709551616')).toThrow('Invalid u64 value');
});
});
describe('encodeBool', () => {
test('encodes boolean values correctly', () => {
expect(encoder.encodeBool(true).toHex()).toBe('01');
encoder.reset();
expect(encoder.encodeBool(false).toHex()).toBe('00');
});
test('handles truthy/falsy values', () => {
encoder.encodeBool(1);
encoder.encodeBool(0);
encoder.encodeBool('true');
encoder.encodeBool('');
encoder.encodeBool({});
encoder.encodeBool(null);
expect(encoder.toHex()).toBe('010001000100');
});
});
describe('encodeLeb128', () => {
test('encodes small values in single byte', () => {
expect(encoder.encodeLeb128(0).toHex()).toBe('00');
encoder.reset();
expect(encoder.encodeLeb128(7).toHex()).toBe('07');
encoder.reset();
expect(encoder.encodeLeb128(127).toHex()).toBe('7f');
});
test('encodes multi-byte values', () => {
expect(encoder.encodeLeb128(128).toHex()).toBe('8001');
encoder.reset();
expect(encoder.encodeLeb128(200).toHex()).toBe('c801'); // Test vector from spec
encoder.reset();
expect(encoder.encodeLeb128(300).toHex()).toBe('ac02');
});
test('encodes large values', () => {
expect(encoder.encodeLeb128(16384).toHex()).toBe('808001');
});
test('throws on invalid values', () => {
expect(() => encoder.encodeLeb128(-1)).toThrow('Invalid LEB128 value');
expect(() => encoder.encodeLeb128(1.5)).toThrow('Invalid LEB128 value');
});
});
describe('encodeString', () => {
test('encodes ASCII strings', () => {
expect(encoder.encodeString('').toHex()).toBe('00'); // Empty string
encoder.reset();
expect(encoder.encodeString('RGB').toHex()).toBe('03524742'); // Test vector from spec
encoder.reset();
expect(encoder.encodeString('NIATCKR').toHex()).toBe('074e494154434b52'); // Test vector from spec
});
test('encodes UTF-8 strings', () => {
encoder.encodeString('Hello 🌈');
const hex = encoder.toHex();
expect(hex.startsWith('0a')).toBe(true); // Length 10 bytes for "Hello 🌈"
});
test('handles long strings with multi-byte LEB128', () => {
const longString = 'A'.repeat(200);
encoder.encodeString(longString);
const hex = encoder.toHex();
expect(hex.startsWith('c801')).toBe(true); // LEB128(200) = c801
expect(hex.slice(4)).toBe('41'.repeat(200)); // 200 'A' characters
});
test('throws on non-string input', () => {
expect(() => encoder.encodeString(123)).toThrow('Invalid string');
expect(() => encoder.encodeString(null)).toThrow('Invalid string');
});
});
describe('encodeOption', () => {
test('encodes None values', () => {
encoder.encodeOption(null, () => {});
expect(encoder.toHex()).toBe('00');
encoder.reset();
encoder.encodeOption(undefined, () => {});
expect(encoder.toHex()).toBe('00');
});
test('encodes Some values', () => {
encoder.encodeOption('test', function(value) {
this.encodeString(value);
});
expect(encoder.toHex()).toBe('010474657374'); // 01 + encodeString("test")
});
test('throws when encoderFunc is missing for Some', () => {
expect(() => encoder.encodeOption('test')).toThrow('encoderFunc must be a function');
});
});
describe('encodeVec', () => {
test('encodes empty array', () => {
encoder.encodeVec([], () => {});
expect(encoder.toHex()).toBe('00'); // Length 0
});
test('encodes string array', () => {
encoder.encodeVec(['RGB', '20'], function(item) {
this.encodeString(item);
});
expect(encoder.toHex()).toBe('0203524742023230'); // 02 + "RGB" + "20"
});
test('encodes number array', () => {
encoder.encodeVec([1, 2, 3], function(item) {
this.encodeU8(item);
});
expect(encoder.toHex()).toBe('03010203'); // 03 + 01 + 02 + 03
});
test('throws on non-array input', () => {
expect(() => encoder.encodeVec('not array', () => {})).toThrow('Invalid Vec');
});
test('throws when encoderFunc is missing', () => {
expect(() => encoder.encodeVec([1, 2])).toThrow('encoderFunc must be a function');
});
});
describe('encodeHashMap', () => {
test('encodes empty map', () => {
encoder.encodeHashMap({}, () => {});
expect(encoder.toHex()).toBe('00'); // Empty Vec
});
test('sorts by key and encodes values', () => {
const map = {
2001: 'terms',
2000: 'spec',
2002: 'amount'
};
encoder.encodeHashMap(map, function(value) {
this.encodeString(value);
});
const hex = encoder.toHex();
expect(hex.startsWith('03')).toBe(true); // 3 items
// Values should be in key order: spec(2000), terms(2001), amount(2002)
expect(hex.includes('7370656')).toBe(true); // "spec" hex
expect(hex.includes('7465726d73')).toBe(true); // "terms" hex
expect(hex.includes('616d6f756e74')).toBe(true); // "amount" hex
});
test('handles Map objects', () => {
const map = new Map();
map.set(100, 'first');
map.set(50, 'second');
encoder.encodeHashMap(map, function(value) {
this.encodeString(value);
});
const hex = encoder.toHex();
expect(hex.startsWith('02')).toBe(true); // 2 items
});
test('throws on invalid keys', () => {
expect(() => encoder.encodeHashMap({ 'invalid': 'value' }, () => {})).toThrow('Invalid HashMap key');
expect(() => encoder.encodeHashMap({ '-1': 'value' }, () => {})).toThrow('Invalid HashMap key');
});
test('throws on invalid input', () => {
expect(() => encoder.encodeHashMap('not object', () => {})).toThrow('Invalid HashMap');
expect(() => encoder.encodeHashMap(null, () => {})).toThrow('Invalid HashMap');
});
});
describe('utility methods', () => {
test('toBytes returns copy', () => {
encoder.encodeU8(123);
const bytes1 = encoder.toBytes();
const bytes2 = encoder.toBytes();
expect(bytes1).toEqual(bytes2);
expect(bytes1).not.toBe(bytes2); // Different objects
bytes1[0] = 0; // Modify copy
expect(encoder.toBytes()[0]).toBe(123); // Original unchanged
});
test('reset clears buffer', () => {
encoder.encodeU8(123).encodeString('test');
expect(encoder.length()).toBeGreaterThan(0);
encoder.reset();
expect(encoder.length()).toBe(0);
expect(encoder.toHex()).toBe('');
});
test('clone creates independent copy', () => {
encoder.encodeU8(123);
const clone = encoder.clone();
expect(clone.toHex()).toBe(encoder.toHex());
expect(clone).not.toBe(encoder);
clone.encodeU8(200);
expect(clone.length()).not.toBe(encoder.length());
});
test('length returns correct byte count', () => {
expect(encoder.length()).toBe(0);
encoder.encodeU8(123);
expect(encoder.length()).toBe(1);
encoder.encodeU32(456);
expect(encoder.length()).toBe(5);
encoder.encodeString('test');
expect(encoder.length()).toBe(10); // 5 + 1(len) + 4(chars)
});
});
describe('chaining', () => {
test('all methods support chaining', () => {
const result = encoder
.encodeU8(1)
.encodeU16(2)
.encodeU32(3)
.encodeU64(4n)
.encodeBool(true)
.encodeLeb128(5)
.encodeString('test')
.encodeOption(null, () => {})
.encodeVec([], () => {})
.encodeHashMap({}, () => {})
.reset();
expect(result).toBe(encoder);
});
});
describe('edge cases', () => {
test('handles maximum values', () => {
expect(() => {
encoder.encodeU8(255)
.encodeU16(65535)
.encodeU32(4294967295)
.encodeU64(18446744073709551615n);
}).not.toThrow();
});
test('handles empty inputs', () => {
expect(() => {
encoder.encodeString('')
.encodeVec([], () => {})
.encodeHashMap({}, () => {})
.encodeOption(null, () => {});
}).not.toThrow();
});
test('preserves data integrity', () => {
// Encode known test vector from spec
encoder.encodeString('NIATCKR')
.encodeString('NIA asset name')
.encodeU8(8)
.encodeOption(null, () => {});
expect(encoder.toHex()).toBe('074e494154434b520e4e4941206173736574206e616d650800');
});
});
});