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..2aeb376907 --- /dev/null +++ b/apps/cli/src/legacy/commands/db/advisors/advisors.live.test.ts @@ -0,0 +1,34 @@ +import { expect, test } from "vitest"; + +import { + describeLiveDataPlane, + requireLiveProjectRef, + runSupabaseLive, +} from "../../../../../tests/helpers/live.ts"; + +const LIVE_TIMEOUT_MS = 180_000; + +// Data-plane scenario: `db advisors --linked` runs lint queries against the +// project's *Postgres* (the cli mints a temp login role via the Management API), +// so it gates on `describeLiveDataPlane` — runs only when the project instance +// is ACTIVE_HEALTHY, otherwise SKIPS (e.g. the current cli-e2e-ci CI without +// supabase-postgres-17, CLI-1825). Activates once the data-plane is provisioned. +// The ref is supplied via SUPABASE_PROJECT_ID (db commands resolve the linked +// ref from env / config.toml / ref-file, not a `--project-ref` flag). +describeLiveDataPlane("supabase db advisors (live)", () => { + test("emits advisor results as machine-readable JSON", { timeout: LIVE_TIMEOUT_MS }, async () => { + const ref = requireLiveProjectRef(); + // `--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, stderr } = await runSupabaseLive( + ["db", "advisors", "--linked", "--fail-on", "none", "--output-format", "json"], + { env: { SUPABASE_PROJECT_ID: ref } }, + ); + expect(`${stdout}${stderr}`).not.toContain("Unauthorized"); + expect(exitCode, `stdout:\n${stdout}\nstderr:\n${stderr}`).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..7118d406c7 --- /dev/null +++ b/apps/cli/src/legacy/commands/db/dump/dump.live.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from "vitest"; + +import { + describeLiveDataPlane, + requireLiveProjectRef, + runSupabaseLive, +} from "../../../../../tests/helpers/live.ts"; + +const LIVE_TIMEOUT_MS = 180_000; + +// Data-plane scenario: `db dump --linked` connects to the project's *Postgres* +// (the cli mints a temp login role via the Management API), so it gates on +// `describeLiveDataPlane` — runs only when the project instance is +// ACTIVE_HEALTHY, otherwise SKIPS (e.g. the current cli-e2e-ci CI without +// supabase-postgres-17, CLI-1825). Activates once the data-plane is provisioned. +// The ref is supplied via SUPABASE_PROJECT_ID (db commands resolve the linked +// ref from env / config.toml / ref-file, not a `--project-ref` flag). +describeLiveDataPlane("supabase db dump (live)", () => { + test("dumps the linked project schema to stdout", { timeout: LIVE_TIMEOUT_MS }, async () => { + const ref = requireLiveProjectRef(); + const { exitCode, stdout, stderr } = await runSupabaseLive(["db", "dump", "--linked"], { + env: { SUPABASE_PROJECT_ID: ref }, + }); + expect(stderr).not.toContain("Unauthorized"); + expect(exitCode, `stdout:\n${stdout}\nstderr:\n${stderr}`).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/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"); + }); +});