207 lines · 7.9 KB · TypeScript
RawDownload
/* oxlint-disable executor/no-error-constructor, executor/no-try-catch-or-throw -- test fake throws on an unexpected query to catch wiring drift */ import { describe, expect, it } from "@effect/vitest";import { Effect } from "effect"; import { gcDeadDcrOAuthClientsMigration } from "../../scripts/code-migrations/gc-dead-dcr-oauth-clients";import type { CodeMigrationContext, CodeMigrationSql } from "../../scripts/code-migrations/runner"; // Cloud counterpart of the local libSQL GC migration test. A small in-memory// fake models the SQL shapes the migration issues (the oauth_client SELECT, the// single GROUPED connection-count query, and the DELETE/UPDATE mutations) so the// same shared decision matrix is proven end-to-end over the Postgres wiring. interface OAuthClientRow {  readonly tenant: string;  readonly owner: string;  readonly slug: string;  readonly grant: string | null;  readonly resource: string | null;  readonly origin_kind: string | null;  readonly origin_issuer: string | null;  readonly token_url: string | null;} const makeFake = (input: {  rows: OAuthClientRow[];  /** connection count keyed by `${tenant}|${owner}|${slug}`. */  readonly references: Record<string, number>;}) => {  const deletes: string[] = [];  const updates: { slug: string; issuer: string }[] = [];  const stamps: { slug: string; kind: string }[] = [];   const sql: CodeMigrationSql = {    unsafe: (query: string, params?: readonly unknown[]) => {      const q = query.trim();      if (q.startsWith("SELECT") && q.includes("FROM oauth_client")) {        return Promise.resolve(input.rows as never);      }      if (q.startsWith("SELECT") && q.includes("FROM connection")) {        // One grouped pass: emit a row per referenced (tenant, owner, slug).        const grouped = Object.entries(input.references).map(([composite, count]) => {          const [tenant, owner, slug] = composite.split("|");          return { tenant, oauth_client_owner: owner, oauth_client: slug, count };        });        return Promise.resolve(grouped as never);      }      if (q.startsWith("DELETE FROM oauth_client")) {        const [tenant, owner, slug] = params as [string, string, string];        input.rows = input.rows.filter(          (r) => !(r.tenant === tenant && r.owner === owner && r.slug === slug),        );        deletes.push(slug);        return Promise.resolve([] as never);      }      if (q.startsWith("UPDATE oauth_client")) {        // The slug is always the last positional param. The DCR-survivor UPDATE        // may set origin_issuer (an `origin_issuer = $N` clause with the issuer        // as its first param) and/or stamp origin_kind (a literal in the SQL).        const args = (params ?? []) as readonly string[];        const slug = args[args.length - 1];        if (q.includes("origin_kind = 'manual'")) {          stamps.push({ slug, kind: "manual" });        }        if (q.includes("origin_kind = 'dynamic_client_registration'")) {          stamps.push({ slug, kind: "dynamic_client_registration" });        }        if (q.includes("origin_issuer =")) {          updates.push({ slug, issuer: args[0] });        }        return Promise.resolve([] as never);      }      throw new Error(`unexpected query: ${q}`);    },  };   return { sql, deletes, updates, stamps, rowsRef: () => input.rows };}; const row = (over: Partial<OAuthClientRow> & { readonly slug: string }): OAuthClientRow => ({  tenant: "t1",  owner: "org",  slug: over.slug,  grant: over.grant ?? "authorization_code",  resource: over.resource ?? null,  origin_kind: over.origin_kind ?? null,  origin_issuer: over.origin_issuer ?? null,  token_url: over.token_url ?? "https://oauth.cloudflare.com/token",}); const runMigration = (  fake: ReturnType<typeof makeFake>,  dryRun: boolean,): Promise<{ readonly summary: string }> => {  const context: CodeMigrationContext = { sql: fake.sql, dryRun, log: () => {} };  return gcDeadDcrOAuthClientsMigration.run(context);}; describe("gcDeadDcrOAuthClientsMigration", () => {  it.effect("deletes orphaned DCR, keeps manual + referenced, backfills survivors", () =>    Effect.promise(async () => {      const fake = makeFake({        rows: [          row({            slug: "dcr-cloudflare-com",            resource: "https://cf.example/mcp",            origin_kind: "dynamic_client_registration",            origin_issuer: "https://cloudflare.com",          }),          row({ slug: "cloudflare-mcp-2", resource: "https://cf.example/mcp" }),          row({ slug: "cloudflare-mcp", resource: "https://cf.example/mcp" }),          row({            slug: "my-github-app",            token_url: "https://github.com/login/oauth/access_token",            origin_kind: "manual",          }),        ],        references: { "t1|org|cloudflare-mcp": 1 },      });       const result = await runMigration(fake, false);       expect(fake.deletes.sort()).toEqual(["cloudflare-mcp-2", "dcr-cloudflare-com"]);      expect(fake.updates).toEqual([{ slug: "cloudflare-mcp", issuer: "https://cloudflare.com" }]);      // The surviving legacy DCR row gets an explicit DCR stamp; the already      // explicitly-stamped manual app is never restamped.      expect(fake.stamps).toEqual([        { slug: "cloudflare-mcp", kind: "dynamic_client_registration" },      ]);      expect(        fake          .rowsRef()          .map((r) => r.slug)          .sort(),      ).toEqual(["cloudflare-mcp", "my-github-app"]);      expect(result.summary).toContain("deleted 2 orphaned DCR client(s)");      expect(result.summary).toContain("backfilled 1 of 1 referenced DCR client(s)");      expect(result.summary).toContain("stamped 1 legacy row(s) (1 dcr, 0 manual)");    }),  );   it.effect("stamps a legacy null-origin manual survivor as manual with no issuer", () =>    Effect.promise(async () => {      const fake = makeFake({        rows: [          // Legacy manual row: null origin_kind, auth-code, NO resource → manual.          row({            slug: "legacy-manual",            token_url: "https://github.com/login/oauth/access_token",            resource: null,          }),        ],        references: {},      });       const result = await runMigration(fake, false);       expect(fake.deletes).toEqual([]);      expect(fake.updates).toEqual([]);      expect(fake.stamps).toEqual([{ slug: "legacy-manual", kind: "manual" }]);      expect(fake.rowsRef().map((r) => r.slug)).toEqual(["legacy-manual"]);      expect(result.summary).toContain("stamped 1 legacy row(s) (0 dcr, 1 manual)");    }),  );   it.effect("dry run mutates nothing but reports the plan", () =>    Effect.promise(async () => {      const fake = makeFake({        rows: [          row({ slug: "cloudflare-mcp-2", resource: "https://cf.example/mcp" }),          row({ slug: "cloudflare-mcp", resource: "https://cf.example/mcp" }),        ],        references: { "t1|org|cloudflare-mcp": 1 },      });       const result = await runMigration(fake, true);       expect(fake.deletes).toEqual([]);      expect(fake.updates).toEqual([]);      expect(fake.stamps).toEqual([]);      expect(fake.rowsRef()).toHaveLength(2);      expect(result.summary).toContain("would delete 1 orphaned DCR client(s)");      expect(result.summary).toContain("would backfill 1 of 1 referenced DCR client(s)");      // The referenced legacy DCR survivor WOULD be stamped, counted but not      // written.      expect(result.summary).toContain("would stamp 1 legacy row(s) (1 dcr, 0 manual)");    }),  );   it.effect("never deletes a manual app even when orphaned and MCP-shaped", () =>    Effect.promise(async () => {      const fake = makeFake({        rows: [          row({            slug: "cloudflare-mcp",            resource: "https://cf.example/mcp",            origin_kind: "manual",          }),        ],        references: {},      });      await runMigration(fake, false);      expect(fake.deletes).toEqual([]);      expect(fake.rowsRef().map((r) => r.slug)).toEqual(["cloudflare-mcp"]);    }),  );});