← Provider replacement walkthrough

IAPKit · convex/commerce/delivery.test.ts

Source snapshot · 2026-09-09

SHA-256 31cd2dae1ec8da9f2f2a2159a622587ffbe895380c04878eed9f8726b4722bef
1import { afterEach, describe, expect, it, vi } from "vitest";2import type { LookupFunction } from "node:net";3 4import {5  buildPinnedRequestOptions,6  deliverPendingEventsHandler,7  MAX_FALLBACK_ADDRESSES,8  postJsonPinned,9  resolvePublicAddresses,10  type DeliveryRequest,11} from "./delivery";12import {13  DELIVERY_ID_HEADER,14  EVENT_ID_HEADER,15  SIGNATURE_HEADER,16  TIMESTAMP_HEADER,17} from "./signing";18 19const claimed = {20  deliveryId: "outboundDeliveries_1",21  leaseToken: "lease-1",22  attempts: 0,23  url: "https://hooks.example.com/iapkit?source=test",24  secret: "whsec_test",25  body: JSON.stringify({ eventType: "subscription.renewed" }),26  eventId: "commerceEvents_1",27} as const;28 29afterEach(() => {30  vi.useRealTimers();31});32 33describe("deliverPendingEventsHandler", () => {34  it("posts signed headers and records the response before claiming again", async () => {35    vi.setSystemTime(new Date("2026-08-27T00:00:00.000Z"));36    let claimCount = 0;37    const recorded: Array<Record<string, unknown>> = [];38    const runMutation = vi.fn(async (_reference, args) => {39      if (Object.keys(args).length === 0) {40        claimCount += 1;41        return claimCount === 1 ? [claimed] : [];42      }43      recorded.push(args);44      return null;45    });46    const requests: DeliveryRequest[] = [];47 48    await expect(49      deliverPendingEventsHandler({ runMutation } as never, async (request) => {50        requests.push(request);51        return 204;52      }),53    ).resolves.toEqual({ attempted: 1, delivered: 1 });54 55    expect(requests).toHaveLength(1);56    expect(requests[0].url.toString()).toBe(claimed.url);57    expect(requests[0].body).toBe(claimed.body);58    expect(requests[0].headers[EVENT_ID_HEADER]).toBe(claimed.eventId);59    expect(requests[0].headers[DELIVERY_ID_HEADER]).toBe(claimed.deliveryId);60    expect(requests[0].headers[TIMESTAMP_HEADER]).toBe("1787788800");61    expect(requests[0].headers[SIGNATURE_HEADER]).toMatch(/^v1=[0-9a-f]{64}$/);62    expect(recorded).toEqual([63      expect.objectContaining({64        deliveryId: claimed.deliveryId,65        leaseToken: claimed.leaseToken,66        ok: true,67        statusCode: 204,68        retryable: false,69      }),70    ]);71  });72 73  it("records redirects as permanent failures without following them", async () => {74    let claimCount = 0;75    const recorded: Array<Record<string, unknown>> = [];76    const runMutation = vi.fn(async (_reference, args) => {77      if (Object.keys(args).length === 0) {78        claimCount += 1;79        return claimCount === 1 ? [claimed] : [];80      }81      recorded.push(args);82      return null;83    });84    const post = vi.fn(async () => 302);85 86    await expect(87      deliverPendingEventsHandler({ runMutation } as never, post),88    ).resolves.toEqual({ attempted: 1, delivered: 0 });89    expect(post).toHaveBeenCalledTimes(1);90    expect(recorded[0]).toMatchObject({91      ok: false,92      statusCode: 302,93      retryable: false,94    });95  });96 97  it("rejects an unsafe stored URL before issuing a request", async () => {98    let claimCount = 0;99    const recorded: Array<Record<string, unknown>> = [];100    const unsafeClaim = { ...claimed, url: "https://127.0.0.1/hook" };101    const runMutation = vi.fn(async (_reference, args) => {102      if (Object.keys(args).length === 0) {103        claimCount += 1;104        return claimCount === 1 ? [unsafeClaim] : [];105      }106      recorded.push(args);107      return null;108    });109    const post = vi.fn(async () => 204);110 111    await expect(112      deliverPendingEventsHandler({ runMutation } as never, post),113    ).resolves.toEqual({ attempted: 1, delivered: 0 });114    expect(post).not.toHaveBeenCalled();115    expect(recorded[0]).toMatchObject({116      deliveryId: claimed.deliveryId,117      leaseToken: claimed.leaseToken,118      ok: false,119      error: "destination rejected: host-not-public",120      retryable: false,121    });122  });123 124  it("records transport failures as retryable", async () => {125    let claimCount = 0;126    const recorded: Array<Record<string, unknown>> = [];127    const runMutation = vi.fn(async (_reference, args) => {128      if (Object.keys(args).length === 0) {129        claimCount += 1;130        return claimCount === 1 ? [claimed] : [];131      }132      recorded.push(args);133      return null;134    });135 136    await expect(137      deliverPendingEventsHandler({ runMutation } as never, async () => {138        throw new Error("connection reset");139      }),140    ).resolves.toEqual({ attempted: 1, delivered: 0 });141    expect(recorded[0]).toMatchObject({142      deliveryId: claimed.deliveryId,143      leaseToken: claimed.leaseToken,144      ok: false,145      error: "connection reset",146      retryable: true,147    });148  });149});150 151describe("resolvePublicAddresses", () => {152  it("rejects private, shared and link-local DNS answers", async () => {153    for (const address of [154      "127.0.0.1",155      "100.64.0.1",156      "169.254.169.254",157      "::1",158    ]) {159      await expect(160        resolvePublicAddresses("hooks.example.com", async () => [161          { address, family: address.includes(":") ? 6 : 4 },162        ]),163      ).rejects.toThrow(/non-public/);164    }165  });166 167  it("rejects a mixed public/private DNS response", async () => {168    await expect(169      resolvePublicAddresses("hooks.example.com", async () => [170        { address: "93.184.216.34", family: 4 },171        { address: "10.0.0.5", family: 4 },172      ]),173    ).rejects.toThrow(/non-public/);174  });175 176  it("accepts only-public answers for a pinned connection", async () => {177    await expect(178      resolvePublicAddresses("hooks.example.com", async () => [179        { address: "93.184.216.34", family: 4 },180        { address: "2606:4700:4700::1111", family: 6 },181      ]),182    ).resolves.toHaveLength(2);183  });184 185  it("caps a tenant-controlled DNS answer set", async () => {186    const addresses = await resolvePublicAddresses(187      "hooks.example.com",188      async () =>189        Array.from({ length: 20 }, (_, index) => ({190          address: `93.184.216.${index + 1}`,191          family: 4,192        })),193    );194    expect(addresses).toHaveLength(MAX_FALLBACK_ADDRESSES);195  });196 197  it("falls back across validated addresses within one request deadline", async () => {198    const attempted: string[] = [];199    const request: DeliveryRequest = {200      url: new URL("https://hooks.example.com/iapkit"),201      headers: { "content-type": "application/json" },202      body: "{}",203    };204    const status = await postJsonPinned(205      request,206      async () => [207        { address: "93.184.216.34", family: 4 },208        { address: "2606:4700:4700::1111", family: 6 },209      ],210      async (_request, address, timeoutMs) => {211        attempted.push(address.address);212        expect(timeoutMs).toBeGreaterThan(0);213        if (attempted.length === 1) throw new Error("network unreachable");214        return 204;215      },216    );217    expect(status).toBe(204);218    expect(attempted).toEqual(["93.184.216.34", "2606:4700:4700::1111"]);219  });220 221  it("pins lookup while preserving TLS SNI and the full request path", () => {222    const request: DeliveryRequest = {223      url: new URL("https://hooks.example.com:8443/iapkit?source=test"),224      headers: { "content-type": "application/json" },225      body: "{}",226    };227    const options = buildPinnedRequestOptions(request, {228      address: "93.184.216.34",229      family: 4,230    });231    expect(options).toMatchObject({232      protocol: "https:",233      hostname: "hooks.example.com",234      servername: "hooks.example.com",235      port: "8443",236      path: "/iapkit?source=test",237      method: "POST",238      headers: request.headers,239    });240    let pinned: unknown;241    (options.lookup as LookupFunction)(242      "hooks.example.com",243      { all: false },244      (error, address, family) => {245        expect(error).toBeNull();246        pinned = { address, family };247      },248    );249    expect(pinned).toEqual({ address: "93.184.216.34", family: 4 });250  });251});252