Test file

Connector contracts and signatures

Each provider’s signature checked against the exact bytes received, tokens encrypted to their workspace, rate limits, retry timing and order parsing.

tests/integrations.test.ts · 133 lines

import { describe, it, expect } from "vitest";
import { randomBytes } from "node:crypto";
import {
  sign,
  verify,
  signatureHeader,
  providerConnector,
  TokenVault,
  retryDelay,
  takePermit,
  newLimit,
  needsRefresh,
  classify,
  type Provider,
  type Gateway,
} from "../packages/integrations/src/index";

const webhookProviders: Provider[] = [
  "shopify",
  "square",
  "xero",
  "quickbooks",
];

describe("reusable integration foundations", () => {
  it.each(webhookProviders)(
    "%s verifies the exact raw bytes and rejects altered signatures",
    (provider) => {
      const body = '{"message":"demo"}';
      const secret = "test-secret";
      const notificationUrl = "https://example.test/hooks";
      const signature = sign(provider, body, secret, notificationUrl);
      const delivery = {
        body,
        notificationUrl,
        headers: { [signatureHeader[provider]]: signature },
      };

      expect(verify(provider, delivery, secret)).toBe(true);
      // One added byte breaks the signature: it is checked before parsing.
      const altered = { ...delivery, body: body + " " };
      expect(verify(provider, altered, secret)).toBe(false);
      const unsigned = { ...delivery, headers: {} };
      expect(verify(provider, unsigned, secret)).toBe(false);
      if (provider === "square") {
        // Square signs the notification URL too, so a replay to another
        // endpoint fails.
        const elsewhere = {
          ...delivery,
          notificationUrl: "https://other.test/hooks",
        };
        expect(verify(provider, elsewhere, secret)).toBe(false);
      }
    },
  );

  it("encrypts tokens bound to their workspace and connection", () => {
    const vault = new TokenVault(randomBytes(32));
    const sealed = vault.seal("secret-token", "tenant-a/connection");

    expect(sealed).not.toContain("secret-token");
    expect(vault.open(sealed, "tenant-a/connection")).toBe("secret-token");
    // The context is authenticated: another workspace can't open it.
    expect(() => vault.open(sealed, "tenant-b/connection")).toThrow();
    // A fresh nonce each time, so equal tokens don't look equal.
    const again = vault.seal("secret-token", "tenant-a/connection");
    expect(again).not.toBe(sealed);
  });

  it("enforces per-connection windows and Xero's documented daily limit", () => {
    // Xero allows 60 calls a minute and 5,000 a day per connection.
    const busy = newLimit();
    const quiet = newLimit();
    const start = 1000;

    for (let call = 0; call < 60; call++)
      expect(takePermit(busy, start)).toBe(0);
    // The 61st waits for the minute to turn over.
    expect(takePermit(busy, start)).toBe(59000);
    expect(takePermit(quiet, start)).toBe(0);
    expect(takePermit(busy, start + 60000)).toBe(0);

    busy.dayCount = 5000;
    expect(takePermit(busy, start + 60001)).toBeGreaterThan(0);
  });

  it("classifies failures, bounds jitter and refreshes tokens early", () => {
    expect(classify(429, "3").retryAfterMs).toBe(3000);
    // Retry-After can also be a date: wait until then.
    const fiveSecondsIn = new Date(5000).toUTCString();
    expect(classify(429, fiveSecondsIn, 0).retryAfterMs).toBe(5000);
    expect(classify(503).kind).toBe("transient");
    expect(classify(400).kind).toBe("permanent");
    expect(classify(401).kind).toBe("auth");

    // A second attempt waits between half and all of 2^2 seconds.
    expect(retryDelay(2, () => 0)).toBe(2000);
    expect(retryDelay(2, () => 1)).toBe(4000);

    // Tokens are refreshed when under two minutes remain.
    const expiresAt = new Date(61000).toISOString();
    expect(needsRefresh(expiresAt, 0)).toBe(true);
  });

  it("parses a Shopify order from the docs without the domain package", async () => {
    const body = JSON.stringify({
      id: 91,
      created_at: "2026-10-11T10:00:00Z",
      updated_at: "2026-10-11T10:01:00Z",
      line_items: [{ variant_id: 41, quantity: 2 }],
    });
    const delivery = {
      body,
      headers: {
        "x-shopify-topic": "orders/create",
        "x-shopify-webhook-id": "delivery-1",
      },
      notificationUrl: "",
    };

    const events = await providerConnector("shopify").parse(
      delivery,
      {} as Gateway,
    );

    expect(events[0]).toMatchObject({
      id: "delivery-1",
      objectId: "91",
      type: "order.created",
      lines: [{ itemId: "41", quantity: 2 }],
    });
  });
});