123 lines
3.8 KiB
TypeScript
123 lines
3.8 KiB
TypeScript
import { EVENTS_CODES, EventsError } from "@suaveplan/events";
|
|
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import type { SubscriptionEntry } from "./event-bus-service.types.js";
|
|
import {
|
|
buildEmitOptions,
|
|
nextSubscriptionId,
|
|
projectSubscription,
|
|
recordPublishMetric,
|
|
stableHash,
|
|
toErrorReport,
|
|
} from "./helpers.js";
|
|
|
|
describe("nextSubscriptionId", () => {
|
|
it("returns a unique id on every call", { timeout: 5000 }, () => {
|
|
const a = nextSubscriptionId();
|
|
const b = nextSubscriptionId();
|
|
expect(a).not.toBe(b);
|
|
expect(a.startsWith("sub_")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("stableHash", () => {
|
|
it("returns deterministic JSON for plain objects", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
expect(stableHash({ a: 1, b: 2 })).toBe(JSON.stringify({ a: 1, b: 2 }));
|
|
});
|
|
|
|
it("returns empty string for undefined", { timeout: 5000 }, () => {
|
|
expect(stableHash(undefined)).toBe("");
|
|
});
|
|
|
|
it("falls back to String(payload) on JSON failure", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const circular: Record<string, unknown> = {};
|
|
circular.self = circular;
|
|
expect(stableHash(circular)).toBe(String(circular));
|
|
});
|
|
});
|
|
|
|
describe("toErrorReport", () => {
|
|
it("preserves EventsError code", { timeout: 5000 }, () => {
|
|
const err = new EventsError(EVENTS_CODES["LISTENER.THREW"], "boom");
|
|
const report = toErrorReport(err);
|
|
expect(report.code).toBe("EVENTS.LISTENER.THREW");
|
|
expect(report.message).toBe("boom");
|
|
});
|
|
|
|
it("uses LISTENER_FAILED envelope for plain Error", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const report = toErrorReport(new Error("oops"));
|
|
expect(report.code).toBe("EVENT_BUS.LISTENER_FAILED");
|
|
expect(report.message).toBe("oops");
|
|
});
|
|
|
|
it("stringifies non-Error throwables", { timeout: 5000 }, () => {
|
|
const report = toErrorReport("literal");
|
|
expect(report.code).toBe("EVENT_BUS.LISTENER_FAILED");
|
|
expect(report.message).toBe("literal");
|
|
});
|
|
});
|
|
|
|
describe("projectSubscription", () => {
|
|
const base: SubscriptionEntry = {
|
|
id: "s",
|
|
topic: "t",
|
|
scopedTopic: "t",
|
|
listener: () => undefined,
|
|
detach: () => undefined,
|
|
createdAt: 0,
|
|
once: false,
|
|
idempotent: false,
|
|
};
|
|
|
|
it("includes tenantId when present", { timeout: 5000 }, () => {
|
|
const view = projectSubscription({ ...base, tenantId: "ta" });
|
|
expect(view.tenantId).toBe("ta");
|
|
});
|
|
|
|
it("omits tenantId when absent", { timeout: 5000 }, () => {
|
|
const view = projectSubscription(base);
|
|
expect("tenantId" in view).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("buildEmitOptions", () => {
|
|
it("returns an empty object when nothing is supplied", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
expect(buildEmitOptions({})).toEqual({});
|
|
});
|
|
|
|
it("forwards signal/timeoutMs/traceId", { timeout: 5000 }, () => {
|
|
const ac = new AbortController();
|
|
const out = buildEmitOptions({
|
|
signal: ac.signal,
|
|
timeoutMs: 1234,
|
|
traceId: "trace-1",
|
|
});
|
|
expect(out?.signal).toBe(ac.signal);
|
|
expect(out?.timeoutMs).toBe(1234);
|
|
expect(out?.traceId).toBe("trace-1");
|
|
});
|
|
|
|
it("falls back from traceId to idempotencyKey", { timeout: 5000 }, () => {
|
|
const out = buildEmitOptions({ idempotencyKey: "k1" });
|
|
expect(out?.traceId).toBe("k1");
|
|
});
|
|
});
|
|
|
|
describe("recordPublishMetric", () => {
|
|
it("does not throw for either outcome", { timeout: 5000 }, () => {
|
|
expect(() =>
|
|
recordPublishMetric("order.created", "success", 1)
|
|
).not.toThrow();
|
|
expect(() =>
|
|
recordPublishMetric("order.created", "error", 1)
|
|
).not.toThrow();
|
|
});
|
|
});
|