Test file

Public evidence redaction

The saved provider reads on the home page: allowlisted fields and routes only, one workspace, an invoice shown only once it was read back, a cache and a rate limit.

tests/showcase.test.ts · 513 lines

import { describe, it, expect, vi } from "vitest";
import Fastify from "fastify";
import { PGlite } from "@electric-sql/pglite";
import { createRequire } from "node:module";
import { randomUUID } from "node:crypto";
import { createShowcase } from "../packages/db/src/showcase";
import {
  observeReads,
  observeGrant,
} from "../packages/db/src/showcase-observer";
import {
  captureRead,
  redactResponse,
  safeEvidence,
  safeScopes,
} from "../packages/db/src/showcase-model";
import { registerShowcase } from "../apps/api/src/showcase";
import type { Store } from "../packages/db/src/index";

type Database = Parameters<typeof createShowcase>[0];
// drizzle-orm is a dependency of packages/db, not of the tests.
const { drizzle } = createRequire(
  new URL("../packages/db/package.json", import.meta.url),
)("drizzle-orm/pglite") as { drizzle: (client: PGlite) => Database };

const providers = [
  "shopify",
  "square",
  "xero",
  "quickbooks",
  "stripe",
] as const;

describe("what a saved read may keep", () => {
  const privateFields = {
    access_token: "SECRET",
    refresh_token: "REFRESH",
    email: "person@example.test",
    address: "PRIVATE_ADDRESS",
    id: "PRIVATE_ID",
    unknown: { token: "SECRET" },
  };
  const privateValues = Object.values(privateFields).filter(
    (value) => typeof value === "string",
  );
  // Each provider's real response shape, with private fields at every level.
  const samples = {
    shopify: {
      ...privateFields,
      data: {
        shop: {
          ...privateFields,
          currencyCode: "GBP",
          plan: { partnerDevelopment: true, ...privateFields },
        },
      },
    },
    square: {
      merchant: {
        ...privateFields,
        currency: "USD",
        country: "US",
        status: "ACTIVE",
      },
    },
    xero: {
      Organisations: [
        {
          ...privateFields,
          IsDemoCompany: true,
          CountryCode: "US",
          BaseCurrency: "USD",
        },
      ],
    },
    quickbooks: {
      Invoice: {
        ...privateFields,
        TotalAmt: 24,
        Balance: 24,
        CurrencyRef: { value: "GBP", ...privateFields },
        TxnTaxDetail: { TotalTax: 4, ...privateFields },
      },
    },
    stripe: {
      ...privateFields,
      object: "list",
      livemode: false,
      data: [
        {
          ...privateFields,
          object: "payment_intent",
          amount: 2400,
          currency: "gbp",
          status: "succeeded",
          livemode: false,
        },
      ],
    },
  } as const;

  it.each(providers)(
    "%s keeps only allowlisted fields, routes and scopes",
    (provider) => {
      const kept = JSON.stringify(redactResponse(provider, samples[provider]));
      for (const value of privateValues) expect(kept).not.toContain(value);
      expect(kept).not.toMatch(
        /access_token|refresh_token|email|address|unknown|PRIVATE_ID/,
      );

      // A path that isn't on the provider's list is refused whole.
      const unlisted = {
        request: { method: "GET", path: "/PRIVATE_ID?token=SECRET" },
        status: 200,
        response: samples[provider],
      };
      expect(safeEvidence(provider, unlisted)).toBeNull();

      const scopes = ["SECRET", "person@example.test", "write_everything"];
      expect(safeScopes(provider, scopes)).toEqual([]);
    },
  );

  it("captures no token exchange, no write and no route off the list", () => {
    const capture = (
      provider: "quickbooks" | "square",
      url: string,
      method: string,
      body: unknown,
    ) =>
      captureRead(
        provider,
        new URL(url),
        method,
        body,
        new Headers(),
        new Date(),
      );

    expect(
      capture(
        "quickbooks",
        "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer",
        "POST",
        { access_token: "SECRET" },
      ),
    ).toBeNull();
    expect(
      capture(
        "quickbooks",
        "https://sandbox-quickbooks.api.intuit.com/v3/company/123/invoice",
        "POST",
        samples.quickbooks,
      ),
    ).toBeNull();
    expect(
      capture(
        "square",
        "https://connect.squareup.com/v2/merchants/me",
        "GET",
        samples.square,
      ),
    ).toBeNull();
  });

  it("counts every record but keeps three, with numeric headroom and no query string", () => {
    const contacts = Array.from({ length: 7 }, () => ({
      ContactStatus: "ACTIVE",
      EmailAddress: "PRIVATE_EMAIL",
      ContactID: "PRIVATE_ID",
    }));
    const result = captureRead(
      "xero",
      new URL(
        "https://api.xero.com/api.xro/2.0/Contacts?page=1&secret=PRIVATE_SECRET",
      ),
      "GET",
      { Contacts: contacts },
      new Headers({
        "x-minlimit-remaining": "59",
        "x-daylimit-remaining": "4999",
        authorization: "PRIVATE_TOKEN",
      }),
      new Date("2026-10-13T12:00:00Z"),
    );

    expect(result?.evidence.recordsInResponse).toBe(7);
    expect(result?.evidence.response.Contacts).toHaveLength(3);
    expect(result?.limits).toEqual({ minuteRemaining: 59, dayRemaining: 4999 });
    expect(result?.evidence.request.path).not.toContain("?");
    expect(JSON.stringify(result)).not.toContain("PRIVATE");
  });
});

it("reads only the configured workspace, redacts stored evidence again and shows only a read-back invoice", async () => {
  const client = new PGlite();
  const db = drizzle(client);
  const tenant = randomUUID();
  const other = randomUUID();
  const connection = randomUUID();
  const otherConnection = randomUUID();
  try {
    // Just the tables the showcase reads, so this tests its SQL, not the
    // code that writes them.
    await client.exec(`
      CREATE TABLE shopify_setups
        (id uuid, tenant_id uuid, status text, credentials text);
      CREATE TABLE square_setups
        (id uuid, tenant_id uuid, status text, credentials text);
      CREATE TABLE xero_setups
        (id uuid, tenant_id uuid, status text, credentials text);
      CREATE TABLE quickbooks_setups
        (id uuid, tenant_id uuid, status text, credentials text);
      CREATE TABLE stripe_setups
        (id uuid, tenant_id uuid, status text, key_fingerprint text);
      CREATE TABLE showcase_observations
        (tenant_id uuid, provider text, connection_id uuid, data jsonb);
      CREATE TABLE billing_delivery_audit
        (tenant_id uuid, delivery_id uuid, action text, at timestamptz);
      CREATE TABLE billing_deliveries
        (id uuid DEFAULT gen_random_uuid(), tenant_id uuid,
         connection_id uuid, status text, external_id text,
         snapshot jsonb, updated_at timestamptz);
    `);
    await client.query(
      `INSERT INTO quickbooks_setups VALUES
         ($1, $2, 'prepared', 'PRIVATE_TOKEN'),
         ($3, $4, 'prepared', 'OTHER_TOKEN')`,
      [connection, tenant, otherConnection, other],
    );

    const evidence = {
      request: {
        method: "GET",
        path: "/v3/company/{redacted}/invoice/{redacted}",
        authorization: "PRIVATE_TOKEN",
      },
      status: 200,
      response: {
        Invoice: {
          TotalAmt: 24,
          CurrencyRef: { value: "GBP" },
          PrivateNote: "PRIVATE_NOTE",
          CustomerRef: { value: "PRIVATE_CUSTOMER" },
          secret: "PRIVATE_TOKEN",
        },
      },
      recordsInResponse: 1,
      sampledAt: "2026-10-13T12:00:00Z",
    };
    const ours = {
      evidence,
      recordCount: 1,
      scopes: ["com.intuit.quickbooks.accounting", "PRIVATE_TOKEN"],
      credentials: "PRIVATE_TOKEN",
    };
    const theirs = {
      recordCount: 999,
      evidence: { ...evidence, response: { Invoice: { TotalAmt: 999 } } },
    };
    await client.query(
      `INSERT INTO showcase_observations VALUES
         ($1, 'quickbooks', $2, $3),
         ($4, 'quickbooks', $5, $6)`,
      [
        tenant,
        connection,
        JSON.stringify(ours),
        other,
        otherConnection,
        JSON.stringify(theirs),
      ],
    );

    const invoice = {
      id: "183",
      currency: "GBP",
      totalCents: 2400,
      privateNote: "PRIVATE_NOTE",
      customerId: "PRIVATE_CUSTOMER",
    };
    const otherInvoice = { id: "999", currency: "GBP", totalCents: 99900 };
    // 184 is still waiting for review, so it isn't evidence.
    await client.query(
      `INSERT INTO billing_deliveries
         (tenant_id, connection_id, status, external_id, snapshot, updated_at)
       VALUES
         ($1, $2, 'posted', '183', $3, now()),
         ($4, $5, 'posted', '999', $6, now()),
         ($1, $2, 'needs_review', '184', $3, now())`,
      [
        tenant,
        connection,
        JSON.stringify(invoice),
        other,
        otherConnection,
        JSON.stringify(otherInvoice),
      ],
    );
    const quickbooks = async () =>
      (await createShowcase(db, tenant).read()).providers.find(
        (provider) => provider.provider === "quickbooks",
      );

    // Posted isn't enough: it must have been read back from QuickBooks.
    expect((await quickbooks())?.invoice).toBeNull();
    await client.query(
      `INSERT INTO billing_delivery_audit
       SELECT tenant_id, id, 'balance.refreshed', now()
       FROM billing_deliveries WHERE external_id = '183'`,
    );

    const result = await createShowcase(db, tenant).read();
    expect(result.configured).toBe(true);
    expect(result.providers).toHaveLength(5);
    const shown = result.providers.find(
      (provider) => provider.provider === "quickbooks",
    );
    expect(shown?.invoice).toMatchObject({
      amountCents: 2400,
      readbackMatch: true,
    });
    // Its own ID stays private: this answer is public.
    expect(shown?.invoice).not.toHaveProperty("id");
    expect(shown?.recordsInLastRead).toBe(1);

    const published = JSON.stringify(result);
    for (const secret of [
      tenant,
      other,
      connection,
      otherConnection,
      "PRIVATE_TOKEN",
      "OTHER_TOKEN",
      "PRIVATE_NOTE",
      "PRIVATE_CUSTOMER",
      '"999"',
      '"184"',
    ])
      expect(published).not.toContain(secret);
    expect(published).not.toMatch(
      /"(?:tenantId|connectionId|credentials|authorization|CustomerRef)"\s*:/,
    );

    expect(await createShowcase(db).read()).toEqual({
      configured: false,
      message: "Live sandboxes not configured in this environment",
      providers: [],
    });
    expect(() => createShowcase(db, "invalid")).toThrow();
  } finally {
    await client.close();
  }
});

it("serves a cache without calling providers, loads it once for concurrent requests, and limits each peer", async () => {
  let clock = Date.parse("2026-10-13T12:00:00Z");
  const read = vi.fn(async () => ({
    configured: false as const,
    message: "Live sandboxes not configured in this environment",
    providers: [],
  }));
  const provider = vi.fn(() => {
    throw new Error("Must never call a provider");
  });
  const store = {
    showcase: { read },
    quickbooks: { company: provider },
    xero: { catalog: provider },
  } as unknown as Store;
  const app = Fastify({ trustProxy: false });
  registerShowcase(app, store, () => clock);
  const status = async (headers: Record<string, string> = {}) =>
    (await app.inject({ url: "/api/showcase/sandboxes", headers })).statusCode;
  try {
    const first = await Promise.all(
      Array.from({ length: 10 }, () =>
        app.inject({ url: "/api/showcase/sandboxes?tenant=PRIVATE_OVERRIDE" }),
      ),
    );
    expect(first.every((response) => response.statusCode === 200)).toBe(true);
    expect(read).toHaveBeenCalledTimes(1);
    // A query string can't pick the workspace.
    expect(read).toHaveBeenCalledWith();
    expect(provider).not.toHaveBeenCalled();
    expect(first[0].body).not.toContain("PRIVATE_OVERRIDE");

    // 60 requests a minute per peer. With trustProxy off, a forwarded-for
    // header doesn't make a new peer.
    for (let request = 10; request < 60; request++)
      expect(await status({ "x-forwarded-for": `198.51.100.${request}` })).toBe(
        200,
      );
    expect(await status()).toBe(429);

    clock += 60001;
    expect(await status()).toBe(200);
    expect(read).toHaveBeenCalledTimes(1);

    // The cache lasts five minutes.
    clock += 300000;
    expect(await status()).toBe(200);
    expect(read).toHaveBeenCalledTimes(2);

    const write = await app.inject({
      method: "POST",
      url: "/api/showcase/sandboxes",
    });
    expect(write.statusCode).toBe(404);
  } finally {
    await app.close();
  }
});

it("answers a generic error when saved evidence fails, never its details", async () => {
  const app = Fastify();
  registerShowcase(app, {
    showcase: {
      read: async () => {
        throw new Error("PRIVATE_TOKEN PRIVATE_TENANT SQL");
      },
    },
  } as unknown as Store);
  try {
    const response = await app.inject({ url: "/api/showcase/sandboxes" });
    expect(response.statusCode).toBe(503);
    expect(response.body).not.toContain("PRIVATE");
  } finally {
    await app.close();
  }
});

it("records an owner's read and grant without tokens, leaving the response for the caller", async () => {
  const client = new PGlite();
  const db = drizzle(client);
  const connection = { tenantId: randomUUID(), id: randomUUID() };
  const at = new Date("2026-10-13T12:00:00Z");
  const organisations = {
    Organisations: [
      {
        OrganisationID: "PRIVATE_ORG",
        Name: "PRIVATE_NAME",
        IsDemoCompany: true,
        CountryCode: "US",
        BaseCurrency: "USD",
      },
    ],
  };
  const fetcher = vi.fn(async (input: RequestInfo | URL) => {
    const body = String(input).includes("oauth")
      ? { access_token: "PRIVATE_TOKEN" }
      : organisations;
    return new Response(JSON.stringify(body), {
      headers: {
        "x-minlimit-remaining": "58",
        "x-daylimit-remaining": "4998",
      },
    });
  });
  try {
    await client.exec(
      `CREATE TABLE showcase_observations
         (tenant_id uuid, provider text, connection_id uuid, data jsonb,
          PRIMARY KEY (tenant_id, provider, connection_id))`,
    );
    await db.transaction(async (tx) => {
      const observed = observeReads(
        tx,
        connection,
        "xero",
        fetcher as typeof fetch,
        () => at,
      );
      await observed("https://identity.xero.com/connect/token", {
        method: "POST",
        body: "PRIVATE_REQUEST_SECRET",
      });
      const response = await observed(
        "https://api.xero.com/api.xro/2.0/Organisation",
        {
          headers: {
            authorization: "Bearer PRIVATE_TOKEN",
            "xero-tenant-id": "PRIVATE_ORG",
          },
        },
      );
      // The caller still gets the whole, unread response.
      const read = await response.json();
      expect(read.Organisations[0].Name).toBe("PRIVATE_NAME");
      await observeGrant(
        tx,
        connection,
        "xero",
        ["offline_access", "accounting.contacts.read", "PRIVATE_SCOPE"],
        at,
      );
    });

    const { rows } = await client.query<{ data: Record<string, unknown> }>(
      "SELECT data FROM showcase_observations",
    );
    expect(rows).toHaveLength(1);
    const [{ data }] = rows;
    expect(data.lastRefreshAt).toBe(at.toISOString());
    expect(data.lastReadAt).toBe(at.toISOString());
    expect(data.recordCount).toBe(1);
    expect(data.scopes).toEqual(["offline_access", "accounting.contacts.read"]);
    expect(JSON.stringify(rows)).not.toContain("PRIVATE");
    expect(fetcher).toHaveBeenCalledTimes(2);
  } finally {
    await client.close();
  }
});