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..cb6051e224 --- /dev/null +++ b/apps/cli/src/legacy/commands/branches/get/get.live.test.ts @@ -0,0 +1,34 @@ +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 (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 + ]); + 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..845a79e0ab --- /dev/null +++ b/apps/cli/src/legacy/commands/network-bans/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; + +// `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), // 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/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..c65d43581b --- /dev/null +++ b/apps/cli/src/legacy/commands/network-restrictions/get/get.live.test.ts @@ -0,0 +1,68 @@ +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: 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 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), + ]); + expect(exitCode).not.toBe(0); + expect(`${stdout}${stderr}`).not.toContain("Unauthorized"); + }); +}); 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/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/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); + } }, ); }); 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"); + }); +}); 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..86e33fc748 --- /dev/null +++ b/apps/cli/src/legacy/commands/sso/list/list.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; + +// `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 cleanly for an unknown project ref", { timeout: LIVE_TIMEOUT_MS }, async () => { + const { exitCode, stdout, stderr } = await runSupabaseLive([ + "sso", + "list", + "--project-ref", + "a".repeat(20), // well-formed (20 lowercase chars) but nonexistent ref + ]); + expect(exitCode).not.toBe(0); + 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 new file mode 100644 index 0000000000..d122acc728 --- /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 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 cleanly 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 + ]); + expect(exitCode).not.toBe(0); + 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 new file mode 100644 index 0000000000..6dbd22be41 --- /dev/null +++ b/apps/cli/src/legacy/commands/vanity-subdomains/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; + +// `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), // 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/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,