@covenant-rpc/ion 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.test.ts ADDED
@@ -0,0 +1,386 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import ION from './index.ts';
3
+
4
+ describe('ION', () => {
5
+ describe('Round-trip tests (stringify → parse)', () => {
6
+ test('primitives: strings', () => {
7
+ const values = ['hello', '', 'with spaces', 'with\nnewlines', 'with\ttabs'];
8
+ for (const val of values) {
9
+ expect(ION.parse(ION.stringify(val))).toBe(val);
10
+ }
11
+ });
12
+
13
+ test('primitives: numbers', () => {
14
+ const values = [0, 1, -1, 123.456, -123.456, 1e10, 1e-10];
15
+ for (const val of values) {
16
+ expect(ION.parse(ION.stringify(val))).toBe(val);
17
+ }
18
+ });
19
+
20
+ test('primitives: booleans', () => {
21
+ expect(ION.parse(ION.stringify(true))).toBe(true);
22
+ expect(ION.parse(ION.stringify(false))).toBe(false);
23
+ });
24
+
25
+ test('primitives: null', () => {
26
+ expect(ION.parse(ION.stringify(null))).toBe(null);
27
+ });
28
+
29
+ test('special numbers: NaN', () => {
30
+ expect(Number.isNaN(ION.parse(ION.stringify(NaN)))).toBe(true);
31
+ });
32
+
33
+ test('special numbers: Infinity', () => {
34
+ expect(ION.parse(ION.stringify(Infinity))).toBe(Infinity);
35
+ });
36
+
37
+ test('special numbers: -Infinity', () => {
38
+ expect(ION.parse(ION.stringify(-Infinity))).toBe(-Infinity);
39
+ });
40
+
41
+ test('dates: various ISO 8601 formats', () => {
42
+ const dates = [
43
+ new Date('2026-01-27T15:30:00Z'),
44
+ new Date('2020-01-01T00:00:00Z'),
45
+ new Date('1999-12-31T23:59:59.999Z'),
46
+ ];
47
+
48
+ for (const date of dates) {
49
+ const result = ION.parse(ION.stringify(date));
50
+ expect(result).toBeInstanceOf(Date);
51
+ expect((result as Date).getTime()).toBe(date.getTime());
52
+ }
53
+ });
54
+
55
+ test('collections: Map with string keys', () => {
56
+ const map = new Map([
57
+ ['key1', 'value1'],
58
+ ['key2', 'value2'],
59
+ ]);
60
+
61
+ const result = ION.parse(ION.stringify(map));
62
+ expect(result).toBeInstanceOf(Map);
63
+ expect((result as Map<unknown, unknown>).get('key1')).toBe('value1');
64
+ expect((result as Map<unknown, unknown>).get('key2')).toBe('value2');
65
+ });
66
+
67
+ test('collections: Map with number keys', () => {
68
+ const map = new Map([
69
+ [1, 'one'],
70
+ [2, 'two'],
71
+ ]);
72
+
73
+ const result = ION.parse(ION.stringify(map));
74
+ expect(result).toBeInstanceOf(Map);
75
+ expect((result as Map<unknown, unknown>).get(1)).toBe('one');
76
+ expect((result as Map<unknown, unknown>).get(2)).toBe('two');
77
+ });
78
+
79
+ test('collections: Set with various values', () => {
80
+ const set = new Set(['a', 'b', 'c', 1, 2, 3]);
81
+
82
+ const result = ION.parse(ION.stringify(set));
83
+ expect(result).toBeInstanceOf(Set);
84
+ expect((result as Set<unknown>).has('a')).toBe(true);
85
+ expect((result as Set<unknown>).has('b')).toBe(true);
86
+ expect((result as Set<unknown>).has('c')).toBe(true);
87
+ expect((result as Set<unknown>).has(1)).toBe(true);
88
+ expect((result as Set<unknown>).has(2)).toBe(true);
89
+ expect((result as Set<unknown>).has(3)).toBe(true);
90
+ });
91
+
92
+ test('collections: empty Map', () => {
93
+ const map = new Map();
94
+ const result = ION.parse(ION.stringify(map));
95
+ expect(result).toBeInstanceOf(Map);
96
+ expect((result as Map<unknown, unknown>).size).toBe(0);
97
+ });
98
+
99
+ test('collections: empty Set', () => {
100
+ const set = new Set();
101
+ const result = ION.parse(ION.stringify(set));
102
+ expect(result).toBeInstanceOf(Set);
103
+ expect((result as Set<unknown>).size).toBe(0);
104
+ });
105
+
106
+ test('nested structures: object in array', () => {
107
+ const obj = [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }];
108
+ const result = ION.parse(ION.stringify(obj));
109
+ expect(result).toEqual(obj);
110
+ });
111
+
112
+ test('nested structures: map in object', () => {
113
+ const obj = {
114
+ data: new Map([['key', 'value']]),
115
+ count: 5,
116
+ };
117
+
118
+ const result = ION.parse(ION.stringify(obj)) as typeof obj;
119
+ expect(result.count).toBe(5);
120
+ expect(result.data).toBeInstanceOf(Map);
121
+ expect(result.data.get('key')).toBe('value');
122
+ });
123
+
124
+ test('nested structures: set in array', () => {
125
+ const arr = [1, 2, new Set([3, 4, 5])];
126
+ const result = ION.parse(ION.stringify(arr)) as unknown[];
127
+ expect(result[0]).toBe(1);
128
+ expect(result[1]).toBe(2);
129
+ expect(result[2]).toBeInstanceOf(Set);
130
+ expect((result[2] as Set<number>).has(3)).toBe(true);
131
+ expect((result[2] as Set<number>).has(4)).toBe(true);
132
+ expect((result[2] as Set<number>).has(5)).toBe(true);
133
+ });
134
+
135
+ test('nested structures: complex nesting', () => {
136
+ const obj = {
137
+ users: [
138
+ {
139
+ name: 'Alice',
140
+ metadata: new Map<string, Date | Set<string>>([
141
+ ['lastLogin', new Date('2026-01-27T15:30:00Z')],
142
+ ['roles', new Set(['admin', 'user'])],
143
+ ]),
144
+ },
145
+ ],
146
+ stats: {
147
+ count: 42,
148
+ ratio: NaN,
149
+ },
150
+ };
151
+
152
+ const result = ION.parse(ION.stringify(obj)) as typeof obj;
153
+ expect(result.users[0]?.name).toBe('Alice');
154
+ expect(result.users[0]?.metadata).toBeInstanceOf(Map);
155
+ expect(result.users[0]?.metadata.get('lastLogin')).toBeInstanceOf(Date);
156
+ expect(result.users[0]?.metadata.get('roles')).toBeInstanceOf(Set);
157
+ expect(result.stats.count).toBe(42);
158
+ expect(Number.isNaN(result.stats.ratio)).toBe(true);
159
+ });
160
+
161
+ test('edge cases: empty object', () => {
162
+ const obj = {};
163
+ expect(ION.parse(ION.stringify(obj))).toEqual(obj);
164
+ });
165
+
166
+ test('edge cases: empty array', () => {
167
+ const arr: unknown[] = [];
168
+ expect(ION.parse(ION.stringify(arr))).toEqual(arr);
169
+ });
170
+ });
171
+
172
+ describe('JSON compatibility', () => {
173
+ test('parses valid JSON objects', () => {
174
+ const json = '{"name": "Alice", "age": 30}';
175
+ const result = ION.parse(json);
176
+ expect(result).toEqual({ name: 'Alice', age: 30 });
177
+ });
178
+
179
+ test('parses valid JSON arrays', () => {
180
+ const json = '[1, 2, 3, "four", true, null]';
181
+ const result = ION.parse(json);
182
+ expect(result).toEqual([1, 2, 3, 'four', true, null]);
183
+ });
184
+
185
+ test('parses JSON primitives', () => {
186
+ expect(ION.parse('"hello"')).toBe('hello');
187
+ expect(ION.parse('123')).toBe(123);
188
+ expect(ION.parse('true')).toBe(true);
189
+ expect(ION.parse('false')).toBe(false);
190
+ expect(ION.parse('null')).toBe(null);
191
+ });
192
+
193
+ test('parses nested JSON', () => {
194
+ const json = '{"users": [{"name": "Alice"}, {"name": "Bob"}], "count": 2}';
195
+ const result = ION.parse(json);
196
+ expect(result).toEqual({
197
+ users: [{ name: 'Alice' }, { name: 'Bob' }],
198
+ count: 2,
199
+ });
200
+ });
201
+ });
202
+
203
+ describe('Error handling: unsupported types', () => {
204
+ test('throws on WeakMap', () => {
205
+ const weakMap = new WeakMap();
206
+ expect(() => ION.stringify(weakMap)).toThrow('Cannot serialize WeakMap');
207
+ });
208
+
209
+ test('throws on WeakSet', () => {
210
+ const weakSet = new WeakSet();
211
+ expect(() => ION.stringify(weakSet)).toThrow('Cannot serialize WeakSet');
212
+ });
213
+
214
+ test('throws on function', () => {
215
+ const fn = () => {};
216
+ expect(() => ION.stringify(fn)).toThrow('Cannot serialize function');
217
+ });
218
+
219
+ test('throws on symbol', () => {
220
+ const sym = Symbol('test');
221
+ expect(() => ION.stringify(sym)).toThrow('Cannot serialize symbol');
222
+ });
223
+
224
+ test('throws on BigInt', () => {
225
+ const big = BigInt(123);
226
+ expect(() => ION.stringify(big)).toThrow('Cannot serialize BigInt');
227
+ });
228
+
229
+ test('throws on circular references', () => {
230
+ const obj: { self?: unknown } = {};
231
+ obj.self = obj;
232
+ expect(() => ION.stringify(obj)).toThrow('Circular reference');
233
+ });
234
+
235
+ test('throws on circular array references', () => {
236
+ const arr: unknown[] = [];
237
+ arr.push(arr);
238
+ expect(() => ION.stringify(arr)).toThrow('Circular reference');
239
+ });
240
+
241
+ test('throws on invalid date', () => {
242
+ const invalidDate = new Date('invalid');
243
+ expect(() => ION.stringify(invalidDate)).toThrow('Invalid Date');
244
+ });
245
+
246
+ test('throws on undefined at root', () => {
247
+ expect(() => ION.stringify(undefined)).toThrow('Cannot serialize undefined');
248
+ });
249
+
250
+ test('throws on undefined in array', () => {
251
+ const arr = [1, undefined, 3];
252
+ expect(() => ION.stringify(arr)).toThrow('Cannot serialize undefined');
253
+ });
254
+ });
255
+
256
+ describe('Error handling: invalid ION syntax', () => {
257
+ test('throws on invalid date string', () => {
258
+ expect(() => ION.parse('date:"not-a-date"')).toThrow('Invalid date string');
259
+ });
260
+
261
+ test('throws on unexpected token', () => {
262
+ expect(() => ION.parse('unexpected')).toThrow('Unexpected');
263
+ });
264
+
265
+ test('throws on unterminated string', () => {
266
+ expect(() => ION.parse('"unterminated')).toThrow('Unterminated string');
267
+ });
268
+
269
+ test('throws on malformed object', () => {
270
+ expect(() => ION.parse('{key: "value"}')).toThrow('Unexpected character');
271
+ });
272
+
273
+ test('throws on unexpected EOF', () => {
274
+ expect(() => ION.parse('{')).toThrow();
275
+ });
276
+
277
+ test('throws on trailing comma in object', () => {
278
+ expect(() => ION.parse('{"a": 1,}')).toThrow();
279
+ });
280
+ });
281
+
282
+ describe('undefined handling', () => {
283
+ test('omits undefined properties from objects', () => {
284
+ const obj = { a: 1, b: undefined, c: 3 };
285
+ const serialized = ION.stringify(obj);
286
+ const result = ION.parse(serialized);
287
+ expect(result).toEqual({ a: 1, c: 3 });
288
+ expect(result).not.toHaveProperty('b');
289
+ });
290
+
291
+ test('preserves object shape without undefined properties', () => {
292
+ const obj = { name: 'Alice', age: undefined, active: true };
293
+ const result = ION.parse(ION.stringify(obj));
294
+ expect(result).toEqual({ name: 'Alice', active: true });
295
+ });
296
+
297
+ test('handles all undefined properties', () => {
298
+ const obj = { a: undefined, b: undefined };
299
+ const result = ION.parse(ION.stringify(obj));
300
+ expect(result).toEqual({});
301
+ });
302
+ });
303
+
304
+ describe('String escaping and edge cases', () => {
305
+ test('handles strings containing ION keywords', () => {
306
+ const values = ['NaN', 'Infinity', '-Infinity', 'date:', 'map', 'set', 'true', 'false', 'null'];
307
+ for (const val of values) {
308
+ expect(ION.parse(ION.stringify(val))).toBe(val);
309
+ }
310
+ });
311
+
312
+ test('handles special characters in strings', () => {
313
+ const values = [
314
+ 'line1\nline2',
315
+ 'tab\there',
316
+ 'quote"inside',
317
+ 'backslash\\here',
318
+ 'slash/here',
319
+ ];
320
+ for (const val of values) {
321
+ expect(ION.parse(ION.stringify(val))).toBe(val);
322
+ }
323
+ });
324
+
325
+ test('handles unicode characters', () => {
326
+ const values = ['Hello 世界', '🚀', 'café', 'naïve'];
327
+ for (const val of values) {
328
+ expect(ION.parse(ION.stringify(val))).toBe(val);
329
+ }
330
+ });
331
+
332
+ test('handles empty strings', () => {
333
+ expect(ION.parse(ION.stringify(''))).toBe('');
334
+ });
335
+
336
+ test('handles objects with special string keys', () => {
337
+ const obj = {
338
+ NaN: 1,
339
+ Infinity: 2,
340
+ 'date:': 3,
341
+ map: 4,
342
+ set: 5,
343
+ };
344
+ expect(ION.parse(ION.stringify(obj))).toEqual(obj);
345
+ });
346
+ });
347
+
348
+ describe('Stringify output format', () => {
349
+ test('formats NaN correctly', () => {
350
+ expect(ION.stringify(NaN)).toBe('NaN');
351
+ });
352
+
353
+ test('formats Infinity correctly', () => {
354
+ expect(ION.stringify(Infinity)).toBe('Infinity');
355
+ });
356
+
357
+ test('formats -Infinity correctly', () => {
358
+ expect(ION.stringify(-Infinity)).toBe('-Infinity');
359
+ });
360
+
361
+ test('formats Date correctly', () => {
362
+ const date = new Date('2026-01-27T15:30:00.000Z');
363
+ expect(ION.stringify(date)).toBe('date:2026-01-27T15:30:00.000Z');
364
+ });
365
+
366
+ test('formats Map correctly', () => {
367
+ const map = new Map([['a', 1]]);
368
+ expect(ION.stringify(map)).toBe('map { "a": 1 }');
369
+ });
370
+
371
+ test('formats Set correctly', () => {
372
+ const set = new Set([1, 2]);
373
+ expect(ION.stringify(set)).toBe('set { 1, 2 }');
374
+ });
375
+
376
+ test('formats empty Map correctly', () => {
377
+ const map = new Map();
378
+ expect(ION.stringify(map)).toBe('map { }');
379
+ });
380
+
381
+ test('formats empty Set correctly', () => {
382
+ const set = new Set();
383
+ expect(ION.stringify(set)).toBe('set { }');
384
+ });
385
+ });
386
+ });