import { describe, expect, it } from "@suaveplan/testing/runner"; import { DeliveryTracker } from "../delivery-tracker/delivery-tracker.js"; import { InMemorySmsProvider } from "../in-memory-provider/in-memory-provider.js"; import { RateLimiter } from "../rate-limiter/rate-limiter.js"; import { RetryPolicy } from "../retry/retry.js"; import { parseSmsMessage } from "../schemas/schemas.js"; import { SmsService } from "../service/sms-service.js"; import { TemplateRegistry } from "../templates/templates.js"; import { SMS_CODES, SmsAbortedError, SmsConfigMissingError, SmsForbiddenError, SmsInvalidConfigError, SmsInvalidNumberError, SmsProviderError, SmsProviderNotConfiguredError, SmsRateLimitedError, SmsReceiptNotFoundError, SmsRecipientOptedOutError, SmsScheduleCancelledError, SmsSendFailedError, SmsTemplateNotFoundError, } from "./error-codes.js"; describe("SMS error-code registry", () => { it("exposes the thirteen declared codes", { timeout: 5000 }, () => { expect(Object.values(SMS_CODES).sort()).toEqual( [ "SMS.ABORTED", "SMS.CONFIG_MISSING", "SMS.FORBIDDEN", "SMS.INVALID_CONFIG", "SMS.INVALID_NUMBER", "SMS.PROVIDER_ERROR", "SMS.PROVIDER_NOT_CONFIGURED", "SMS.RATE_LIMITED", "SMS.RECEIPT_NOT_FOUND", "SMS.RECIPIENT_OPTED_OUT", "SMS.SCHEDULE_CANCELLED", "SMS.SEND_FAILED", "SMS.TEMPLATE_NOT_FOUND", ].sort() ); }); it("every SMS code is reachable from a production call site", { timeout: 30_000, }, async () => { // SMS.PROVIDER_NOT_CONFIGURED const noProvider = new SmsService(); await expect( noProvider.send({ to: "+15551234567", body: "hi" }) ).rejects.toBeInstanceOf(SmsProviderNotConfiguredError); // SMS.INVALID_NUMBER const svc = new SmsService({ provider: new InMemorySmsProvider(), retry: { maxAttempts: 1 }, }); await expect( svc.send({ to: "bad", body: "hi" }) ).rejects.toBeInstanceOf(SmsInvalidNumberError); // SMS.RECIPIENT_OPTED_OUT const svc2 = new SmsService({ provider: new InMemorySmsProvider(), retry: { maxAttempts: 1 }, }); svc2.optOut("+15551234567"); await expect( svc2.send({ to: "+15551234567", body: "hi" }) ).rejects.toBeInstanceOf(SmsRecipientOptedOutError); // SMS.RATE_LIMITED const svc3 = new SmsService({ provider: new InMemorySmsProvider(), rateLimiter: { capacity: 1, refillPerSecond: 1 }, retry: { maxAttempts: 1 }, }); await svc3.send({ to: "+15551234567", body: "hi" }); await expect( svc3.send({ to: "+15551234567", body: "hi" }) ).rejects.toBeInstanceOf(SmsRateLimitedError); // SMS.TEMPLATE_NOT_FOUND const reg = new TemplateRegistry(); expect(() => reg.require("absent")).toThrow(SmsTemplateNotFoundError); // SMS.INVALID_CONFIG expect(() => parseSmsMessage({ to: "+1", body: "" })).toThrow( SmsInvalidConfigError ); // SMS.RECEIPT_NOT_FOUND const tracker = new DeliveryTracker(); await expect(tracker.lookupOrFetch("absent")).rejects.toBeInstanceOf( SmsReceiptNotFoundError ); // SMS.ABORTED (pre-aborted) const ctrl = new AbortController(); ctrl.abort(); const retry = new RetryPolicy({ maxAttempts: 1, random: () => 0, }); await expect( retry.execute(async () => 1, { signal: ctrl.signal }) ).rejects.toBeInstanceOf(SmsAbortedError); // SMS.PROVIDER_ERROR (raw construct) expect(() => { throw new SmsProviderError(); }).toThrow(SmsProviderError); // SMS.SEND_FAILED — exhausted retries with non-coded error const svc4 = new SmsService({ provider: (() => { const p = new InMemorySmsProvider(); p.failOn(() => true, new Error("transient")); return p; })(), retry: { maxAttempts: 1 }, rateLimiter: { capacity: 100, refillPerSecond: 100 }, }); await expect( svc4.send({ to: "+15559999999", body: "hi" }) ).rejects.toBeInstanceOf(SmsSendFailedError); // SMS.CONFIG_MISSING — synthetic constructable expect(() => { throw new SmsConfigMissingError(); }).toThrow(SmsConfigMissingError); // SMS.FORBIDDEN — synthetic constructable expect(() => { throw new SmsForbiddenError(); }).toThrow(SmsForbiddenError); // SMS.SCHEDULE_CANCELLED — via cancel handle const svc5 = new SmsService({ provider: new InMemorySmsProvider(), }); const handle = svc5.scheduleAt(new Date(Date.now() + 60_000), { to: "+15551234567", body: "later", }); handle.cancel(); expect(() => { throw new SmsScheduleCancelledError(); }).toThrow(SmsScheduleCancelledError); }); it("rate limiter is independent of the service", { timeout: 5000 }, () => { const r = new RateLimiter({ capacity: 1, refillPerSecond: 1 }); expect(r.tryConsume("x")).toBe(true); }); it("every error class accepts a custom message + context", { timeout: 5000, }, () => { const ctx = { messageId: "m1" }; const errs = [ new SmsInvalidNumberError({ message: "custom", context: ctx }), new SmsInvalidConfigError({ message: "custom", context: ctx }), new SmsProviderNotConfiguredError({ message: "custom", context: ctx, }), new SmsProviderError({ message: "custom", context: ctx }), new SmsSendFailedError({ message: "custom", context: ctx }), new SmsTemplateNotFoundError({ message: "custom", context: ctx }), new SmsRecipientOptedOutError({ message: "custom", context: ctx, }), new SmsRateLimitedError({ message: "custom", context: ctx }), new SmsForbiddenError({ message: "custom", context: ctx }), new SmsConfigMissingError({ message: "custom", context: ctx }), new SmsScheduleCancelledError({ message: "custom", context: ctx, }), new SmsAbortedError({ message: "custom", context: ctx }), new SmsReceiptNotFoundError({ message: "custom", context: ctx }), ]; for (const e of errs) { expect(e.message).toBe("custom"); expect(e.context).toEqual(ctx); } }); it("constructs with cause + traceId branches", { timeout: 5000 }, () => { const cause = new Error("inner"); const e = new SmsSendFailedError({ cause, traceId: "t1" }); expect(e.cause).toBe(cause); expect(e.traceId).toBe("t1"); }); it("SmsInvalidConfigError merges context + issues", { timeout: 5000, }, () => { const e = new SmsInvalidConfigError({ context: { extra: 1 }, issues: [{ code: "x", path: [], message: "y" }], }); expect(e.context?.["extra"]).toBe(1); expect(e.context?.["issues"]).toBeDefined(); }); });