From 9c23cd7b04815d34343d23e8198cf15ed4ec864a Mon Sep 17 00:00:00 2001 From: avallete Date: Fri, 26 Jun 2026 12:10:16 +0200 Subject: [PATCH 1/5] test(cli): deepen live JSON assertions for orgs/projects list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `--output-format json` live tests only asserted `JSON.parse` succeeded, which proves "command exited and emitted JSON" but not platform correctness. Assert real shape + state instead: - orgs list: `organizations` is a non-empty array of `{ id, slug, name }` (the live token always belongs to at least one org). - projects list: `projects` is an array, and when the runner provisioned a project (SUPABASE_LIVE_PROJECT_REF set) that ref appears in the listing — proving the JSON reflects real platform state. Validated against a real supabox stack: 8 passed (the project-ref containment assertion exercised with a provisioned project). Co-Authored-By: Claude Opus 4.8 --- .../commands/orgs/list/list.live.test.ts | 35 +++++++++++-------- .../commands/projects/list/list.live.test.ts | 21 ++++++++--- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/apps/cli/src/legacy/commands/orgs/list/list.live.test.ts b/apps/cli/src/legacy/commands/orgs/list/list.live.test.ts index 515e8a855d..e01f00ee41 100644 --- a/apps/cli/src/legacy/commands/orgs/list/list.live.test.ts +++ b/apps/cli/src/legacy/commands/orgs/list/list.live.test.ts @@ -22,21 +22,26 @@ describeLive("supabase orgs list (live)", () => { }, ); - test( - "emits machine-readable JSON with --output-format json", - { timeout: LIVE_TIMEOUT_MS }, - async () => { - const { exitCode, stdout } = await runSupabaseLive([ - "orgs", - "list", - "--output-format", - "json", - ]); - expect(exitCode).toBe(0); - // stdout must be payload-only valid JSON in json mode (no spinner/log noise). - expect(() => JSON.parse(stdout)).not.toThrow(); - }, - ); + test("emits organizations as machine-readable JSON", { timeout: LIVE_TIMEOUT_MS }, async () => { + const { exitCode, stdout } = await runSupabaseLive(["orgs", "list", "--output-format", "json"]); + expect(exitCode).toBe(0); + // Payload-only JSON (no spinner/log noise) shaped like the Go CLI's + // { organizations: [{ id, slug, name }], message }. The live token always + // belongs to at least one org (supabox seeds one), so assert a real row — + // not merely that the output parses. + const parsed = JSON.parse(stdout) as { + organizations: Array<{ id: string; slug: string; name: string }>; + }; + expect(Array.isArray(parsed.organizations)).toBe(true); + expect(parsed.organizations.length).toBeGreaterThan(0); + expect(parsed.organizations[0]).toEqual( + expect.objectContaining({ + id: expect.any(String), + slug: expect.any(String), + name: expect.any(String), + }), + ); + }); // Negative path: a bad token must round-trip to the real Management API, come // back 401, and surface as a non-zero exit with the upstream "Unauthorized" diff --git a/apps/cli/src/legacy/commands/projects/list/list.live.test.ts b/apps/cli/src/legacy/commands/projects/list/list.live.test.ts index 8c4ca20f33..a4f80c9d5c 100644 --- a/apps/cli/src/legacy/commands/projects/list/list.live.test.ts +++ b/apps/cli/src/legacy/commands/projects/list/list.live.test.ts @@ -1,6 +1,10 @@ import { expect, test } from "vitest"; -import { describeLive, runSupabaseLive } from "../../../../../tests/helpers/live.ts"; +import { + describeLive, + liveProjectRef, + runSupabaseLive, +} from "../../../../../tests/helpers/live.ts"; const LIVE_TIMEOUT_MS = 60_000; @@ -16,7 +20,7 @@ describeLive("supabase projects list (live)", () => { }); test( - "emits machine-readable JSON with --output-format json", + "emits projects as machine-readable JSON, including the provisioned project", { timeout: LIVE_TIMEOUT_MS }, async () => { const { exitCode, stdout } = await runSupabaseLive([ @@ -26,8 +30,17 @@ describeLive("supabase projects list (live)", () => { "json", ]); expect(exitCode).toBe(0); - // stdout must be payload-only valid JSON in json mode (no spinner/log noise). - expect(() => JSON.parse(stdout)).not.toThrow(); + // Payload-only JSON shaped like { projects: [{ id, ref, name, status, … }], message }. + const parsed = JSON.parse(stdout) as { + projects: Array<{ id: string; ref: string; name: string }>; + }; + expect(Array.isArray(parsed.projects)).toBe(true); + const ref = liveProjectRef(); + if (ref) { + // When the runner provisioned a project, it must appear in the listing — + // proves the JSON reflects real platform state, not just valid syntax. + expect(parsed.projects.map((project) => project.ref)).toContain(ref); + } }, ); }); From 40c50f6b4b846931f192a5a18cbec23b9d7b0858 Mon Sep 17 00:00:00 2001 From: avallete Date: Fri, 26 Jun 2026 15:34:33 +0200 Subject: [PATCH 2/5] test(cli): add secrets list live coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the live suite beyond orgs/projects/functions/branches with the secrets resource — a new Management API control-plane surface. - describeLiveProject: lists secrets for the provisioned project (exit 0, not Unauthorized) and asserts `{ secrets: [...] }` JSON envelope shape. - describeLive: a valid token with an unknown --project-ref round-trips to the live Management API, returns 404, and exits non-zero — exercising the request path + error mapping even on a control-plane-only stack. Mirrors the functions/list live pattern. Secrets are edge-function env vars served by the control plane, so it runs against a freshly provisioned project regardless of data-plane health (empty list is a valid payload). Co-Authored-By: Claude Opus 4.8 --- .../commands/secrets/list/list.live.test.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 apps/cli/src/legacy/commands/secrets/list/list.live.test.ts diff --git a/apps/cli/src/legacy/commands/secrets/list/list.live.test.ts b/apps/cli/src/legacy/commands/secrets/list/list.live.test.ts new file mode 100644 index 0000000000..3bd0f61505 --- /dev/null +++ b/apps/cli/src/legacy/commands/secrets/list/list.live.test.ts @@ -0,0 +1,70 @@ +import { expect, test } from "vitest"; + +import { + describeLive, + describeLiveProject, + requireLiveProjectRef, + runSupabaseLive, +} from "../../../../../tests/helpers/live.ts"; + +const LIVE_TIMEOUT_MS = 120_000; + +// Project-scoped read-only scenario. Skipped unless SUPABASE_LIVE_PROJECT_REF is +// set — i.e. a project has been provisioned on the stack (the cli-e2e-ci runner +// does this; a control-plane-only stack, like local macOS, skips it). +// +// Secrets are edge-function env vars served by the Management API control plane +// (no project DB needed), so this is safe to run against a freshly provisioned +// project regardless of data-plane health — a new project simply has an empty +// secrets list, which is still a valid `{ secrets: [] }` payload. +describeLiveProject("supabase secrets list (live)", () => { + test("lists secrets for the project", { timeout: LIVE_TIMEOUT_MS }, async () => { + const ref = requireLiveProjectRef(); + const { exitCode, stdout, stderr } = await runSupabaseLive([ + "secrets", + "list", + "--project-ref", + ref, + ]); + expect(`${stdout}${stderr}`).not.toContain("Unauthorized"); + expect(exitCode).toBe(0); + }); + + test("emits secrets as machine-readable JSON", { timeout: LIVE_TIMEOUT_MS }, async () => { + const ref = requireLiveProjectRef(); + const { exitCode, stdout } = await runSupabaseLive([ + "secrets", + "list", + "--project-ref", + ref, + "--output-format", + "json", + ]); + expect(exitCode).toBe(0); + // Payload-only JSON shaped like { secrets: [{ name, value }], message }. + // Assert the envelope shape rather than specific rows — a fresh project may + // legitimately have zero secrets, but the array must always be present. + const parsed = JSON.parse(stdout) as { secrets: Array<{ name: string; value: string }> }; + expect(Array.isArray(parsed.secrets)).toBe(true); + }); +}); + +// Project-scoped error path that needs NO provisioned project: a valid token +// with an unknown `--project-ref` must reach the live Management API, come back +// 404, and surface as a non-zero exit (not a crash, not "Unauthorized"). Runs +// under `describeLive` so it exercises the request path + error mapping even on +// a control-plane-only stack. +describeLive("supabase secrets list — unknown project (live)", () => { + test("fails with a 404 for an unknown project ref", { timeout: LIVE_TIMEOUT_MS }, async () => { + const { exitCode, stdout, stderr } = await runSupabaseLive([ + "secrets", + "list", + "--project-ref", + "a".repeat(20), // well-formed (20 lowercase chars) but nonexistent ref + ]); + const out = `${stdout}${stderr}`; + expect(exitCode).not.toBe(0); + expect(out).not.toContain("Unauthorized"); + expect(out).toContain("404"); + }); +}); From f09bdfcc564f7978f16175d4ef1f96a125433967 Mon Sep 17 00:00:00 2001 From: avallete Date: Fri, 26 Jun 2026 15:53:34 +0200 Subject: [PATCH 3/5] test(cli): cover read-only command surface with live tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Broadens the live suite across the rest of the read-only command surface so the supabox-backed run exercises each command's auth + request path + output shaping against the real platform. Control-plane, project-scoped (success + unknown-ref 404): - backups list, network-bans get, network-restrictions get, postgres-config get, ssl-enforcement get, vanity-subdomains get, sso list — assert the JSON envelope shape (array key or single object) plus a 404 negative. Control-plane, 404-negative only (no stable success on a bare project, or a resource id is required): - domains get (custom hostname unset), branches get (needs a branch), sso show (needs a provider id). Data-plane, gated by the new `describeLiveDb` / SUPABASE_LIVE_DB_URL (connect to the project Postgres via --db-url, skipped until the runner resolves the pooler URL): - db dump, db advisors, migration list. Adds liveDbUrl()/requireLiveDbUrl() + describeLiveDb to the live helpers. All 18 live files collect and skip cleanly with no live env configured. Co-Authored-By: Claude Opus 4.8 --- .../commands/backups/list/list.live.test.ts | 66 +++++++++++++++++ .../commands/branches/get/get.live.test.ts | 29 ++++++++ .../db/advisors/advisors.live.test.ts | 36 ++++++++++ .../legacy/commands/db/dump/dump.live.test.ts | 25 +++++++ .../commands/domains/get/get.live.test.ts | 28 ++++++++ .../commands/migration/list/list.live.test.ts | 30 ++++++++ .../network-bans/get/get.live.test.ts | 61 ++++++++++++++++ .../network-restrictions/get/get.live.test.ts | 66 +++++++++++++++++ .../postgres-config/get/get.live.test.ts | 66 +++++++++++++++++ .../ssl-enforcement/get/get.live.test.ts | 70 +++++++++++++++++++ .../commands/sso/list/list.live.test.ts | 60 ++++++++++++++++ .../commands/sso/show/show.live.test.ts | 29 ++++++++ .../vanity-subdomains/get/get.live.test.ts | 70 +++++++++++++++++++ apps/cli/tests/helpers/live-env.ts | 31 ++++++++ apps/cli/tests/helpers/live.ts | 13 ++++ 15 files changed, 680 insertions(+) create mode 100644 apps/cli/src/legacy/commands/backups/list/list.live.test.ts create mode 100644 apps/cli/src/legacy/commands/branches/get/get.live.test.ts create mode 100644 apps/cli/src/legacy/commands/db/advisors/advisors.live.test.ts create mode 100644 apps/cli/src/legacy/commands/db/dump/dump.live.test.ts create mode 100644 apps/cli/src/legacy/commands/domains/get/get.live.test.ts create mode 100644 apps/cli/src/legacy/commands/migration/list/list.live.test.ts create mode 100644 apps/cli/src/legacy/commands/network-bans/get/get.live.test.ts create mode 100644 apps/cli/src/legacy/commands/network-restrictions/get/get.live.test.ts create mode 100644 apps/cli/src/legacy/commands/postgres-config/get/get.live.test.ts create mode 100644 apps/cli/src/legacy/commands/ssl-enforcement/get/get.live.test.ts create mode 100644 apps/cli/src/legacy/commands/sso/list/list.live.test.ts create mode 100644 apps/cli/src/legacy/commands/sso/show/show.live.test.ts create mode 100644 apps/cli/src/legacy/commands/vanity-subdomains/get/get.live.test.ts diff --git a/apps/cli/src/legacy/commands/backups/list/list.live.test.ts b/apps/cli/src/legacy/commands/backups/list/list.live.test.ts new file mode 100644 index 0000000000..d3a4d8c659 --- /dev/null +++ b/apps/cli/src/legacy/commands/backups/list/list.live.test.ts @@ -0,0 +1,66 @@ +import { expect, test } from "vitest"; + +import { + describeLive, + describeLiveProject, + requireLiveProjectRef, + runSupabaseLive, +} from "../../../../../tests/helpers/live.ts"; + +const LIVE_TIMEOUT_MS = 120_000; + +// Project-scoped read-only scenario. Skipped unless SUPABASE_LIVE_PROJECT_REF is +// set — i.e. a project has been provisioned on the stack (the cli-e2e-ci runner +// does this; a control-plane-only stack, like local macOS, skips it). +// +// Backups are listed via the Management API control plane (no project DB query), +// so this runs against a freshly provisioned project regardless of data-plane +// health — a new project simply has an empty backups list. +describeLiveProject("supabase backups list (live)", () => { + test("lists backups for the project", { timeout: LIVE_TIMEOUT_MS }, async () => { + const ref = requireLiveProjectRef(); + const { exitCode, stdout, stderr } = await runSupabaseLive([ + "backups", + "list", + "--project-ref", + ref, + ]); + expect(`${stdout}${stderr}`).not.toContain("Unauthorized"); + expect(exitCode).toBe(0); + }); + + test("emits backups as machine-readable JSON", { timeout: LIVE_TIMEOUT_MS }, async () => { + const ref = requireLiveProjectRef(); + const { exitCode, stdout } = await runSupabaseLive([ + "backups", + "list", + "--project-ref", + ref, + "--output-format", + "json", + ]); + expect(exitCode).toBe(0); + // Payload-only JSON shaped like { backups: [...], ... }. A fresh project may + // have zero backups, but the array must always be present. + const parsed = JSON.parse(stdout) as { backups: unknown[] }; + expect(Array.isArray(parsed.backups)).toBe(true); + }); +}); + +// Project-scoped error path needing NO provisioned project: a valid token with +// an unknown --project-ref must reach the live Management API, come back 404, +// and exit non-zero (not a crash, not "Unauthorized"). +describeLive("supabase backups list — unknown project (live)", () => { + test("fails with a 404 for an unknown project ref", { timeout: LIVE_TIMEOUT_MS }, async () => { + const { exitCode, stdout, stderr } = await runSupabaseLive([ + "backups", + "list", + "--project-ref", + "a".repeat(20), + ]); + const out = `${stdout}${stderr}`; + expect(exitCode).not.toBe(0); + expect(out).not.toContain("Unauthorized"); + expect(out).toContain("404"); + }); +}); diff --git a/apps/cli/src/legacy/commands/branches/get/get.live.test.ts b/apps/cli/src/legacy/commands/branches/get/get.live.test.ts new file mode 100644 index 0000000000..353f49cdac --- /dev/null +++ b/apps/cli/src/legacy/commands/branches/get/get.live.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from "vitest"; + +import { describeLive, runSupabaseLive } from "../../../../../tests/helpers/live.ts"; + +const LIVE_TIMEOUT_MS = 120_000; + +// `branches get` resolves a branch within a project, so a stable success path +// needs a project that has branching enabled and a known branch — not +// guaranteed on a freshly provisioned project (branch lifecycle coverage is +// tracked separately in CLI-1834). +// +// The portable live signal is the request path + error mapping: a valid token +// with an unknown --project-ref must reach the live Management API, come back +// 404, and exit non-zero. Runs under `describeLive` so it needs no provisioned +// project. +describeLive("supabase branches get — unknown project (live)", () => { + test("fails with a 404 for an unknown project ref", { timeout: LIVE_TIMEOUT_MS }, async () => { + const { exitCode, stdout, stderr } = await runSupabaseLive([ + "branches", + "get", + "--project-ref", + "a".repeat(20), // well-formed (20 lowercase chars) but nonexistent ref + ]); + const out = `${stdout}${stderr}`; + expect(exitCode).not.toBe(0); + expect(out).not.toContain("Unauthorized"); + expect(out).toContain("404"); + }); +}); diff --git a/apps/cli/src/legacy/commands/db/advisors/advisors.live.test.ts b/apps/cli/src/legacy/commands/db/advisors/advisors.live.test.ts new file mode 100644 index 0000000000..65c292fb96 --- /dev/null +++ b/apps/cli/src/legacy/commands/db/advisors/advisors.live.test.ts @@ -0,0 +1,36 @@ +import { expect, test } from "vitest"; + +import { + describeLiveDb, + requireLiveDbUrl, + runSupabaseLive, +} from "../../../../../tests/helpers/live.ts"; + +const LIVE_TIMEOUT_MS = 180_000; + +// Data-plane scenario: `db advisors` runs lint queries against the project +// Postgres via --db-url, so it is gated by `describeLiveDb` — it runs only when +// SUPABASE_LIVE_DB_URL is set (the cli-e2e-ci runner resolves the provisioned +// project's pooler URL). Skipped otherwise. +describeLiveDb("supabase db advisors (live)", () => { + test("emits advisor results as machine-readable JSON", { timeout: LIVE_TIMEOUT_MS }, async () => { + const dbUrl = requireLiveDbUrl(); + // `--fail-on none` keeps the exit code 0 regardless of which advisories the + // project happens to have, so the test asserts the command path, not the + // project's current lint state. + const { exitCode, stdout } = await runSupabaseLive([ + "db", + "advisors", + "--db-url", + dbUrl, + "--fail-on", + "none", + "--output-format", + "json", + ]); + expect(exitCode).toBe(0); + // Payload-only JSON shaped like { results: [...] }. + const parsed = JSON.parse(stdout) as { results: unknown[] }; + expect(Array.isArray(parsed.results)).toBe(true); + }); +}); diff --git a/apps/cli/src/legacy/commands/db/dump/dump.live.test.ts b/apps/cli/src/legacy/commands/db/dump/dump.live.test.ts new file mode 100644 index 0000000000..43fd03f6e7 --- /dev/null +++ b/apps/cli/src/legacy/commands/db/dump/dump.live.test.ts @@ -0,0 +1,25 @@ +import { expect, test } from "vitest"; + +import { + describeLiveDb, + requireLiveDbUrl, + runSupabaseLive, +} from "../../../../../tests/helpers/live.ts"; + +const LIVE_TIMEOUT_MS = 180_000; + +// Data-plane scenario: `db dump` connects to the project Postgres directly via +// --db-url (not the Management API), so it is gated by `describeLiveDb` — it +// runs only when SUPABASE_LIVE_DB_URL is set (the cli-e2e-ci runner resolves the +// provisioned project's pooler URL). Skipped otherwise. +describeLiveDb("supabase db dump (live)", () => { + test("dumps the project schema to stdout", { timeout: LIVE_TIMEOUT_MS }, async () => { + const dbUrl = requireLiveDbUrl(); + const { exitCode, stdout, stderr } = await runSupabaseLive(["db", "dump", "--db-url", dbUrl]); + expect(stderr).not.toContain("Unauthorized"); + expect(exitCode).toBe(0); + // A real pg_dump of a Supabase project emits SQL DDL to stdout; assert it is + // non-empty rather than pinning an exact header that varies by pg version. + expect(stdout.trim().length).toBeGreaterThan(0); + }); +}); diff --git a/apps/cli/src/legacy/commands/domains/get/get.live.test.ts b/apps/cli/src/legacy/commands/domains/get/get.live.test.ts new file mode 100644 index 0000000000..82799a33ad --- /dev/null +++ b/apps/cli/src/legacy/commands/domains/get/get.live.test.ts @@ -0,0 +1,28 @@ +import { expect, test } from "vitest"; + +import { describeLive, runSupabaseLive } from "../../../../../tests/helpers/live.ts"; + +const LIVE_TIMEOUT_MS = 120_000; + +// `domains get` reads the custom-hostname config, which the Management API only +// returns once a custom hostname has been configured — a freshly provisioned +// project legitimately has none, so there is no stable success path to assert. +// +// The valuable live signal is the request path + error mapping: a valid token +// with an unknown --project-ref must reach the live Management API, come back +// 404, and exit non-zero (not a crash, not "Unauthorized"). Runs under +// `describeLive` so it needs no provisioned project. +describeLive("supabase domains get — unknown project (live)", () => { + test("fails with a 404 for an unknown project ref", { timeout: LIVE_TIMEOUT_MS }, async () => { + const { exitCode, stdout, stderr } = await runSupabaseLive([ + "domains", + "get", + "--project-ref", + "a".repeat(20), // well-formed (20 lowercase chars) but nonexistent ref + ]); + const out = `${stdout}${stderr}`; + expect(exitCode).not.toBe(0); + expect(out).not.toContain("Unauthorized"); + expect(out).toContain("404"); + }); +}); diff --git a/apps/cli/src/legacy/commands/migration/list/list.live.test.ts b/apps/cli/src/legacy/commands/migration/list/list.live.test.ts new file mode 100644 index 0000000000..40fd558c2b --- /dev/null +++ b/apps/cli/src/legacy/commands/migration/list/list.live.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from "vitest"; + +import { + describeLiveDb, + requireLiveDbUrl, + runSupabaseLive, +} from "../../../../../tests/helpers/live.ts"; + +const LIVE_TIMEOUT_MS = 180_000; + +// Data-plane scenario: `migration list` reads the remote migration history table +// over a direct Postgres connection via --db-url (not the Management API), so it +// is gated by `describeLiveDb` — it runs only when SUPABASE_LIVE_DB_URL is set +// (the cli-e2e-ci runner resolves the provisioned project's pooler URL). Skipped +// otherwise. +describeLiveDb("supabase migration list (live)", () => { + test("lists remote migrations for the project", { timeout: LIVE_TIMEOUT_MS }, async () => { + const dbUrl = requireLiveDbUrl(); + const { exitCode, stdout, stderr } = await runSupabaseLive([ + "migration", + "list", + "--db-url", + dbUrl, + ]); + expect(`${stdout}${stderr}`).not.toContain("Unauthorized"); + // A freshly provisioned project may have no applied migrations; the command + // still exits 0 and prints the (possibly empty) history table. + expect(exitCode).toBe(0); + }); +}); diff --git a/apps/cli/src/legacy/commands/network-bans/get/get.live.test.ts b/apps/cli/src/legacy/commands/network-bans/get/get.live.test.ts new file mode 100644 index 0000000000..608fc5ae0a --- /dev/null +++ b/apps/cli/src/legacy/commands/network-bans/get/get.live.test.ts @@ -0,0 +1,61 @@ +import { expect, test } from "vitest"; + +import { + describeLive, + describeLiveProject, + requireLiveProjectRef, + runSupabaseLive, +} from "../../../../../tests/helpers/live.ts"; + +const LIVE_TIMEOUT_MS = 120_000; + +// Project-scoped read-only scenario. Skipped unless SUPABASE_LIVE_PROJECT_REF is +// set (the cli-e2e-ci runner provisions a project; a control-plane-only stack +// skips it). Reads the project's network bans via the Management API control +// plane — a fresh project has none, which is a valid empty list. +describeLiveProject("supabase network-bans get (live)", () => { + test("gets network bans for the project", { timeout: LIVE_TIMEOUT_MS }, async () => { + const ref = requireLiveProjectRef(); + const { exitCode, stdout, stderr } = await runSupabaseLive([ + "network-bans", + "get", + "--project-ref", + ref, + ]); + expect(`${stdout}${stderr}`).not.toContain("Unauthorized"); + expect(exitCode).toBe(0); + }); + + test("emits network bans as machine-readable JSON", { timeout: LIVE_TIMEOUT_MS }, async () => { + const ref = requireLiveProjectRef(); + const { exitCode, stdout } = await runSupabaseLive([ + "network-bans", + "get", + "--project-ref", + ref, + "--output-format", + "json", + ]); + expect(exitCode).toBe(0); + // Payload-only JSON shaped like { banned_ipv4_addresses: [...] }. + const parsed = JSON.parse(stdout) as { banned_ipv4_addresses: string[] }; + expect(Array.isArray(parsed.banned_ipv4_addresses)).toBe(true); + }); +}); + +// Error path needing NO provisioned project: unknown --project-ref → 404 → exit +// non-zero, exercising the request path + error mapping on a control-plane stack. +describeLive("supabase network-bans get — unknown project (live)", () => { + test("fails with a 404 for an unknown project ref", { timeout: LIVE_TIMEOUT_MS }, async () => { + const { exitCode, stdout, stderr } = await runSupabaseLive([ + "network-bans", + "get", + "--project-ref", + "a".repeat(20), + ]); + const out = `${stdout}${stderr}`; + expect(exitCode).not.toBe(0); + expect(out).not.toContain("Unauthorized"); + expect(out).toContain("404"); + }); +}); diff --git a/apps/cli/src/legacy/commands/network-restrictions/get/get.live.test.ts b/apps/cli/src/legacy/commands/network-restrictions/get/get.live.test.ts new file mode 100644 index 0000000000..b980f6187a --- /dev/null +++ b/apps/cli/src/legacy/commands/network-restrictions/get/get.live.test.ts @@ -0,0 +1,66 @@ +import { expect, test } from "vitest"; + +import { + describeLive, + describeLiveProject, + requireLiveProjectRef, + runSupabaseLive, +} from "../../../../../tests/helpers/live.ts"; + +const LIVE_TIMEOUT_MS = 120_000; + +// Project-scoped read-only scenario. Skipped unless SUPABASE_LIVE_PROJECT_REF is +// set (the cli-e2e-ci runner provisions a project; a control-plane-only stack +// skips it). Reads the project's network restrictions config via the Management +// API control plane — every project has a config object. +describeLiveProject("supabase network-restrictions get (live)", () => { + test("gets network restrictions for the project", { timeout: LIVE_TIMEOUT_MS }, async () => { + const ref = requireLiveProjectRef(); + const { exitCode, stdout, stderr } = await runSupabaseLive([ + "network-restrictions", + "get", + "--project-ref", + ref, + ]); + expect(`${stdout}${stderr}`).not.toContain("Unauthorized"); + expect(exitCode).toBe(0); + }); + + test( + "emits network restrictions as machine-readable JSON", + { timeout: LIVE_TIMEOUT_MS }, + async () => { + const ref = requireLiveProjectRef(); + const { exitCode, stdout } = await runSupabaseLive([ + "network-restrictions", + "get", + "--project-ref", + ref, + "--output-format", + "json", + ]); + expect(exitCode).toBe(0); + // Payload-only JSON: the restrictions config is a single object, not an array. + const parsed: unknown = JSON.parse(stdout); + expect(typeof parsed).toBe("object"); + expect(parsed).not.toBeNull(); + expect(Array.isArray(parsed)).toBe(false); + }, + ); +}); + +// Error path needing NO provisioned project: unknown --project-ref → 404. +describeLive("supabase network-restrictions get — unknown project (live)", () => { + test("fails with a 404 for an unknown project ref", { timeout: LIVE_TIMEOUT_MS }, async () => { + const { exitCode, stdout, stderr } = await runSupabaseLive([ + "network-restrictions", + "get", + "--project-ref", + "a".repeat(20), + ]); + const out = `${stdout}${stderr}`; + expect(exitCode).not.toBe(0); + expect(out).not.toContain("Unauthorized"); + expect(out).toContain("404"); + }); +}); diff --git a/apps/cli/src/legacy/commands/postgres-config/get/get.live.test.ts b/apps/cli/src/legacy/commands/postgres-config/get/get.live.test.ts new file mode 100644 index 0000000000..d56d5d4e0d --- /dev/null +++ b/apps/cli/src/legacy/commands/postgres-config/get/get.live.test.ts @@ -0,0 +1,66 @@ +import { expect, test } from "vitest"; + +import { + describeLive, + describeLiveProject, + requireLiveProjectRef, + runSupabaseLive, +} from "../../../../../tests/helpers/live.ts"; + +const LIVE_TIMEOUT_MS = 120_000; + +// Project-scoped read-only scenario. Skipped unless SUPABASE_LIVE_PROJECT_REF is +// set (the cli-e2e-ci runner provisions a project; a control-plane-only stack +// skips it). Reads the project's Postgres config via the Management API control +// plane — every project exposes a config object regardless of data-plane health. +describeLiveProject("supabase postgres-config get (live)", () => { + test("gets the Postgres config for the project", { timeout: LIVE_TIMEOUT_MS }, async () => { + const ref = requireLiveProjectRef(); + const { exitCode, stdout, stderr } = await runSupabaseLive([ + "postgres-config", + "get", + "--project-ref", + ref, + ]); + expect(`${stdout}${stderr}`).not.toContain("Unauthorized"); + expect(exitCode).toBe(0); + }); + + test( + "emits the Postgres config as machine-readable JSON", + { timeout: LIVE_TIMEOUT_MS }, + async () => { + const ref = requireLiveProjectRef(); + const { exitCode, stdout } = await runSupabaseLive([ + "postgres-config", + "get", + "--project-ref", + ref, + "--output-format", + "json", + ]); + expect(exitCode).toBe(0); + // Payload-only JSON: the Postgres config is a single object, not an array. + const parsed: unknown = JSON.parse(stdout); + expect(typeof parsed).toBe("object"); + expect(parsed).not.toBeNull(); + expect(Array.isArray(parsed)).toBe(false); + }, + ); +}); + +// Error path needing NO provisioned project: unknown --project-ref → 404. +describeLive("supabase postgres-config get — unknown project (live)", () => { + test("fails with a 404 for an unknown project ref", { timeout: LIVE_TIMEOUT_MS }, async () => { + const { exitCode, stdout, stderr } = await runSupabaseLive([ + "postgres-config", + "get", + "--project-ref", + "a".repeat(20), + ]); + const out = `${stdout}${stderr}`; + expect(exitCode).not.toBe(0); + expect(out).not.toContain("Unauthorized"); + expect(out).toContain("404"); + }); +}); diff --git a/apps/cli/src/legacy/commands/ssl-enforcement/get/get.live.test.ts b/apps/cli/src/legacy/commands/ssl-enforcement/get/get.live.test.ts new file mode 100644 index 0000000000..26440d107e --- /dev/null +++ b/apps/cli/src/legacy/commands/ssl-enforcement/get/get.live.test.ts @@ -0,0 +1,70 @@ +import { expect, test } from "vitest"; + +import { + describeLive, + describeLiveProject, + requireLiveProjectRef, + runSupabaseLive, +} from "../../../../../tests/helpers/live.ts"; + +const LIVE_TIMEOUT_MS = 120_000; + +// Project-scoped read-only scenario. Skipped unless SUPABASE_LIVE_PROJECT_REF is +// set (the cli-e2e-ci runner provisions a project; a control-plane-only stack +// skips it). Reads the project's SSL enforcement config via the Management API +// control plane — every project exposes a config object. +describeLiveProject("supabase ssl-enforcement get (live)", () => { + test( + "gets the SSL enforcement config for the project", + { timeout: LIVE_TIMEOUT_MS }, + async () => { + const ref = requireLiveProjectRef(); + const { exitCode, stdout, stderr } = await runSupabaseLive([ + "ssl-enforcement", + "get", + "--project-ref", + ref, + ]); + expect(`${stdout}${stderr}`).not.toContain("Unauthorized"); + expect(exitCode).toBe(0); + }, + ); + + test( + "emits the SSL enforcement config as machine-readable JSON", + { timeout: LIVE_TIMEOUT_MS }, + async () => { + const ref = requireLiveProjectRef(); + const { exitCode, stdout } = await runSupabaseLive([ + "ssl-enforcement", + "get", + "--project-ref", + ref, + "--output-format", + "json", + ]); + expect(exitCode).toBe(0); + // Payload-only JSON: the SSL enforcement config is a single object. + const parsed: unknown = JSON.parse(stdout); + expect(typeof parsed).toBe("object"); + expect(parsed).not.toBeNull(); + expect(Array.isArray(parsed)).toBe(false); + }, + ); +}); + +// Error path needing NO provisioned project: unknown --project-ref → 404. +describeLive("supabase ssl-enforcement get — unknown project (live)", () => { + test("fails with a 404 for an unknown project ref", { timeout: LIVE_TIMEOUT_MS }, async () => { + const { exitCode, stdout, stderr } = await runSupabaseLive([ + "ssl-enforcement", + "get", + "--project-ref", + "a".repeat(20), + ]); + const out = `${stdout}${stderr}`; + expect(exitCode).not.toBe(0); + expect(out).not.toContain("Unauthorized"); + expect(out).toContain("404"); + }); +}); diff --git a/apps/cli/src/legacy/commands/sso/list/list.live.test.ts b/apps/cli/src/legacy/commands/sso/list/list.live.test.ts new file mode 100644 index 0000000000..020ca8b287 --- /dev/null +++ b/apps/cli/src/legacy/commands/sso/list/list.live.test.ts @@ -0,0 +1,60 @@ +import { expect, test } from "vitest"; + +import { + describeLive, + describeLiveProject, + requireLiveProjectRef, + runSupabaseLive, +} from "../../../../../tests/helpers/live.ts"; + +const LIVE_TIMEOUT_MS = 120_000; + +// Project-scoped read-only scenario. Skipped unless SUPABASE_LIVE_PROJECT_REF is +// set (the cli-e2e-ci runner provisions a project; a control-plane-only stack +// skips it). Lists the project's SSO identity providers via the Management API +// control plane — a fresh project has none, which is a valid empty list. +describeLiveProject("supabase sso list (live)", () => { + test("lists SSO providers for the project", { timeout: LIVE_TIMEOUT_MS }, async () => { + const ref = requireLiveProjectRef(); + const { exitCode, stdout, stderr } = await runSupabaseLive([ + "sso", + "list", + "--project-ref", + ref, + ]); + expect(`${stdout}${stderr}`).not.toContain("Unauthorized"); + expect(exitCode).toBe(0); + }); + + test("emits SSO providers as machine-readable JSON", { timeout: LIVE_TIMEOUT_MS }, async () => { + const ref = requireLiveProjectRef(); + const { exitCode, stdout } = await runSupabaseLive([ + "sso", + "list", + "--project-ref", + ref, + "--output-format", + "json", + ]); + expect(exitCode).toBe(0); + // Payload-only JSON shaped like { providers: [...] }. + const parsed = JSON.parse(stdout) as { providers: unknown[] }; + expect(Array.isArray(parsed.providers)).toBe(true); + }); +}); + +// Error path needing NO provisioned project: unknown --project-ref → 404. +describeLive("supabase sso list — unknown project (live)", () => { + test("fails with a 404 for an unknown project ref", { timeout: LIVE_TIMEOUT_MS }, async () => { + const { exitCode, stdout, stderr } = await runSupabaseLive([ + "sso", + "list", + "--project-ref", + "a".repeat(20), + ]); + const out = `${stdout}${stderr}`; + expect(exitCode).not.toBe(0); + expect(out).not.toContain("Unauthorized"); + expect(out).toContain("404"); + }); +}); diff --git a/apps/cli/src/legacy/commands/sso/show/show.live.test.ts b/apps/cli/src/legacy/commands/sso/show/show.live.test.ts new file mode 100644 index 0000000000..346213aed6 --- /dev/null +++ b/apps/cli/src/legacy/commands/sso/show/show.live.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from "vitest"; + +import { describeLive, runSupabaseLive } from "../../../../../tests/helpers/live.ts"; + +const LIVE_TIMEOUT_MS = 120_000; + +// `sso show` requires an existing provider ID, which a freshly provisioned +// project does not have — so there is no stable success path to assert here +// (provider lifecycle is out of scope for read-only live coverage). +// +// The portable live signal is the request path + error mapping: a valid token +// with an unknown --project-ref (and any provider ID) must reach the live +// Management API, come back 404, and exit non-zero. Runs under `describeLive` +// so it needs no provisioned project. +describeLive("supabase sso show — unknown project (live)", () => { + test("fails with a 404 for an unknown project ref", { timeout: LIVE_TIMEOUT_MS }, async () => { + const { exitCode, stdout, stderr } = await runSupabaseLive([ + "sso", + "show", + "00000000-0000-0000-0000-000000000000", // well-formed UUID, nonexistent provider + "--project-ref", + "a".repeat(20), // well-formed (20 lowercase chars) but nonexistent ref + ]); + const out = `${stdout}${stderr}`; + expect(exitCode).not.toBe(0); + expect(out).not.toContain("Unauthorized"); + expect(out).toContain("404"); + }); +}); diff --git a/apps/cli/src/legacy/commands/vanity-subdomains/get/get.live.test.ts b/apps/cli/src/legacy/commands/vanity-subdomains/get/get.live.test.ts new file mode 100644 index 0000000000..d4edf28190 --- /dev/null +++ b/apps/cli/src/legacy/commands/vanity-subdomains/get/get.live.test.ts @@ -0,0 +1,70 @@ +import { expect, test } from "vitest"; + +import { + describeLive, + describeLiveProject, + requireLiveProjectRef, + runSupabaseLive, +} from "../../../../../tests/helpers/live.ts"; + +const LIVE_TIMEOUT_MS = 120_000; + +// Project-scoped read-only scenario. Skipped unless SUPABASE_LIVE_PROJECT_REF is +// set (the cli-e2e-ci runner provisions a project; a control-plane-only stack +// skips it). Reads the project's vanity subdomain config via the Management API +// control plane — a fresh project returns a status object (e.g. not used). +describeLiveProject("supabase vanity-subdomains get (live)", () => { + test( + "gets the vanity subdomain config for the project", + { timeout: LIVE_TIMEOUT_MS }, + async () => { + const ref = requireLiveProjectRef(); + const { exitCode, stdout, stderr } = await runSupabaseLive([ + "vanity-subdomains", + "get", + "--project-ref", + ref, + ]); + expect(`${stdout}${stderr}`).not.toContain("Unauthorized"); + expect(exitCode).toBe(0); + }, + ); + + test( + "emits the vanity subdomain config as machine-readable JSON", + { timeout: LIVE_TIMEOUT_MS }, + async () => { + const ref = requireLiveProjectRef(); + const { exitCode, stdout } = await runSupabaseLive([ + "vanity-subdomains", + "get", + "--project-ref", + ref, + "--output-format", + "json", + ]); + expect(exitCode).toBe(0); + // Payload-only JSON: a status object like { status, custom_domain? }. + const parsed = JSON.parse(stdout) as { status?: string }; + expect(typeof parsed).toBe("object"); + expect(parsed).not.toBeNull(); + expect(typeof parsed.status).toBe("string"); + }, + ); +}); + +// Error path needing NO provisioned project: unknown --project-ref → 404. +describeLive("supabase vanity-subdomains get — unknown project (live)", () => { + test("fails with a 404 for an unknown project ref", { timeout: LIVE_TIMEOUT_MS }, async () => { + const { exitCode, stdout, stderr } = await runSupabaseLive([ + "vanity-subdomains", + "get", + "--project-ref", + "a".repeat(20), + ]); + const out = `${stdout}${stderr}`; + expect(exitCode).not.toBe(0); + expect(out).not.toContain("Unauthorized"); + expect(out).toContain("404"); + }); +}); diff --git a/apps/cli/tests/helpers/live-env.ts b/apps/cli/tests/helpers/live-env.ts index f6705d71e0..be988d75f9 100644 --- a/apps/cli/tests/helpers/live-env.ts +++ b/apps/cli/tests/helpers/live-env.ts @@ -16,6 +16,10 @@ * `http://localhost:8080`. * - `SUPABASE_LIVE_PROJECT_REF` — a provisioned project; gates project-scoped * suites (functions, branches, db, storage). + * - `SUPABASE_LIVE_DB_URL` — a percent-encoded Postgres connection string for the + * provisioned project (e.g. the pooler URL). Gates the data-plane suites + * (`db dump`, `db advisors`, `migration list`) which connect to the project DB + * via `--db-url` rather than the Management API. Absent → those suites skip. * - `NODE_EXTRA_CA_CERTS` — trusts the supabox CA for `*.supabase.red` TLS; * inherited by the subprocess via the parent environment. */ @@ -71,3 +75,30 @@ export function requireLiveProjectRef(): string { } return ref; } + +/** + * Percent-encoded Postgres connection string for the provisioned project, used + * by the data-plane commands (`db dump`, `db advisors`, `migration list`) that + * connect to the database directly via `--db-url` instead of the Management API. + * The cli-e2e-ci runner sets this once it can resolve the project's pooler URL; + * absent → those suites skip. Returns `undefined` when unset. + */ +export function liveDbUrl(): string | undefined { + return process.env["SUPABASE_LIVE_DB_URL"]; +} + +/** + * The live project DB URL, or a thrown error if unset. Safe to call inside a + * `describeLiveDb` block (the gate guarantees it is present) and gives a typed + * `string` without a non-null assertion. + */ +export function requireLiveDbUrl(): string { + const url = liveDbUrl(); + if (!url) { + throw new Error( + "SUPABASE_LIVE_DB_URL must be set for data-plane live tests " + + "(the cli-e2e-ci runner sets it once the project's pooler URL is resolvable).", + ); + } + return url; +} diff --git a/apps/cli/tests/helpers/live.ts b/apps/cli/tests/helpers/live.ts index 78d2558190..fc4a3e33a6 100644 --- a/apps/cli/tests/helpers/live.ts +++ b/apps/cli/tests/helpers/live.ts @@ -5,6 +5,7 @@ import { isLiveConfigured, LIVE_DEFAULT_PROFILE, LIVE_EXIT_TIMEOUT_MS, + liveDbUrl, liveProjectRef, } from "./live-env.ts"; @@ -26,7 +27,9 @@ export { LIVE_DEFAULT_PROFILE, LIVE_EXIT_TIMEOUT_MS, liveApiBaseUrl, + liveDbUrl, liveProjectRef, + requireLiveDbUrl, requireLiveProjectRef, } from "./live-env.ts"; @@ -45,6 +48,16 @@ export const describeLive = describe.skipIf(!isLiveConfigured()); */ export const describeLiveProject = describe.skipIf(!isLiveConfigured() || !liveProjectRef()); +/** + * `describe` for data-plane live suites (`db dump`, `db advisors`, + * `migration list`): runs only when the live env is configured AND a project DB + * URL is available. These connect to the project Postgres directly via + * `--db-url`, so they need more than the Management API token — on a stack + * without a resolvable pooler URL they skip rather than fail. See + * `requireLiveDbUrl`. + */ +export const describeLiveDb = describe.skipIf(!isLiveConfigured() || !liveDbUrl()); + /** * Spawn the built CLI against the live platform, injecting the profile so the * Management API base resolves to the stack. Defaults to the `legacy` shell, From 1456b0f35710256e94f1a3fa5658c8ece46a73f6 Mon Sep 17 00:00:00 2001 From: avallete Date: Fri, 26 Jun 2026 16:09:28 +0200 Subject: [PATCH 4/5] test(cli): align live negatives with real error mapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Codex review of the read-only live coverage — several assertions assumed an error shape the handlers do not produce: - sso list / sso show / network-restrictions get: the 404 paths map to errors that omit the HTTP status code (SAML-disabled, NotFound, "received: "), so the negatives now assert clean non-zero + non-Unauthorized instead of a literal "404". - sso list / vanity-subdomains get: success is entitlement-gated (SAML 2.0; Pro+ plan) and 404s/400s on a fresh project, so both are demoted to unknown-project negative-only rather than an unconditional success. - branches get: pass a placeholder branch name so the non-TTY subprocess does not enter the interactive branch-id prompt before reaching the API. Commands whose mappers do include the status code (backups, network-bans, postgres-config, ssl-enforcement, domains, secrets) keep the stricter 404 assertion. Co-Authored-By: Claude Opus 4.8 --- .../commands/branches/get/get.live.test.ts | 9 ++- .../network-restrictions/get/get.live.test.ts | 12 ++-- .../commands/sso/list/list.live.test.ts | 60 +++++------------- .../commands/sso/show/show.live.test.ts | 12 ++-- .../vanity-subdomains/get/get.live.test.ts | 62 +++---------------- 5 files changed, 44 insertions(+), 111 deletions(-) diff --git a/apps/cli/src/legacy/commands/branches/get/get.live.test.ts b/apps/cli/src/legacy/commands/branches/get/get.live.test.ts index 353f49cdac..cb6051e224 100644 --- a/apps/cli/src/legacy/commands/branches/get/get.live.test.ts +++ b/apps/cli/src/legacy/commands/branches/get/get.live.test.ts @@ -11,13 +11,18 @@ const LIVE_TIMEOUT_MS = 120_000; // // The portable live signal is the request path + error mapping: a valid token // with an unknown --project-ref must reach the live Management API, come back -// 404, and exit non-zero. Runs under `describeLive` so it needs no provisioned -// project. +// 404 (the find-branch error includes the status code), and exit non-zero. +// +// A branch name is passed explicitly: omitting the optional [name] makes +// `legacyBranchesGet` prompt for a branch id, which in a non-TTY live subprocess +// (e.g. detached HEAD) fails before the API call and would not exercise the +// intended path. Runs under `describeLive` so it needs no provisioned project. describeLive("supabase branches get — unknown project (live)", () => { test("fails with a 404 for an unknown project ref", { timeout: LIVE_TIMEOUT_MS }, async () => { const { exitCode, stdout, stderr } = await runSupabaseLive([ "branches", "get", + "main", // placeholder branch name to skip the non-TTY prompt "--project-ref", "a".repeat(20), // well-formed (20 lowercase chars) but nonexistent ref ]); diff --git a/apps/cli/src/legacy/commands/network-restrictions/get/get.live.test.ts b/apps/cli/src/legacy/commands/network-restrictions/get/get.live.test.ts index b980f6187a..c65d43581b 100644 --- a/apps/cli/src/legacy/commands/network-restrictions/get/get.live.test.ts +++ b/apps/cli/src/legacy/commands/network-restrictions/get/get.live.test.ts @@ -49,18 +49,20 @@ describeLiveProject("supabase network-restrictions get (live)", () => { ); }); -// Error path needing NO provisioned project: unknown --project-ref → 404. +// Error path needing NO provisioned project: a valid token with an unknown +// --project-ref must reach the live Management API and exit non-zero. The +// handler formats non-200s as "failed to retrieve network restrictions; +// received: " without the HTTP status code, so we assert behavior rather +// than a literal "404". describeLive("supabase network-restrictions get — unknown project (live)", () => { - test("fails with a 404 for an unknown project ref", { timeout: LIVE_TIMEOUT_MS }, async () => { + test("fails cleanly for an unknown project ref", { timeout: LIVE_TIMEOUT_MS }, async () => { const { exitCode, stdout, stderr } = await runSupabaseLive([ "network-restrictions", "get", "--project-ref", "a".repeat(20), ]); - const out = `${stdout}${stderr}`; expect(exitCode).not.toBe(0); - expect(out).not.toContain("Unauthorized"); - expect(out).toContain("404"); + expect(`${stdout}${stderr}`).not.toContain("Unauthorized"); }); }); diff --git a/apps/cli/src/legacy/commands/sso/list/list.live.test.ts b/apps/cli/src/legacy/commands/sso/list/list.live.test.ts index 020ca8b287..86e33fc748 100644 --- a/apps/cli/src/legacy/commands/sso/list/list.live.test.ts +++ b/apps/cli/src/legacy/commands/sso/list/list.live.test.ts @@ -1,60 +1,28 @@ import { expect, test } from "vitest"; -import { - describeLive, - describeLiveProject, - requireLiveProjectRef, - runSupabaseLive, -} from "../../../../../tests/helpers/live.ts"; +import { describeLive, runSupabaseLive } from "../../../../../tests/helpers/live.ts"; const LIVE_TIMEOUT_MS = 120_000; -// Project-scoped read-only scenario. Skipped unless SUPABASE_LIVE_PROJECT_REF is -// set (the cli-e2e-ci runner provisions a project; a control-plane-only stack -// skips it). Lists the project's SSO identity providers via the Management API -// control plane — a fresh project has none, which is a valid empty list. -describeLiveProject("supabase sso list (live)", () => { - test("lists SSO providers for the project", { timeout: LIVE_TIMEOUT_MS }, async () => { - const ref = requireLiveProjectRef(); - const { exitCode, stdout, stderr } = await runSupabaseLive([ - "sso", - "list", - "--project-ref", - ref, - ]); - expect(`${stdout}${stderr}`).not.toContain("Unauthorized"); - expect(exitCode).toBe(0); - }); - - test("emits SSO providers as machine-readable JSON", { timeout: LIVE_TIMEOUT_MS }, async () => { - const ref = requireLiveProjectRef(); - const { exitCode, stdout } = await runSupabaseLive([ - "sso", - "list", - "--project-ref", - ref, - "--output-format", - "json", - ]); - expect(exitCode).toBe(0); - // Payload-only JSON shaped like { providers: [...] }. - const parsed = JSON.parse(stdout) as { providers: unknown[] }; - expect(Array.isArray(parsed.providers)).toBe(true); - }); -}); - -// Error path needing NO provisioned project: unknown --project-ref → 404. +// `sso list` requires SAML 2.0 to be enabled for the project: the Management API +// returns 404 when it is not, which `legacySsoList` maps to a "SAML disabled" +// error rather than an empty `{ providers: [] }` payload. A freshly provisioned +// project has no SAML entitlement, so there is no stable success path here — +// listing exits non-zero on such a stack. +// +// The portable live signal is the request path + error mapping: a valid token +// with an unknown --project-ref must reach the live Management API and exit +// non-zero (not a crash, not "Unauthorized"). The mapped error intentionally +// omits the HTTP status code, so we assert behavior rather than a literal "404". describeLive("supabase sso list — unknown project (live)", () => { - test("fails with a 404 for an unknown project ref", { timeout: LIVE_TIMEOUT_MS }, async () => { + test("fails cleanly for an unknown project ref", { timeout: LIVE_TIMEOUT_MS }, async () => { const { exitCode, stdout, stderr } = await runSupabaseLive([ "sso", "list", "--project-ref", - "a".repeat(20), + "a".repeat(20), // well-formed (20 lowercase chars) but nonexistent ref ]); - const out = `${stdout}${stderr}`; expect(exitCode).not.toBe(0); - expect(out).not.toContain("Unauthorized"); - expect(out).toContain("404"); + expect(`${stdout}${stderr}`).not.toContain("Unauthorized"); }); }); diff --git a/apps/cli/src/legacy/commands/sso/show/show.live.test.ts b/apps/cli/src/legacy/commands/sso/show/show.live.test.ts index 346213aed6..d122acc728 100644 --- a/apps/cli/src/legacy/commands/sso/show/show.live.test.ts +++ b/apps/cli/src/legacy/commands/sso/show/show.live.test.ts @@ -10,10 +10,12 @@ const LIVE_TIMEOUT_MS = 120_000; // // The portable live signal is the request path + error mapping: a valid token // with an unknown --project-ref (and any provider ID) must reach the live -// Management API, come back 404, and exit non-zero. Runs under `describeLive` -// so it needs no provisioned project. +// Management API and exit non-zero. `legacySsoShow` maps the 404 to a +// "could not be found" error that omits the HTTP status code, so we assert +// behavior rather than a literal "404". Runs under `describeLive` (no project +// needed). describeLive("supabase sso show — unknown project (live)", () => { - test("fails with a 404 for an unknown project ref", { timeout: LIVE_TIMEOUT_MS }, async () => { + test("fails cleanly for an unknown project ref", { timeout: LIVE_TIMEOUT_MS }, async () => { const { exitCode, stdout, stderr } = await runSupabaseLive([ "sso", "show", @@ -21,9 +23,7 @@ describeLive("supabase sso show — unknown project (live)", () => { "--project-ref", "a".repeat(20), // well-formed (20 lowercase chars) but nonexistent ref ]); - const out = `${stdout}${stderr}`; expect(exitCode).not.toBe(0); - expect(out).not.toContain("Unauthorized"); - expect(out).toContain("404"); + expect(`${stdout}${stderr}`).not.toContain("Unauthorized"); }); }); diff --git a/apps/cli/src/legacy/commands/vanity-subdomains/get/get.live.test.ts b/apps/cli/src/legacy/commands/vanity-subdomains/get/get.live.test.ts index d4edf28190..6dbd22be41 100644 --- a/apps/cli/src/legacy/commands/vanity-subdomains/get/get.live.test.ts +++ b/apps/cli/src/legacy/commands/vanity-subdomains/get/get.live.test.ts @@ -1,66 +1,24 @@ import { expect, test } from "vitest"; -import { - describeLive, - describeLiveProject, - requireLiveProjectRef, - runSupabaseLive, -} from "../../../../../tests/helpers/live.ts"; +import { describeLive, runSupabaseLive } from "../../../../../tests/helpers/live.ts"; const LIVE_TIMEOUT_MS = 120_000; -// Project-scoped read-only scenario. Skipped unless SUPABASE_LIVE_PROJECT_REF is -// set (the cli-e2e-ci runner provisions a project; a control-plane-only stack -// skips it). Reads the project's vanity subdomain config via the Management API -// control plane — a fresh project returns a status object (e.g. not used). -describeLiveProject("supabase vanity-subdomains get (live)", () => { - test( - "gets the vanity subdomain config for the project", - { timeout: LIVE_TIMEOUT_MS }, - async () => { - const ref = requireLiveProjectRef(); - const { exitCode, stdout, stderr } = await runSupabaseLive([ - "vanity-subdomains", - "get", - "--project-ref", - ref, - ]); - expect(`${stdout}${stderr}`).not.toContain("Unauthorized"); - expect(exitCode).toBe(0); - }, - ); - - test( - "emits the vanity subdomain config as machine-readable JSON", - { timeout: LIVE_TIMEOUT_MS }, - async () => { - const ref = requireLiveProjectRef(); - const { exitCode, stdout } = await runSupabaseLive([ - "vanity-subdomains", - "get", - "--project-ref", - ref, - "--output-format", - "json", - ]); - expect(exitCode).toBe(0); - // Payload-only JSON: a status object like { status, custom_domain? }. - const parsed = JSON.parse(stdout) as { status?: string }; - expect(typeof parsed).toBe("object"); - expect(parsed).not.toBeNull(); - expect(typeof parsed.status).toBe("string"); - }, - ); -}); - -// Error path needing NO provisioned project: unknown --project-ref → 404. +// `vanity-subdomains get` is plan-gated: the Management API contract returns 400 +// when the org is not on a Pro/Team/Enterprise plan, so a freshly provisioned +// project under a non-entitled org has no stable success path here. +// +// The portable live signal is the unknown-project path: a valid token with an +// unknown --project-ref must reach the live Management API, come back 404 (the +// status mapper includes the code), and exit non-zero. Runs under `describeLive` +// so it needs no provisioned project. describeLive("supabase vanity-subdomains get — unknown project (live)", () => { test("fails with a 404 for an unknown project ref", { timeout: LIVE_TIMEOUT_MS }, async () => { const { exitCode, stdout, stderr } = await runSupabaseLive([ "vanity-subdomains", "get", "--project-ref", - "a".repeat(20), + "a".repeat(20), // well-formed (20 lowercase chars) but nonexistent ref ]); const out = `${stdout}${stderr}`; expect(exitCode).not.toBe(0); From 3b2049aba03ea161cee8bb1d8b12e272f85f83d4 Mon Sep 17 00:00:00 2001 From: avallete Date: Fri, 26 Jun 2026 18:21:50 +0200 Subject: [PATCH 5/5] test(cli): demote network-bans get live to negative-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A supabox-backed live run showed `network-bans get` exits non-zero on a freshly provisioned project: the retrieve endpoint returns a non-200 there (the request reaches the API — not Unauthorized — but does not 200), same class as the SAML-gated sso and plan-gated vanity-subdomains commands. Drop the project-scoped success/JSON tests; keep the unknown-project 404 negative (which passed live), matching the established pattern for endpoints without a stable success path on supabox. Co-Authored-By: Claude Opus 4.8 --- .../network-bans/get/get.live.test.ts | 54 ++++--------------- 1 file changed, 11 insertions(+), 43 deletions(-) diff --git a/apps/cli/src/legacy/commands/network-bans/get/get.live.test.ts b/apps/cli/src/legacy/commands/network-bans/get/get.live.test.ts index 608fc5ae0a..845a79e0ab 100644 --- a/apps/cli/src/legacy/commands/network-bans/get/get.live.test.ts +++ b/apps/cli/src/legacy/commands/network-bans/get/get.live.test.ts @@ -1,57 +1,25 @@ import { expect, test } from "vitest"; -import { - describeLive, - describeLiveProject, - requireLiveProjectRef, - runSupabaseLive, -} from "../../../../../tests/helpers/live.ts"; +import { describeLive, runSupabaseLive } from "../../../../../tests/helpers/live.ts"; const LIVE_TIMEOUT_MS = 120_000; -// Project-scoped read-only scenario. Skipped unless SUPABASE_LIVE_PROJECT_REF is -// set (the cli-e2e-ci runner provisions a project; a control-plane-only stack -// skips it). Reads the project's network bans via the Management API control -// plane — a fresh project has none, which is a valid empty list. -describeLiveProject("supabase network-bans get (live)", () => { - test("gets network bans for the project", { timeout: LIVE_TIMEOUT_MS }, async () => { - const ref = requireLiveProjectRef(); - const { exitCode, stdout, stderr } = await runSupabaseLive([ - "network-bans", - "get", - "--project-ref", - ref, - ]); - expect(`${stdout}${stderr}`).not.toContain("Unauthorized"); - expect(exitCode).toBe(0); - }); - - test("emits network bans as machine-readable JSON", { timeout: LIVE_TIMEOUT_MS }, async () => { - const ref = requireLiveProjectRef(); - const { exitCode, stdout } = await runSupabaseLive([ - "network-bans", - "get", - "--project-ref", - ref, - "--output-format", - "json", - ]); - expect(exitCode).toBe(0); - // Payload-only JSON shaped like { banned_ipv4_addresses: [...] }. - const parsed = JSON.parse(stdout) as { banned_ipv4_addresses: string[] }; - expect(Array.isArray(parsed.banned_ipv4_addresses)).toBe(true); - }); -}); - -// Error path needing NO provisioned project: unknown --project-ref → 404 → exit -// non-zero, exercising the request path + error mapping on a control-plane stack. +// `network-bans get` retrieves bans via a dedicated Management API endpoint that +// supabox returns a non-200 for on a freshly provisioned project (verified +// against the live stack: the request reaches the API — not Unauthorized — but +// exits non-zero), so there is no stable success path here. +// +// The portable live signal is the unknown-project path: a valid token with an +// unknown --project-ref must reach the live Management API, come back 404 (the +// status mapper includes the code), and exit non-zero. Runs under `describeLive` +// so it needs no provisioned project. describeLive("supabase network-bans get — unknown project (live)", () => { test("fails with a 404 for an unknown project ref", { timeout: LIVE_TIMEOUT_MS }, async () => { const { exitCode, stdout, stderr } = await runSupabaseLive([ "network-bans", "get", "--project-ref", - "a".repeat(20), + "a".repeat(20), // well-formed (20 lowercase chars) but nonexistent ref ]); const out = `${stdout}${stderr}`; expect(exitCode).not.toBe(0);