1import { describe, expect, it } from "vitest";
2import { of } from "rxjs";
3import { firstValueFrom } from "rxjs";
4import { tap } from "rxjs/operators";
5import type { Observable } from "rxjs";
6import { BigIntSerializationInterceptor } from "../interceptors/bigint-serialization.interceptor";
7
8
22
23describe("BigIntSerializationInterceptor", () => {
24 const make = () => new BigIntSerializationInterceptor();
25
26 it("converts bigint leaves in nested plain objects to decimal strings", async () => {
27 const body = {
28 workspace: { id: "ws_1", usageStorageBytes: 10737418240n },
29 list: [{ bytes: 42n }],
30 untouched: "text",
31 count: 7,
32 };
33 const out = (await firstValueFrom(make().intercept({} as never, { handle: () => of(body) }))) as {
34 workspace: { usageStorageBytes: string };
35 list: { bytes: string }[];
36 untouched: string;
37 count: number;
38 };
39 expect(out.workspace.usageStorageBytes).toBe("10737418240");
40 expect(out.list[0].bytes).toBe("42");
41 expect(out.untouched).toBe("text");
42 expect(out.count).toBe(7);
43
44 expect(() => JSON.stringify(out)).not.toThrow();
45 });
46
47 it("passes through null/arrays and preserves Date instances", async () => {
48 const d = new Date("2026-01-02T03:04:05Z");
49 const out = (await firstValueFrom(
50 make().intercept({} as never, {
51 handle: () => of({ when: d, empty: null, arr: [1n, 2] }),
52 }),
53 )) as { when: Date; empty: null; arr: (string | number)[] };
54 expect(out.when).toBe(d);
55 expect(out.empty).toBeNull();
56 expect(out.arr).toEqual(["1", 2]);
57 expect(JSON.stringify({ when: out.when })).toBe('{"when":"2026-01-02T03:04:05.000Z"}');
58 });
59
60 it(
61 "in production order [idempotency, bigint], the idempotency tap sees a JSON-safe body",
62 async () => {
63 const captured: unknown[] = [];
64 const outer = (obs: Observable<unknown>) => obs.pipe(tap((v) => captured.push(v)));
65
66
67
68 const handlerValue = { ok: true, file: { size: 12345n, bytes: 9007199254740993n } };
69 const obs = outer(
70 make().intercept({} as never, { handle: () => of(handlerValue) }),
71 );
72 const out = await firstValueFrom(obs);
73
74
75 expect(captured).toHaveLength(1);
76 expect(() => JSON.stringify(captured[0])).not.toThrow();
77 expect((captured[0] as { file: { size: string; bytes: string } }).file.size).toBe("12345");
78 expect((captured[0] as { file: { bytes: string } }).file.bytes).toBe("9007199254740993");
79 expect(JSON.stringify(captured[0])).toBe(JSON.stringify(out));
80 },
81 );
82
83 it("documents the root cause: JSON.stringify on a raw bigint throws", () => {
84
85
86 expect(() => JSON.stringify({ usageStorageBytes: 1n })).toThrow();
87 });
88
89 it("fast path: a bigint-free body is returned by REFERENCE (no clone)", async () => {
90 const body = { id: "ws_1", items: [{ n: 1, s: "x" }], at: new Date() };
91 const out = await firstValueFrom(
92 make().intercept({} as never, { handle: () => of(body) }),
93 );
94 expect(out).toBe(body);
95 });
96});
97