diff --git a/.oxlintrc.effect.json b/.oxlintrc.effect.json index 9c2cecafc9..efd01b6473 100644 --- a/.oxlintrc.effect.json +++ b/.oxlintrc.effect.json @@ -9,16 +9,14 @@ "!packages/api/**", "!packages/config/**", "!packages/stack/**", - "!apps/cli-e2e/src/server/**", - "!apps/cli-e2e/src/tests/env.ts", - "!apps/cli-e2e/tests/**", - "!apps/cli-e2e/vitest.config.ts", + "!apps/cli-e2e/**", "!apps/cli/**", "!apps/docs/**", "!packages/cli-test-helpers/**", "!tools/**", // Last match wins across the whole list: keep bare re-exclusions after every // `!` entry that would otherwise re-include them. + "apps/cli-e2e/scripts/replay-serve.ts", "apps/cli-e2e/src/server/replay-server.ts", "apps/cli/src/shared/cli/bin.ts", "apps/cli/src/shared/compute/stacks/**", diff --git a/apps/cli-e2e/AGENTS.md b/apps/cli-e2e/AGENTS.md index 7cd57a5f91..0b75112979 100644 --- a/apps/cli-e2e/AGENTS.md +++ b/apps/cli-e2e/AGENTS.md @@ -61,20 +61,30 @@ the running tests exercise. The implications: ### `testBehaviour` — the default for all success and error tests ```typescript -import { testBehaviour } from "./test-context.ts"; - -testBehaviour("does the thing", async ({ run, projectRef, orgId, apiUrl, workspace }) => { - const result = await run(["command", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Expected output"); -}); +import { Effect } from "effect"; +import { runE2e, testBehaviour } from "./test-context.ts"; + +testBehaviour("does the thing", ({ run, projectRef, orgId, apiUrl, workspace, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["command", `--remote=${projectRef}`]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Expected output"); + }), + signal, + ), +); ``` The `workspace` fixture is what triggers scenario setup — if you destructure `workspace` or `run` (which depends on `workspace`), the scenario is automatically started before the test and cleaned up after. +Always return `runE2e(...)` from the body: Vitest does not run a returned Effect, so the harness fails a test that requests `run` or `runNoProjectId` and passes without executing it. A custom runner added with `testBehaviour.extend` gets the same check by wrapping its CLI Effect in `runGuard.track(...)`, as `stackRun` does. Pass the test's `signal` so a Vitest timeout interrupts the body. + +This workspace is under the Effect lint (`.oxlintrc.effect.json`, warnings are errors), which rejects `async` test bodies, global `fetch`, `process.env` and `JSON.parse` in Effect code. Decode JSON stdout with `decodeJsonOutput(schema)(result.stdout)`, which reports a stdout excerpt on failure. + Available fixtures: -- `run(cmd)` — executes the CLI via the relay server +- `run(cmd)` — returns an Effect that executes the CLI via the relay server - `projectRef` — 20-char project ref (real in record mode, default in replay) - `orgId` — org slug (real in record mode, default in replay) - `apiUrl` — the relay server base URL, for direct `/_ctrl/` calls @@ -85,15 +95,23 @@ Available fixtures: Inject errors via `/_ctrl/error-all` before the `run` call: ```typescript -testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run(["command", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); -}); +import { Effect } from "effect"; +import { PROJECT_REF } from "./env.ts"; +import { postControl, runE2e, testBehaviour } from "./test-context.ts"; + +testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run(["command", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), +); ``` Error injection tests can use `PROJECT_REF` directly (not `projectRef`) because no real API call reaches staging — the error is injected before the relay server checks the fixture. Their scenario fixture is always `[]` (empty). @@ -103,15 +121,21 @@ Error injection tests can use `PROJECT_REF` directly (not `projectRef`) because If a test depends on timing (e.g. deleting a project that was just created won't work because it's not yet provisioned), use `skipIf(isRecording)` and write a static fixture instead: ```typescript +import { Effect } from "effect"; import { isRecording } from "./env.ts"; +import { runE2e, testBehaviour } from "./test-context.ts"; testBehaviour.skipIf(isRecording)( "deletes project with --yes flag", - async ({ run, projectRef }) => { - const result = await run(["projects", "delete", projectRef, "--yes"]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Deleted project"); - }, + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["projects", "delete", projectRef, "--yes"]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Deleted project"); + }), + signal, + ), ); ``` diff --git a/apps/cli-e2e/src/tests/advanced-platform-features.e2e.test.ts b/apps/cli-e2e/src/tests/advanced-platform-features.e2e.test.ts index b3a9e76493..eecc95b949 100644 --- a/apps/cli-e2e/src/tests/advanced-platform-features.e2e.test.ts +++ b/apps/cli-e2e/src/tests/advanced-platform-features.e2e.test.ts @@ -1,1360 +1,1771 @@ +import { Effect, Schema } from "effect"; import { describe, expect } from "vitest"; import { BACKUP_TIMESTAMP, PROJECT_REF, SNIPPET_ID, isRecording } from "./env.ts"; -import { testBehaviour } from "./test-context.ts"; +import { decodeJsonOutput, postControl, runE2e, testBehaviour } from "./test-context.ts"; describe("postgres-config", () => { describe("postgres-config:get", () => { - testBehaviour("renders config overrides", async ({ run, projectRef }) => { - const result = await run([ - "postgres-config", - "get", - "--experimental", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stdout).not.toBe(""); - }); + testBehaviour("renders config overrides", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "postgres-config", + "get", + "--experimental", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout).not.toBe(""); + }), + signal, + ), + ); testBehaviour( "returns config overrides as JSON with --output json", - async ({ run, projectRef }) => { - const result = await run([ - "postgres-config", - "get", - "--experimental", - "--output", - "json", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - const parsed = JSON.parse(result.stdout); - expect(parsed).not.toBeNull(); - expect(typeof parsed).toBe("object"); - }, - ); - - testBehaviour("--debug shows HTTP trace", async ({ run, projectRef }) => { - const result = await run([ - "postgres-config", - "get", - "--experimental", - "--debug", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stderr).toMatch(/HTTP.*GET:/); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "postgres-config", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run([ - "postgres-config", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); - - testBehaviour("exits non-zero on 404 project not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), - }); - const result = await run([ - "postgres-config", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Project not found"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run([ - "postgres-config", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run([ - "postgres-config", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "postgres-config", + "get", + "--experimental", + "--output", + "json", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + const parsed = yield* decodeJsonOutput(Schema.Unknown)(result.stdout); + expect(parsed).not.toBeNull(); + expect(typeof parsed).toBe("object"); + }), + signal, + ), + ); + + testBehaviour("--debug shows HTTP trace", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "postgres-config", + "get", + "--experimental", + "--debug", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toMatch(/HTTP.*GET:/); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "postgres-config", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run([ + "postgres-config", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 project not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Project not found" }, + }); + const result = yield* run([ + "postgres-config", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Project not found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run([ + "postgres-config", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run([ + "postgres-config", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); describe("postgres-config:update", () => { - testBehaviour("sets single config override", async ({ run, projectRef }) => { - const result = await run([ - "postgres-config", - "update", - "--experimental", - "--config", - "max_connections=200", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - }); - - testBehaviour("sets multiple config overrides", async ({ run, projectRef }) => { - const result = await run([ - "postgres-config", - "update", - "--experimental", - "--config", - "max_connections=200", - "--config", - "shared_buffers=256MB", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - }); + testBehaviour("sets single config override", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "postgres-config", + "update", + "--experimental", + "--config", + "max_connections=200", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); + + testBehaviour("sets multiple config overrides", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "postgres-config", + "update", + "--experimental", + "--config", + "max_connections=200", + "--config", + "shared_buffers=256MB", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); testBehaviour( "--replace-existing-overrides replaces all overrides", - async ({ run, projectRef }) => { - const result = await run([ - "postgres-config", - "update", - "--experimental", - "--config", - "max_connections=200", - "--replace-existing-overrides", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - }, + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "postgres-config", + "update", + "--experimental", + "--config", + "max_connections=200", + "--replace-existing-overrides", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), ); testBehaviour( "--no-restart applies config without restarting postgres", - async ({ run, projectRef }) => { - const result = await run([ - "postgres-config", - "update", - "--experimental", - "--config", - "max_connections=200", - "--no-restart", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - }, - ); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "postgres-config", - "update", - "--experimental", - "--config", - "max_connections=200", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 404 project not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), - }); - const result = await run([ - "postgres-config", - "update", - "--experimental", - "--config", - "max_connections=200", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Project not found"); - }); - - testBehaviour("exits non-zero on 422 unrecognized config key", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - status: 422, - body: { message: "unrecognized config key: invalid_key" }, - }), - }); - const result = await run([ - "postgres-config", - "update", - "--experimental", - "--config", - "invalid_key=value", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("unrecognized config key"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run([ - "postgres-config", - "update", - "--experimental", - "--config", - "max_connections=200", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run([ - "postgres-config", - "update", - "--experimental", - "--config", - "max_connections=200", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "postgres-config", + "update", + "--experimental", + "--config", + "max_connections=200", + "--no-restart", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "postgres-config", + "update", + "--experimental", + "--config", + "max_connections=200", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 project not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Project not found" }, + }); + const result = yield* run([ + "postgres-config", + "update", + "--experimental", + "--config", + "max_connections=200", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Project not found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 422 unrecognized config key", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 422, + body: { message: "unrecognized config key: invalid_key" }, + }); + const result = yield* run([ + "postgres-config", + "update", + "--experimental", + "--config", + "invalid_key=value", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("unrecognized config key"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run([ + "postgres-config", + "update", + "--experimental", + "--config", + "max_connections=200", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run([ + "postgres-config", + "update", + "--experimental", + "--config", + "max_connections=200", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); describe("postgres-config:delete", () => { - testBehaviour("removes config override", async ({ run, projectRef }) => { - const result = await run([ - "postgres-config", - "delete", - "--experimental", - "--config", - "max_connections", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - }); - - testBehaviour("removes multiple config keys", async ({ run, projectRef }) => { - const result = await run([ - "postgres-config", - "delete", - "--experimental", - "--config", - "max_connections", - "--config", - "shared_buffers", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - }); + testBehaviour("removes config override", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "postgres-config", + "delete", + "--experimental", + "--config", + "max_connections", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); + + testBehaviour("removes multiple config keys", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "postgres-config", + "delete", + "--experimental", + "--config", + "max_connections", + "--config", + "shared_buffers", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); testBehaviour( "--no-restart removes config without restarting postgres", - async ({ run, projectRef }) => { - const result = await run([ - "postgres-config", - "delete", - "--experimental", - "--config", - "max_connections", - "--no-restart", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - }, - ); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "postgres-config", - "delete", - "--experimental", - "--config", - "max_connections", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 404 project not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), - }); - const result = await run([ - "postgres-config", - "delete", - "--experimental", - "--config", - "max_connections", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Project not found"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run([ - "postgres-config", - "delete", - "--experimental", - "--config", - "max_connections", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run([ - "postgres-config", - "delete", - "--experimental", - "--config", - "max_connections", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "postgres-config", + "delete", + "--experimental", + "--config", + "max_connections", + "--no-restart", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "postgres-config", + "delete", + "--experimental", + "--config", + "max_connections", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 project not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Project not found" }, + }); + const result = yield* run([ + "postgres-config", + "delete", + "--experimental", + "--config", + "max_connections", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Project not found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run([ + "postgres-config", + "delete", + "--experimental", + "--config", + "max_connections", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run([ + "postgres-config", + "delete", + "--experimental", + "--config", + "max_connections", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); }); describe("vanity-subdomains", () => { describe("vanity-subdomains:get", () => { - testBehaviour("renders vanity subdomain and status", async ({ run, projectRef }) => { - const result = await run([ - "vanity-subdomains", - "get", - "--experimental", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stdout).not.toBe(""); - }); + testBehaviour("renders vanity subdomain and status", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "vanity-subdomains", + "get", + "--experimental", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout).not.toBe(""); + }), + signal, + ), + ); testBehaviour( "returns subdomain config as JSON with --output json", - async ({ run, projectRef }) => { - const result = await run([ - "vanity-subdomains", - "get", - "--experimental", - "--output", - "json", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - const parsed = JSON.parse(result.stdout); - expect(parsed).toHaveProperty("status"); - }, - ); - - testBehaviour("--debug shows HTTP trace", async ({ run, projectRef }) => { - const result = await run([ - "vanity-subdomains", - "get", - "--experimental", - "--debug", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stderr).toMatch(/HTTP.*GET:/); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "vanity-subdomains", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run([ - "vanity-subdomains", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); - - testBehaviour("exits non-zero on 404 project not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), - }); - const result = await run([ - "vanity-subdomains", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Project not found"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run([ - "vanity-subdomains", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run([ - "vanity-subdomains", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "vanity-subdomains", + "get", + "--experimental", + "--output", + "json", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + const parsed = yield* decodeJsonOutput(Schema.Unknown)(result.stdout); + expect(parsed).toHaveProperty("status"); + }), + signal, + ), + ); + + testBehaviour("--debug shows HTTP trace", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "vanity-subdomains", + "get", + "--experimental", + "--debug", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toMatch(/HTTP.*GET:/); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "vanity-subdomains", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run([ + "vanity-subdomains", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 project not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Project not found" }, + }); + const result = yield* run([ + "vanity-subdomains", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Project not found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run([ + "vanity-subdomains", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run([ + "vanity-subdomains", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); describe("vanity-subdomains:check-availability", () => { - testBehaviour("reports availability for desired subdomain", async ({ run, projectRef }) => { - const result = await run([ - "vanity-subdomains", - "check-availability", - "--experimental", - "--desired-subdomain", - "myapp", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stdout).not.toBe(""); - }); + testBehaviour("reports availability for desired subdomain", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "vanity-subdomains", + "check-availability", + "--experimental", + "--desired-subdomain", + "myapp", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout).not.toBe(""); + }), + signal, + ), + ); testBehaviour( "returns availability as JSON with --output json", - async ({ run, projectRef }) => { - const result = await run([ - "vanity-subdomains", - "check-availability", - "--experimental", - "--desired-subdomain", - "myapp", - "--output", - "json", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - const parsed = JSON.parse(result.stdout); - expect(parsed).toHaveProperty("available"); - }, - ); - - testBehaviour("exits non-zero without --desired-subdomain flag", async ({ run }) => { - const result = await run([ - "vanity-subdomains", - "check-availability", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - }); - - testBehaviour("exits non-zero on 409 subdomain taken", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - status: 409, - body: { message: "Subdomain already taken" }, - }), - }); - const result = await run([ - "vanity-subdomains", - "check-availability", - "--experimental", - "--desired-subdomain", - "taken", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("already taken"); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "vanity-subdomains", - "check-availability", - "--experimental", - "--desired-subdomain", - "myapp", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 404 project not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), - }); - const result = await run([ - "vanity-subdomains", - "check-availability", - "--experimental", - "--desired-subdomain", - "myapp", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Project not found"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run([ - "vanity-subdomains", - "check-availability", - "--experimental", - "--desired-subdomain", - "myapp", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "vanity-subdomains", + "check-availability", + "--experimental", + "--desired-subdomain", + "myapp", + "--output", + "json", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + const parsed = yield* decodeJsonOutput(Schema.Unknown)(result.stdout); + expect(parsed).toHaveProperty("available"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero without --desired-subdomain flag", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "vanity-subdomains", + "check-availability", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 409 subdomain taken", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 409, + body: { message: "Subdomain already taken" }, + }); + const result = yield* run([ + "vanity-subdomains", + "check-availability", + "--experimental", + "--desired-subdomain", + "taken", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("already taken"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "vanity-subdomains", + "check-availability", + "--experimental", + "--desired-subdomain", + "myapp", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 project not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Project not found" }, + }); + const result = yield* run([ + "vanity-subdomains", + "check-availability", + "--experimental", + "--desired-subdomain", + "myapp", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Project not found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run([ + "vanity-subdomains", + "check-availability", + "--experimental", + "--desired-subdomain", + "myapp", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); }); describe("vanity-subdomains:activate", () => { - testBehaviour("activates desired vanity subdomain", async ({ run, projectRef }) => { - const result = await run([ - "vanity-subdomains", - "activate", - "--experimental", - "--desired-subdomain", - "myapp", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Activated vanity subdomain"); - }); - - testBehaviour("exits non-zero on 409 subdomain already taken", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - status: 409, - body: { message: "Subdomain already taken" }, - }), - }); - const result = await run([ - "vanity-subdomains", - "activate", - "--experimental", - "--desired-subdomain", - "taken", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("already taken"); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "vanity-subdomains", - "activate", - "--experimental", - "--desired-subdomain", - "myapp", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 404 project not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), - }); - const result = await run([ - "vanity-subdomains", - "activate", - "--experimental", - "--desired-subdomain", - "myapp", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Project not found"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run([ - "vanity-subdomains", - "activate", - "--experimental", - "--desired-subdomain", - "myapp", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run([ - "vanity-subdomains", - "activate", - "--experimental", - "--desired-subdomain", - "myapp", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + testBehaviour("activates desired vanity subdomain", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "vanity-subdomains", + "activate", + "--experimental", + "--desired-subdomain", + "myapp", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Activated vanity subdomain"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 409 subdomain already taken", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 409, + body: { message: "Subdomain already taken" }, + }); + const result = yield* run([ + "vanity-subdomains", + "activate", + "--experimental", + "--desired-subdomain", + "taken", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("already taken"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "vanity-subdomains", + "activate", + "--experimental", + "--desired-subdomain", + "myapp", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 project not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Project not found" }, + }); + const result = yield* run([ + "vanity-subdomains", + "activate", + "--experimental", + "--desired-subdomain", + "myapp", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Project not found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run([ + "vanity-subdomains", + "activate", + "--experimental", + "--desired-subdomain", + "myapp", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run([ + "vanity-subdomains", + "activate", + "--experimental", + "--desired-subdomain", + "myapp", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); describe("vanity-subdomains:delete", () => { - testBehaviour("removes vanity subdomain", async ({ run, projectRef }) => { - const result = await run([ - "vanity-subdomains", - "delete", - "--experimental", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stderr).toContain("Deleted vanity subdomain"); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "vanity-subdomains", - "delete", - "--experimental", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 404 project not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), - }); - const result = await run([ - "vanity-subdomains", - "delete", - "--experimental", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Project not found"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run([ - "vanity-subdomains", - "delete", - "--experimental", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run([ - "vanity-subdomains", - "delete", - "--experimental", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + testBehaviour("removes vanity subdomain", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "vanity-subdomains", + "delete", + "--experimental", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toContain("Deleted vanity subdomain"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "vanity-subdomains", + "delete", + "--experimental", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 project not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Project not found" }, + }); + const result = yield* run([ + "vanity-subdomains", + "delete", + "--experimental", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Project not found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run([ + "vanity-subdomains", + "delete", + "--experimental", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run([ + "vanity-subdomains", + "delete", + "--experimental", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); }); describe("encryption", () => { describe("encryption:get-root-key", () => { - testBehaviour("renders root encryption key", async ({ run, projectRef }) => { - const result = await run(["encryption", "get-root-key", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(result.stdout).not.toBe(""); - }); - - testBehaviour("returns root_key as JSON with --output json", async ({ run, projectRef }) => { - const result = await run([ - "encryption", - "get-root-key", - "--output", - "json", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - }); - - testBehaviour("--debug shows HTTP trace", async ({ run, projectRef }) => { - const result = await run(["encryption", "get-root-key", "--debug", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(result.stderr).toMatch(/HTTP.*GET:/); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run(["encryption", "get-root-key", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run(["encryption", "get-root-key", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); - - testBehaviour("exits non-zero on 404 project not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), - }); - const result = await run(["encryption", "get-root-key", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Project not found"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run(["encryption", "get-root-key", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run(["encryption", "get-root-key", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + testBehaviour("renders root encryption key", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["encryption", "get-root-key", `--remote=${projectRef}`]); + expect(result.exitCode).toBe(0); + expect(result.stdout).not.toBe(""); + }), + signal, + ), + ); + + testBehaviour("returns root_key as JSON with --output json", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "encryption", + "get-root-key", + "--output", + "json", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); + + testBehaviour("--debug shows HTTP trace", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "encryption", + "get-root-key", + "--debug", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toMatch(/HTTP.*GET:/); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run(["encryption", "get-root-key", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run(["encryption", "get-root-key", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 project not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Project not found" }, + }); + const result = yield* run(["encryption", "get-root-key", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Project not found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run(["encryption", "get-root-key", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run(["encryption", "get-root-key", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); describe("encryption:update-root-key", () => { - testBehaviour.skipIf(isRecording)("rotates the vault root key", async ({ run, projectRef }) => { - const result = await run([ - "encryption", - "update-root-key", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "encryption", - "update-root-key", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run([ - "encryption", - "update-root-key", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); - - testBehaviour("exits non-zero on 404 project not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), - }); - const result = await run([ - "encryption", - "update-root-key", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Project not found"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run([ - "encryption", - "update-root-key", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run([ - "encryption", - "update-root-key", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + testBehaviour.skipIf(isRecording)("rotates the vault root key", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "encryption", + "update-root-key", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "encryption", + "update-root-key", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run([ + "encryption", + "update-root-key", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 project not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Project not found" }, + }); + const result = yield* run([ + "encryption", + "update-root-key", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Project not found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run([ + "encryption", + "update-root-key", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run([ + "encryption", + "update-root-key", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); }); describe("backups", () => { describe("backups:list", () => { - testBehaviour("renders backup table with REGION column", async ({ run, projectRef }) => { - const result = await run(["backups", "list", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("REGION"); - }); + testBehaviour("renders backup table with REGION column", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["backups", "list", `--remote=${projectRef}`]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("REGION"); + }), + signal, + ), + ); testBehaviour( "returns backup response as JSON with --output json", - async ({ run, projectRef }) => { - const result = await run(["backups", "list", "--output", "json", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - const parsed = JSON.parse(result.stdout); - expect(parsed).toHaveProperty("region"); - }, - ); - - testBehaviour("--debug shows HTTP trace", async ({ run, projectRef }) => { - const result = await run(["backups", "list", "--debug", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(result.stderr).toMatch(/HTTP.*GET:/); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run(["backups", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run(["backups", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); - - testBehaviour("exits non-zero on 404 project not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), - }); - const result = await run(["backups", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Project not found"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run(["backups", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run(["backups", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "backups", + "list", + "--output", + "json", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + const parsed = yield* decodeJsonOutput(Schema.Unknown)(result.stdout); + expect(parsed).toHaveProperty("region"); + }), + signal, + ), + ); + + testBehaviour("--debug shows HTTP trace", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["backups", "list", "--debug", `--remote=${projectRef}`]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toMatch(/HTTP.*GET:/); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run(["backups", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run(["backups", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 project not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Project not found" }, + }); + const result = yield* run(["backups", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Project not found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run(["backups", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run(["backups", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); describe("backups:restore", () => { testBehaviour.skipIf(isRecording)( "initiates PITR restore with -t timestamp", - async ({ run, projectRef }) => { - const result = await run([ - "backups", - "restore", - "-t", - String(BACKUP_TIMESTAMP), - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - }, - ); - - testBehaviour("exits non-zero on 422 out-of-range timestamp", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - status: 422, - body: { message: "recovery time target is out of range" }, - }), - }); - const result = await run([ - "backups", - "restore", - "-t", - "0", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("out of range"); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "backups", - "restore", - "-t", - String(BACKUP_TIMESTAMP), - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 404 project not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), - }); - const result = await run([ - "backups", - "restore", - "-t", - String(BACKUP_TIMESTAMP), - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Project not found"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run([ - "backups", - "restore", - "-t", - String(BACKUP_TIMESTAMP), - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run([ - "backups", - "restore", - "-t", - String(BACKUP_TIMESTAMP), - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "backups", + "restore", + "-t", + String(BACKUP_TIMESTAMP), + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 422 out-of-range timestamp", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 422, + body: { message: "recovery time target is out of range" }, + }); + const result = yield* run([ + "backups", + "restore", + "-t", + "0", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("out of range"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "backups", + "restore", + "-t", + String(BACKUP_TIMESTAMP), + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 project not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Project not found" }, + }); + const result = yield* run([ + "backups", + "restore", + "-t", + String(BACKUP_TIMESTAMP), + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Project not found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run([ + "backups", + "restore", + "-t", + String(BACKUP_TIMESTAMP), + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run([ + "backups", + "restore", + "-t", + String(BACKUP_TIMESTAMP), + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); }); describe("snippets", () => { describe("snippets:list", () => { - testBehaviour("renders snippet table with ID and NAME columns", async ({ run, projectRef }) => { - const result = await run(["snippets", "list", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("ID"); - expect(result.stdout).toContain("NAME"); - }); + testBehaviour("renders snippet table with ID and NAME columns", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["snippets", "list", `--remote=${projectRef}`]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("ID"); + expect(result.stdout).toContain("NAME"); + }), + signal, + ), + ); testBehaviour( "returns snippet list as JSON with --output json", - async ({ run, projectRef }) => { - const result = await run([ - "snippets", - "list", - "--output", - "json", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - const parsed = JSON.parse(result.stdout); - expect(Array.isArray(parsed.data)).toBe(true); - }, - ); - - testBehaviour("--debug shows HTTP trace", async ({ run, projectRef }) => { - const result = await run(["snippets", "list", "--debug", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(result.stderr).toMatch(/HTTP.*GET:/); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run(["snippets", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run(["snippets", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); - - testBehaviour("exits non-zero on 404 project not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), - }); - const result = await run(["snippets", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Project not found"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run(["snippets", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run(["snippets", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "snippets", + "list", + "--output", + "json", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + const parsed = yield* decodeJsonOutput(Schema.Record(Schema.String, Schema.Unknown))( + result.stdout, + ); + expect(Array.isArray(parsed.data)).toBe(true); + }), + signal, + ), + ); + + testBehaviour("--debug shows HTTP trace", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["snippets", "list", "--debug", `--remote=${projectRef}`]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toMatch(/HTTP.*GET:/); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run(["snippets", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run(["snippets", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 project not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Project not found" }, + }); + const result = yield* run(["snippets", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Project not found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run(["snippets", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run(["snippets", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); describe("snippets:download", () => { testBehaviour.skipIf(isRecording)( "prints SQL content to stdout", - async ({ run, projectRef }) => { - const result = await run(["snippets", "download", SNIPPET_ID, `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(result.stdout).not.toBe(""); - }, - ); - - testBehaviour("exits non-zero on 404 snippet not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Snippet not found" } }), - }); - const result = await run(["snippets", "download", SNIPPET_ID, `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("not found"); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run(["snippets", "download", SNIPPET_ID, `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run(["snippets", "download", SNIPPET_ID, `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run(["snippets", "download", SNIPPET_ID, `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "snippets", + "download", + SNIPPET_ID, + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout).not.toBe(""); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 snippet not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Snippet not found" }, + }); + const result = yield* run([ + "snippets", + "download", + SNIPPET_ID, + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("not found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "snippets", + "download", + SNIPPET_ID, + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run([ + "snippets", + "download", + SNIPPET_ID, + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run([ + "snippets", + "download", + SNIPPET_ID, + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); }); diff --git a/apps/cli-e2e/src/tests/branches.e2e.test.ts b/apps/cli-e2e/src/tests/branches.e2e.test.ts index f979199dba..5d317f4308 100644 --- a/apps/cli-e2e/src/tests/branches.e2e.test.ts +++ b/apps/cli-e2e/src/tests/branches.e2e.test.ts @@ -1,500 +1,642 @@ +import { Effect, Schema } from "effect"; import { describe, expect } from "vitest"; import { isRecording, PROJECT_REF } from "./env.ts"; -import { testBehaviour } from "./test-context.ts"; +import { decodeJsonOutput, postControl, runE2e, testBehaviour } from "./test-context.ts"; const BRANCH_NAME = "my-branch"; describe("branches", () => { describe("branches:list", () => { - testBehaviour("renders fixture data in output", async ({ run, projectRef }) => { - const result = await run(["branches", "list", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("NAME"); - expect(result.stdout).toContain("STATUS"); - }); + testBehaviour("renders fixture data in output", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["branches", "list", `--remote=${projectRef}`]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("NAME"); + expect(result.stdout).toContain("STATUS"); + }), + signal, + ), + ); testBehaviour.skipIf(isRecording)( "returns json output with --output json", - async ({ run, projectRef }) => { - const result = await run([ - "branches", - "list", - "--output", - "json", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - const parsed = JSON.parse(result.stdout) as unknown[]; - expect(Array.isArray(parsed)).toBe(true); - }, - ); - - testBehaviour("includes debug output with --debug", async ({ run, projectRef }) => { - const result = await run(["branches", "list", "--debug", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(result.stderr).toMatch(/HTTP.*GET:/); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run(["branches", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run(["branches", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run(["branches", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run(["branches", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "branches", + "list", + "--output", + "json", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + const parsed = yield* decodeJsonOutput(Schema.Unknown)(result.stdout); + expect(Array.isArray(parsed)).toBe(true); + }), + signal, + ), + ); + + testBehaviour("includes debug output with --debug", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["branches", "list", "--debug", `--remote=${projectRef}`]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toMatch(/HTTP.*GET:/); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run(["branches", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run(["branches", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run(["branches", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run(["branches", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); describe("branches:create", () => { - testBehaviour.skipIf(isRecording)("creates ephemeral branch", async ({ run, projectRef }) => { - const result = await run([ - "branches", - "create", - BRANCH_NAME, - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Created preview branch:"); - }); - - testBehaviour.skipIf(isRecording)("creates persistent branch", async ({ run, projectRef }) => { - const result = await run([ - "branches", - "create", - BRANCH_NAME, - "--persistent", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Created preview branch:"); - }); - - testBehaviour.skipIf(isRecording)("creates branch with data", async ({ run, projectRef }) => { - const result = await run([ - "branches", - "create", - BRANCH_NAME, - "--with-data", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Created preview branch:"); - }); - - testBehaviour("exits non-zero on 409 name conflict", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 409, body: { message: "Branch name already in use" } }), - }); - const result = await run([ - "branches", - "create", - BRANCH_NAME, - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Branch name already in use"); - }); - - testBehaviour("exits non-zero on 422 branching not enabled", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - status: 422, - body: { message: "Preview branching is not enabled" }, + testBehaviour.skipIf(isRecording)("creates ephemeral branch", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "branches", + "create", + BRANCH_NAME, + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Created preview branch:"); + }), + signal, + ), + ); + + testBehaviour.skipIf(isRecording)("creates persistent branch", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "branches", + "create", + BRANCH_NAME, + "--persistent", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Created preview branch:"); + }), + signal, + ), + ); + + testBehaviour.skipIf(isRecording)("creates branch with data", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "branches", + "create", + BRANCH_NAME, + "--with-data", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Created preview branch:"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 409 name conflict", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 409, + body: { message: "Branch name already in use" }, + }); + const result = yield* run([ + "branches", + "create", + BRANCH_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Branch name already in use"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 422 branching not enabled", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 422, + body: { message: "Preview branching is not enabled" }, + }); + const result = yield* run([ + "branches", + "create", + BRANCH_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Preview branching is not enabled"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 422 invalid region", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 422, + body: { message: "Invalid region" }, + }); + const result = yield* run([ + "branches", + "create", + BRANCH_NAME, + "--region", + "invalid-region", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("--region"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "branches", + "create", + BRANCH_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run([ + "branches", + "create", + BRANCH_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run([ + "branches", + "create", + BRANCH_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); }), - }); - const result = await run([ - "branches", - "create", - BRANCH_NAME, - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Preview branching is not enabled"); - }); - - testBehaviour("exits non-zero on 422 invalid region", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 422, body: { message: "Invalid region" } }), - }); - const result = await run([ - "branches", - "create", - BRANCH_NAME, - "--region", - "invalid-region", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("--region"); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "branches", - "create", - BRANCH_NAME, - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run([ - "branches", - "create", - BRANCH_NAME, - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run([ - "branches", - "create", - BRANCH_NAME, - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); + signal, + ), + ); }); describe("branches:get", () => { testBehaviour.skipIf(isRecording)( "returns single branch details", - async ({ run, projectRef }) => { - const result = await run(["branches", "get", BRANCH_NAME, `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain(BRANCH_NAME); - }, + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["branches", "get", BRANCH_NAME, `--remote=${projectRef}`]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain(BRANCH_NAME); + }), + signal, + ), ); testBehaviour.skipIf(isRecording)( "returns json output with --output json", - async ({ run, projectRef }) => { - const result = await run([ - "branches", - "get", - BRANCH_NAME, - "--output", - "json", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - const parsed = JSON.parse(result.stdout) as Record; - expect(parsed).toMatchObject({ SUPABASE_JWT_SECRET: expect.any(String) }); - }, - ); - - testBehaviour("exits non-zero on 404 branch not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Branch not found" } }), - }); - const result = await run(["branches", "get", "nonexistent", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Branch not found"); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run(["branches", "get", BRANCH_NAME, `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "branches", + "get", + BRANCH_NAME, + "--output", + "json", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + const parsed = yield* decodeJsonOutput(Schema.Unknown)(result.stdout); + expect(parsed).toMatchObject({ SUPABASE_JWT_SECRET: expect.any(String) }); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 branch not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Branch not found" }, + }); + const result = yield* run(["branches", "get", "nonexistent", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Branch not found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run(["branches", "get", BRANCH_NAME, `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); }); describe("branches:update", () => { - testBehaviour.skipIf(isRecording)("renames branch with --name", async ({ run, projectRef }) => { - const result = await run([ - "branches", - "update", - BRANCH_NAME, - "--name", - "renamed-branch", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stderr).toContain("Updated preview branch:"); - }); + testBehaviour.skipIf(isRecording)("renames branch with --name", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "branches", + "update", + BRANCH_NAME, + "--name", + "renamed-branch", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toContain("Updated preview branch:"); + }), + signal, + ), + ); testBehaviour.skipIf(isRecording)( "changes git branch with --git-branch", - async ({ run, projectRef }) => { - const result = await run([ - "branches", - "update", - BRANCH_NAME, - "--git-branch", - "feature/new", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stderr).toContain("Updated preview branch:"); - }, - ); - - testBehaviour("exits non-zero on 404 branch not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Branch not found" } }), - }); - const result = await run([ - "branches", - "update", - "nonexistent", - "--name", - "new-name", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Branch not found"); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "branches", - "update", - BRANCH_NAME, - "--name", - "new-name", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "branches", + "update", + BRANCH_NAME, + "--git-branch", + "feature/new", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toContain("Updated preview branch:"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 branch not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Branch not found" }, + }); + const result = yield* run([ + "branches", + "update", + "nonexistent", + "--name", + "new-name", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Branch not found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "branches", + "update", + BRANCH_NAME, + "--name", + "new-name", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); }); describe("branches:pause", () => { - testBehaviour.skipIf(isRecording)("pauses branch successfully", async ({ run, projectRef }) => { - const result = await run([ - "branches", - "pause", - BRANCH_NAME, - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - }); - - testBehaviour("exits non-zero on 404 branch not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Branch not found" } }), - }); - const result = await run([ - "branches", - "pause", - "nonexistent", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Branch not found"); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "branches", - "pause", - BRANCH_NAME, - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); + testBehaviour.skipIf(isRecording)("pauses branch successfully", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "branches", + "pause", + BRANCH_NAME, + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 branch not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Branch not found" }, + }); + const result = yield* run([ + "branches", + "pause", + "nonexistent", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Branch not found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "branches", + "pause", + BRANCH_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); }); describe("branches:unpause", () => { testBehaviour.skipIf(isRecording)( "unpauses branch successfully", - async ({ run, projectRef }) => { - const result = await run([ - "branches", - "unpause", - BRANCH_NAME, - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - }, - ); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "branches", - "unpause", - BRANCH_NAME, - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "branches", + "unpause", + BRANCH_NAME, + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "branches", + "unpause", + BRANCH_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); }); describe("branches:delete", () => { testBehaviour.skipIf(isRecording)( "deletes branch successfully", - async ({ run, projectRef }) => { - const result = await run([ - "branches", - "delete", - BRANCH_NAME, - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stderr).toContain("Deleted preview branch:"); - }, - ); - - testBehaviour("exits non-zero on 404 branch not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Branch not found" } }), - }); - const result = await run([ - "branches", - "delete", - "nonexistent", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Branch not found"); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "branches", - "delete", - BRANCH_NAME, - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "branches", + "delete", + BRANCH_NAME, + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toContain("Deleted preview branch:"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 branch not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Branch not found" }, + }); + const result = yield* run([ + "branches", + "delete", + "nonexistent", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Branch not found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "branches", + "delete", + BRANCH_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); }); describe("branches:disable", () => { - testBehaviour.skipIf(isRecording)("disables preview branching", async ({ run, projectRef }) => { - const result = await run(["branches", "disable", "--yes", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Disabled preview branching for project:"); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run(["branches", "disable", "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); + testBehaviour.skipIf(isRecording)("disables preview branching", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["branches", "disable", "--yes", `--remote=${projectRef}`]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Disabled preview branching for project:"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run(["branches", "disable", "--yes", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); }); }); diff --git a/apps/cli-e2e/src/tests/config-push.e2e.test.ts b/apps/cli-e2e/src/tests/config-push.e2e.test.ts index 3be38add6e..10604b33bf 100644 --- a/apps/cli-e2e/src/tests/config-push.e2e.test.ts +++ b/apps/cli-e2e/src/tests/config-push.e2e.test.ts @@ -1,18 +1,19 @@ -import { mkdirSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { Effect, FileSystem, Path } from "effect"; import { describe, expect } from "vitest"; import { PROJECT_REF } from "./env.ts"; -import { testBehaviour } from "./test-context.ts"; +import { postControl, runE2e, testBehaviour } from "./test-context.ts"; /** * Writes a supabase/config.toml covering every section `config push` * reconciles, each with a small diff against the recorded test project — a * section without a diff sends no PATCH/POST to the management API. */ -function writeConfigToml(dir: string): void { - mkdirSync(join(dir, "supabase"), { recursive: true }); - writeFileSync( - join(dir, "supabase", "config.toml"), +const writeConfigToml = Effect.fnUntraced(function* (dir: string) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fs.makeDirectory(path.join(dir, "supabase"), { recursive: true }); + yield* fs.writeFileString( + path.join(dir, "supabase", "config.toml"), ` project_id = "test-project" @@ -53,150 +54,179 @@ file_size_limit = "50MiB" enabled = true `.trimStart(), ); -} +}); // `--yes` skips the per-section y/n prompt, since the test process runs with stdin closed. describe("config push", () => { - testBehaviour("reconciles every section against the remote", async ({ run, workspace }) => { - writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).toBe(0); - }); - - testBehaviour("emits HTTP trace with --debug", async ({ run, workspace }) => { - writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", "--debug", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).toBe(0); - expect(result.stderr).toMatch(/HTTP.*GET:/); - }); - - testBehaviour("exits non-zero on 401 with token guidance", async ({ run, apiUrl, workspace }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - status: 401, - body: { message: "Invalid token" }, + testBehaviour("reconciles every section against the remote", ({ run, workspace, signal }) => + runE2e( + Effect.gen(function* () { + yield* writeConfigToml(workspace.path); + const result = yield* run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); + + testBehaviour("emits HTTP trace with --debug", ({ run, workspace, signal }) => + runE2e( + Effect.gen(function* () { + yield* writeConfigToml(workspace.path); + const result = yield* run([ + "config", + "push", + "--yes", + "--debug", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toMatch(/HTTP.*GET:/); }), - }); + signal, + ), + ); - writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("401"); - }); + testBehaviour("exits non-zero on 401 with token guidance", ({ run, apiUrl, workspace, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + + yield* writeConfigToml(workspace.path); + const result = yield* run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("401"); + }), + signal, + ), + ); testBehaviour( "exits non-zero on 403 with resource context", - async ({ run, apiUrl, workspace }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - status: 403, - body: { - message: `Forbidden: you do not have access to project ${PROJECT_REF}`, - }, + ({ run, apiUrl, workspace, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { + message: `Forbidden: you do not have access to project ${PROJECT_REF}`, + }, + }); + + yield* writeConfigToml(workspace.path); + const result = yield* run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("403"); }), - }); + signal, + ), + ); + + testBehaviour("exits non-zero on 404", ({ run, apiUrl, workspace, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Project not found" }, + }); + + yield* writeConfigToml(workspace.path); + const result = yield* run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("404"); + }), + signal, + ), + ); - writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("403"); - }, + testBehaviour("exits non-zero on 409", ({ run, apiUrl, workspace, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 409, + body: { message: "Conflict" }, + }); + + yield* writeConfigToml(workspace.path); + const result = yield* run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("409"); + }), + signal, + ), ); - testBehaviour("exits non-zero on 404", async ({ run, apiUrl, workspace }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - status: 404, - body: { message: "Project not found" }, + testBehaviour("exits non-zero on 422 with field detail", ({ run, apiUrl, workspace, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 422, + body: { + message: "Invalid config: max_rows must be a positive integer", + }, + }); + + yield* writeConfigToml(workspace.path); + const result = yield* run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("422"); + expect(result.stderr).toContain("max_rows"); }), - }); - - writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("404"); - }); - - testBehaviour("exits non-zero on 409", async ({ run, apiUrl, workspace }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 409, body: { message: "Conflict" } }), - }); - - writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("409"); - }); - - testBehaviour("exits non-zero on 422 with field detail", async ({ run, apiUrl, workspace }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - status: 422, - body: { - message: "Invalid config: max_rows must be a positive integer", - }, + signal, + ), + ); + + testBehaviour("exits non-zero on 429 after retrying", ({ run, apiUrl, workspace, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + + yield* writeConfigToml(workspace.path); + const result = yield* run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("429"); }), - }); - - writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("422"); - expect(result.stderr).toContain("max_rows"); - }); - - testBehaviour("exits non-zero on 429 after retrying", async ({ run, apiUrl, workspace }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - status: 429, - body: { message: "Too Many Requests" }, + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, workspace, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + + yield* writeConfigToml(workspace.path); + const result = yield* run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("500"); }), - }); - - writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("429"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl, workspace }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - status: 500, - body: { message: "Internal Server Error" }, + signal, + ), + ); + + testBehaviour("exits non-zero on 502", ({ run, apiUrl, workspace, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 502, + body: { message: "Bad Gateway" }, + }); + + yield* writeConfigToml(workspace.path); + const result = yield* run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("502"); }), - }); - - writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("500"); - }); - - testBehaviour("exits non-zero on 502", async ({ run, apiUrl, workspace }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 502, body: { message: "Bad Gateway" } }), - }); - - writeConfigToml(workspace.path); - const result = await run(["config", "push", "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("502"); - }); + signal, + ), + ); }); diff --git a/apps/cli-e2e/src/tests/config-toml.e2e.test.ts b/apps/cli-e2e/src/tests/config-toml.e2e.test.ts index 5275c838b8..121dc418aa 100644 --- a/apps/cli-e2e/src/tests/config-toml.e2e.test.ts +++ b/apps/cli-e2e/src/tests/config-toml.e2e.test.ts @@ -1,8 +1,7 @@ -import { mkdirSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { Effect, FileSystem, Path } from "effect"; import { describe, expect } from "vitest"; import { PROJECT_REF } from "./env.ts"; -import { testBehaviour } from "./test-context.ts"; +import { postControl, runE2e, testBehaviour } from "./test-context.ts"; // Regression guard: a non-string field written as `env(VAR)` (e.g. // a port) must not crash config decoding before env resolution runs. @@ -10,10 +9,12 @@ import { testBehaviour } from "./test-context.ts"; // A 401 is injected so the test doesn't need a real API fixture; it only // asserts that the CLI got past config decode. -function writeConfigWithEnvPorts(dir: string): void { - mkdirSync(join(dir, "supabase"), { recursive: true }); - writeFileSync( - join(dir, "supabase", "config.toml"), +const writeConfigWithEnvPorts = Effect.fnUntraced(function* (dir: string) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fs.makeDirectory(path.join(dir, "supabase"), { recursive: true }); + yield* fs.writeFileString( + path.join(dir, "supabase", "config.toml"), [ 'project_id = "with-env-ports"', "", @@ -28,7 +29,7 @@ function writeConfigWithEnvPorts(dir: string): void { "", ].join("\n"), ); -} +}); const ENV_PORTS = { SUPABASE_API_PORT: "54321", @@ -37,21 +38,25 @@ const ENV_PORTS = { }; describe("env-in-config-toml", () => { - testBehaviour("does not crash on numeric fields", async ({ run, workspace, apiUrl }) => { - writeConfigWithEnvPorts(workspace.path); + testBehaviour("does not crash on numeric fields", ({ run, workspace, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* writeConfigWithEnvPorts(workspace.path); - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); - const result = await run(["secrets", "list", `--remote=${PROJECT_REF}`], { - env: ENV_PORTS, - }); + const result = yield* run(["secrets", "list", `--remote=${PROJECT_REF}`], { + env: ENV_PORTS, + }); - const output = `${result.stdout}\n${result.stderr}`; - expect(output).not.toContain("CliConfigParseError"); - expect(output).not.toMatch(/Expected number.*env\(SUPABASE_/); - }); + const output = `${result.stdout}\n${result.stderr}`; + expect(output).not.toContain("CliConfigParseError"); + expect(output).not.toMatch(/Expected number.*env\(SUPABASE_/); + }), + signal, + ), + ); }); diff --git a/apps/cli-e2e/src/tests/database-core.e2e.test.ts b/apps/cli-e2e/src/tests/database-core.e2e.test.ts index 1a4f575c59..11c385925c 100644 --- a/apps/cli-e2e/src/tests/database-core.e2e.test.ts +++ b/apps/cli-e2e/src/tests/database-core.e2e.test.ts @@ -1,115 +1,154 @@ -import { mkdirSync, readdirSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { Effect, FileSystem, Path, Schema } from "effect"; import { describe, expect } from "vitest"; import { linkProject } from "./link-project.ts"; -import { testBehaviour } from "./test-context.ts"; +import { decodeJsonOutput, postControl, runE2e, testBehaviour } from "./test-context.ts"; const TEST_MIGRATION_SQL = "CREATE TABLE IF NOT EXISTS e2e_test_table (id bigint generated always as identity primary key);"; /** Create a single migration file in supabase/migrations/ and return the SQL. */ -function seedMigration(dir: string): void { - const migrationsDir = join(dir, "supabase", "migrations"); - mkdirSync(migrationsDir, { recursive: true }); - writeFileSync(join(migrationsDir, "20240101000000_e2e_test.sql"), TEST_MIGRATION_SQL); -} +const seedMigration = Effect.fnUntraced(function* (dir: string) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const migrationsDir = path.join(dir, "supabase", "migrations"); + yield* fs.makeDirectory(migrationsDir, { recursive: true }); + yield* fs.writeFileString( + path.join(migrationsDir, "20240101000000_e2e_test.sql"), + TEST_MIGRATION_SQL, + ); +}); describe("db advisors", () => { describe("db advisors:security", () => { - testBehaviour("returns security advisors", async ({ run, projectRef, workspace }) => { - linkProject(workspace.path, projectRef); - const result = await run(["db", "advisors", "--type", "security", "--remote"]); - expect(result.exitCode).toBe(0); - expect(result.stdout.trim()).toBe(""); - expect(result.stderr).toContain("No issues found"); - }); + testBehaviour("returns security advisors", ({ run, projectRef, workspace, signal }) => + runE2e( + Effect.gen(function* () { + yield* linkProject(workspace.path, projectRef); + const result = yield* run(["db", "advisors", "--type", "security", "--remote"]); + expect(result.exitCode).toBe(0); + expect(result.stdout.trim()).toBe(""); + expect(result.stderr).toContain("No issues found"); + }), + signal, + ), + ); testBehaviour( "exits zero when --fail-on error and no error-level advisors found", - async ({ run, projectRef, workspace }) => { - linkProject(workspace.path, projectRef); - const result = await run([ - "db", - "advisors", - "--type", - "security", - "--fail-on", - "error", - "--remote", - ]); - expect(result.exitCode).toBe(0); - }, + ({ run, projectRef, workspace, signal }) => + runE2e( + Effect.gen(function* () { + yield* linkProject(workspace.path, projectRef); + const result = yield* run([ + "db", + "advisors", + "--type", + "security", + "--fail-on", + "error", + "--remote", + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), ); - testBehaviour("exits non-zero on 401", async ({ run, projectRef, apiUrl, workspace }) => { - linkProject(workspace.path, projectRef); - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run(["db", "advisors", "--type", "security", "--remote"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); + testBehaviour("exits non-zero on 401", ({ run, projectRef, apiUrl, workspace, signal }) => + runE2e( + Effect.gen(function* () { + yield* linkProject(workspace.path, projectRef); + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run(["db", "advisors", "--type", "security", "--remote"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 403", async ({ run, projectRef, apiUrl, workspace }) => { - linkProject(workspace.path, projectRef); - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run(["db", "advisors", "--type", "security", "--remote"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); + testBehaviour("exits non-zero on 403", ({ run, projectRef, apiUrl, workspace, signal }) => + runE2e( + Effect.gen(function* () { + yield* linkProject(workspace.path, projectRef); + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run(["db", "advisors", "--type", "security", "--remote"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 429", async ({ run, projectRef, apiUrl, workspace }) => { - linkProject(workspace.path, projectRef); - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run(["db", "advisors", "--type", "security", "--remote"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); + testBehaviour("exits non-zero on 429", ({ run, projectRef, apiUrl, workspace, signal }) => + runE2e( + Effect.gen(function* () { + yield* linkProject(workspace.path, projectRef); + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run(["db", "advisors", "--type", "security", "--remote"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 500", async ({ run, projectRef, apiUrl, workspace }) => { - linkProject(workspace.path, projectRef); - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run(["db", "advisors", "--type", "security", "--remote"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + testBehaviour("exits non-zero on 500", ({ run, projectRef, apiUrl, workspace, signal }) => + runE2e( + Effect.gen(function* () { + yield* linkProject(workspace.path, projectRef); + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run(["db", "advisors", "--type", "security", "--remote"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); describe("db advisors:performance", () => { - testBehaviour("returns performance advisors", async ({ run, projectRef, workspace }) => { - linkProject(workspace.path, projectRef); - const result = await run(["db", "advisors", "--type", "performance", "--remote"]); - expect(result.exitCode).toBe(0); - if (result.stdout.trim()) { - const parsed = JSON.parse(result.stdout) as unknown[]; - expect(Array.isArray(parsed)).toBe(true); - } else { - expect(result.stderr).toContain("No issues found"); - } - }); + testBehaviour("returns performance advisors", ({ run, projectRef, workspace, signal }) => + runE2e( + Effect.gen(function* () { + yield* linkProject(workspace.path, projectRef); + const result = yield* run(["db", "advisors", "--type", "performance", "--remote"]); + expect(result.exitCode).toBe(0); + if (result.stdout.trim()) { + const parsed = yield* decodeJsonOutput(Schema.Unknown)(result.stdout); + expect(Array.isArray(parsed)).toBe(true); + } else { + expect(result.stderr).toContain("No issues found"); + } + }), + signal, + ), + ); }); describe("db advisors:all", () => { - testBehaviour("returns advisors with --type all", async ({ run, projectRef, workspace }) => { - linkProject(workspace.path, projectRef); - const result = await run(["db", "advisors", "--type", "all", "--remote"]); - expect(result.exitCode).toBe(0); - }); + testBehaviour("returns advisors with --type all", ({ run, projectRef, workspace, signal }) => + runE2e( + Effect.gen(function* () { + yield* linkProject(workspace.path, projectRef); + const result = yield* run(["db", "advisors", "--type", "all", "--remote"]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); }); }); @@ -117,57 +156,74 @@ describe("db query", () => { describe("db query:remote", () => { testBehaviour( "returns SELECT 1 result in table format", - async ({ run, projectRef, workspace }) => { - linkProject(workspace.path, projectRef); - const result = await run(["db", "query", "SELECT 1", "--read-only", "--remote"]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("1"); - }, + ({ run, projectRef, workspace, signal }) => + runE2e( + Effect.gen(function* () { + yield* linkProject(workspace.path, projectRef); + const result = yield* run(["db", "query", "SELECT 1", "--read-only", "--remote"]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("1"); + }), + signal, + ), ); - testBehaviour("returns JSON with --output json", async ({ run, projectRef, workspace }) => { - linkProject(workspace.path, projectRef); - const result = await run([ - "db", - "query", - "--output", - "json", - "SELECT 1", - "--read-only", - "--remote", - ]); - expect(result.exitCode).toBe(0); - const parsed = JSON.parse(result.stdout) as unknown; - // In agent mode (CLAUDECODE env set) the output is wrapped in {warning, boundary, rows}. - // In normal mode it's a plain array. - const rows = Array.isArray(parsed) ? parsed : (parsed as { rows: unknown[] }).rows; - expect(Array.isArray(rows)).toBe(true); - expect(rows).toHaveLength(1); - }); + testBehaviour("returns JSON with --output json", ({ run, projectRef, workspace, signal }) => + runE2e( + Effect.gen(function* () { + yield* linkProject(workspace.path, projectRef); + const result = yield* run([ + "db", + "query", + "--output", + "json", + "SELECT 1", + "--read-only", + "--remote", + ]); + expect(result.exitCode).toBe(0); + const parsed = yield* decodeJsonOutput(Schema.Unknown)(result.stdout); + // In agent mode (CLAUDECODE env set) the output is wrapped in {warning, boundary, rows}. + // In normal mode it's a plain array. + const rows = Array.isArray(parsed) ? parsed : (parsed as { rows: unknown[] }).rows; + expect(Array.isArray(rows)).toBe(true); + expect(rows).toHaveLength(1); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 401", async ({ run, projectRef, apiUrl, workspace }) => { - linkProject(workspace.path, projectRef); - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run(["db", "query", "SELECT 1", "--read-only", "--remote"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); + testBehaviour("exits non-zero on 401", ({ run, projectRef, apiUrl, workspace, signal }) => + runE2e( + Effect.gen(function* () { + yield* linkProject(workspace.path, projectRef); + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run(["db", "query", "SELECT 1", "--read-only", "--remote"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 403", async ({ run, projectRef, apiUrl, workspace }) => { - linkProject(workspace.path, projectRef); - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run(["db", "query", "SELECT 1", "--read-only", "--remote"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); + testBehaviour("exits non-zero on 403", ({ run, projectRef, apiUrl, workspace, signal }) => + runE2e( + Effect.gen(function* () { + yield* linkProject(workspace.path, projectRef); + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run(["db", "query", "SELECT 1", "--read-only", "--remote"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); }); }); @@ -175,96 +231,151 @@ describe("db push", () => { describe("db push:dry-run", () => { testBehaviour( "exits non-zero on connection refused with --dry-run", - async ({ run, projectRef, workspace }) => { - linkProject(workspace.path, projectRef); - seedMigration(workspace.path); - const result = await run(["db", "push", "--dry-run", "--remote"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("connect"); - }, + ({ run, projectRef, workspace, signal }) => + runE2e( + Effect.gen(function* () { + yield* linkProject(workspace.path, projectRef); + yield* seedMigration(workspace.path); + const result = yield* run(["db", "push", "--dry-run", "--remote"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("connect"); + }), + signal, + ), ); }); describe("db push:local", () => { - testBehaviour("exits non-zero on connection refused without --remote", async ({ run }) => { - const result = await run(["db", "push"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("connect"); - }); + testBehaviour("exits non-zero on connection refused without --remote", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["db", "push"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("connect"); + }), + signal, + ), + ); }); describe("db push:remote", () => { testBehaviour( "exits non-zero on connection refused with --remote", - async ({ run, projectRef, workspace }) => { - linkProject(workspace.path, projectRef); - const result = await run(["db", "push", "--remote"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("connect"); - }, + ({ run, projectRef, workspace, signal }) => + runE2e( + Effect.gen(function* () { + yield* linkProject(workspace.path, projectRef); + const result = yield* run(["db", "push", "--remote"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("connect"); + }), + signal, + ), ); }); }); describe("db pull", () => { - testBehaviour("exits non-zero on connection refused without --remote", async ({ run }) => { - const result = await run(["db", "pull"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("connect"); - }); + testBehaviour("exits non-zero on connection refused without --remote", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["db", "pull"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("connect"); + }), + signal, + ), + ); }); describe("db lint", () => { - testBehaviour("exits non-zero on connection refused without --remote", async ({ run }) => { - const result = await run(["db", "lint"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("connect"); - }); + testBehaviour("exits non-zero on connection refused without --remote", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["db", "lint"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("connect"); + }), + signal, + ), + ); }); describe("db dump", () => { - testBehaviour("exits non-zero on connection refused without --remote", async ({ run }) => { - const result = await run(["db", "dump"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("connect"); - }); + testBehaviour("exits non-zero on connection refused without --remote", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["db", "dump"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("connect"); + }), + signal, + ), + ); - testBehaviour("exits non-zero when --role-only and --data-only are both set", async ({ run }) => { - const result = await run(["db", "dump", "--role-only", "--data-only"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr.toLowerCase()).toMatch(/role.only|data.only|mutually exclusive/); - }); + testBehaviour("exits non-zero when --role-only and --data-only are both set", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["db", "dump", "--role-only", "--data-only"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr.toLowerCase()).toMatch(/role.only|data.only|mutually exclusive/); + }), + signal, + ), + ); }); describe("db reset", () => { - testBehaviour("exits non-zero on connection refused without --remote", async ({ run }) => { - const result = await run(["db", "reset"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toMatch(/connect|not running/i); - }); + testBehaviour("exits non-zero on connection refused without --remote", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["db", "reset"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toMatch(/connect|not running/i); + }), + signal, + ), + ); }); describe("test new", () => { - testBehaviour("creates a pgTAP test file", async ({ run, workspace }) => { - const result = await run(["test", "new", "my_test"]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toMatch(/my_test/); - const files = readdirSync(join(workspace.path, "supabase", "tests")).filter((f) => - f.endsWith("my_test_test.sql"), - ); - expect(files.length).toBe(1); - }); + testBehaviour("creates a pgTAP test file", ({ run, workspace, signal }) => + runE2e( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const result = yield* run(["test", "new", "my_test"]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/my_test/); + const files = (yield* fs.readDirectory( + path.join(workspace.path, "supabase", "tests"), + )).filter((f) => f.endsWith("my_test_test.sql")); + expect(files.length).toBe(1); + }), + signal, + ), + ); - testBehaviour("exits non-zero when name argument is missing", async ({ run }) => { - const result = await run(["test", "new"]); - expect(result.exitCode).not.toBe(0); - }); + testBehaviour("exits non-zero when name argument is missing", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["test", "new"]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); }); describe("test db", () => { - testBehaviour("exits non-zero on connection refused without --remote", async ({ run }) => { - const result = await run(["test", "db"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("connect"); - }); + testBehaviour("exits non-zero on connection refused without --remote", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["test", "db"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("connect"); + }), + signal, + ), + ); }); diff --git a/apps/cli-e2e/src/tests/domains.e2e.test.ts b/apps/cli-e2e/src/tests/domains.e2e.test.ts index e2244691e6..30cc4b6384 100644 --- a/apps/cli-e2e/src/tests/domains.e2e.test.ts +++ b/apps/cli-e2e/src/tests/domains.e2e.test.ts @@ -1,6 +1,7 @@ +import { Effect, Schema } from "effect"; import { describe, expect } from "vitest"; import { REPLAY_PROJECT_REF } from "../server/placeholder.ts"; -import { testBehaviour } from "./test-context"; +import { decodeJsonOutput, postControl, runE2e, testBehaviour } from "./test-context"; import { isRecording, PROJECT_REF } from "./env"; const CONFIGURED_CNAME = "www.urgsimurksi.xyz"; @@ -9,258 +10,355 @@ describe("domains", () => { describe.todo("domains:create — requires mocking of 1.1.1.1 for DNS queries"); describe("domains:get", () => { - testBehaviour("custom domain disabled", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - status: 400, - body: { message: "Please enable custom domains first" }, + testBehaviour("custom domain disabled", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 400, + body: { message: "Please enable custom domains first" }, + }); + + const result = yield* run(["domains", "get", `--remote=${PROJECT_REF}`]); + + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("400"); }), - }); - - const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); - - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("400"); - }); - - testBehaviour.skipIf(isRecording)("no custom domain", async ({ run }) => { - const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); - - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("400"); - }); - - testBehaviour.skipIf(isRecording)("pending verification", async ({ run }) => { - const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); - - expect(result.exitCode).toBe(0); - expect(result.stderr).toContain( - "_acme-challenge.www.urgsimurksi.xyz TXT -> dx8wOwXMeAgc7uOQ3q0RlSQKvGl_HhcIsph_9PqwQYw", - ); - }); - - testBehaviour.skipIf(isRecording)("verification completed", async ({ run }) => { - const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); - - expect(result.exitCode).toBe(0); - expect(result.stderr).toContain( - `www.urgsimurksi.xyz CNAME -> ${REPLAY_PROJECT_REF}.supabase.red`, - ); - }); - - testBehaviour.skipIf(isRecording)("domain activated", async ({ run }) => { - const result = await run(["domains", "get", "--output", "json", `--remote=${PROJECT_REF}`]); - - expect(result.exitCode).toBe(0); - - expect(JSON.parse(result.stdout)).toEqual({ - custom_hostname: "www.urgsimurksi.xyz", - data: { - errors: [], - messages: [], - result: { - custom_origin_server: `${REPLAY_PROJECT_REF}.supabase.red`, - hostname: "www.urgsimurksi.xyz", - id: "00000000-0000-0000-0000-000000000000", - ownership_verification: { - name: "", - type: "", - value: "", - }, - ssl: { - status: "active", - validation_records: [], + signal, + ), + ); + + testBehaviour.skipIf(isRecording)("no custom domain", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["domains", "get", `--remote=${PROJECT_REF}`]); + + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("400"); + }), + signal, + ), + ); + + testBehaviour.skipIf(isRecording)("pending verification", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["domains", "get", `--remote=${PROJECT_REF}`]); + + expect(result.exitCode).toBe(0); + expect(result.stderr).toContain( + "_acme-challenge.www.urgsimurksi.xyz TXT -> dx8wOwXMeAgc7uOQ3q0RlSQKvGl_HhcIsph_9PqwQYw", + ); + }), + signal, + ), + ); + + testBehaviour.skipIf(isRecording)("verification completed", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["domains", "get", `--remote=${PROJECT_REF}`]); + + expect(result.exitCode).toBe(0); + expect(result.stderr).toContain( + `www.urgsimurksi.xyz CNAME -> ${REPLAY_PROJECT_REF}.supabase.red`, + ); + }), + signal, + ), + ); + + testBehaviour.skipIf(isRecording)("domain activated", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "domains", + "get", + "--output", + "json", + `--remote=${PROJECT_REF}`, + ]); + + expect(result.exitCode).toBe(0); + + expect(yield* decodeJsonOutput(Schema.Unknown)(result.stdout)).toEqual({ + custom_hostname: "www.urgsimurksi.xyz", + data: { + errors: [], + messages: [], + result: { + custom_origin_server: `${REPLAY_PROJECT_REF}.supabase.red`, + hostname: "www.urgsimurksi.xyz", + id: "00000000-0000-0000-0000-000000000000", + ownership_verification: { + name: "", + type: "", + value: "", + }, + ssl: { + status: "active", + validation_records: [], + }, + status: "active", + }, + success: true, }, - status: "active", - }, - success: true, - }, - status: "5_services_reconfigured", - }); - }); - - testBehaviour("exists non-zero on 403", async ({ apiUrl, run }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - status: 403, - body: { message: "Unauthorized" }, + status: "5_services_reconfigured", + }); + }), + signal, + ), + ); + + testBehaviour("exists non-zero on 403", ({ apiUrl, run, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Unauthorized" }, + }); + + const result = yield* run(["domains", "get", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("403"); }), - }); - - const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("403"); - }); - - testBehaviour("exists non-zero on 401", async ({ apiUrl, run }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - status: 401, - body: { message: "Unauthorized" }, + signal, + ), + ); + + testBehaviour("exists non-zero on 401", ({ apiUrl, run, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Unauthorized" }, + }); + + const result = yield* run(["domains", "get", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("401"); }), - }); - - const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("401"); - }); - - testBehaviour("exists non-zero on 429", async ({ apiUrl, run }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - status: 429, - body: { message: "Too Many Requests" }, + signal, + ), + ); + + testBehaviour("exists non-zero on 429", ({ apiUrl, run, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + + const result = yield* run(["domains", "get", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("429"); }), - }); - - const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("429"); - }); - - testBehaviour("exists non-zero on 500", async ({ apiUrl, run }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - status: 500, - body: { message: "Internal Server Error" }, + signal, + ), + ); + + testBehaviour("exists non-zero on 500", ({ apiUrl, run, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + + const result = yield* run(["domains", "get", `--remote=${PROJECT_REF}`]); + + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("500"); }), - }); + signal, + ), + ); + + testBehaviour("exists non-zero on 502", ({ apiUrl, run, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 502, + body: { message: "Bad Gateway" }, + }); + + const result = yield* run(["domains", "get", `--remote=${PROJECT_REF}`]); + + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("502"); + }), + signal, + ), + ); + }); - const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); + describe("domains:reverify", () => { + testBehaviour("custom domain disabled", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["domains", "reverify", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("500"); - }); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("400"); + }), + signal, + ), + ); - testBehaviour("exists non-zero on 502", async ({ apiUrl, run }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - status: 502, - body: { message: "Bad Gateway" }, + testBehaviour.skipIf(isRecording)("no custom domain", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["domains", "reverify", `--remote=${PROJECT_REF}`]); + + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("400"); + }), + signal, + ), + ); + + testBehaviour.skipIf(isRecording)("pending verification", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["domains", "reverify", `--remote=${PROJECT_REF}`]); + + expect(result.exitCode).toBe(0); + expect(result.stderr).toContain( + "_acme-challenge.www.urgsimurksi.xyz TXT -> dx8wOwXMeAgc7uOQ3q0RlSQKvGl_HhcIsph_9PqwQYw", + ); }), - }); + signal, + ), + ); - const result = await run(["domains", "get", `--remote=${PROJECT_REF}`]); + testBehaviour.skipIf(isRecording)("verification completed", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["domains", "reverify", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("502"); - }); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("400"); + }), + signal, + ), + ); }); + describe("domains:activate", () => { + testBehaviour("custom domain disabled", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["domains", "activate", "--yes", `--remote=${PROJECT_REF}`]); - describe("domains:reverify", () => { - testBehaviour("custom domain disabled", async ({ run }) => { - const result = await run(["domains", "reverify", `--remote=${PROJECT_REF}`]); - - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("400"); - }); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("400"); + }), + signal, + ), + ); - testBehaviour.skipIf(isRecording)("no custom domain", async ({ run }) => { - const result = await run(["domains", "reverify", `--remote=${PROJECT_REF}`]); + testBehaviour.skipIf(isRecording)("no custom domain", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["domains", "activate", "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("400"); - }); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("400"); + }), + signal, + ), + ); - testBehaviour.skipIf(isRecording)("pending verification", async ({ run }) => { - const result = await run(["domains", "reverify", `--remote=${PROJECT_REF}`]); + testBehaviour.skipIf(isRecording)("pending verification", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["domains", "activate", "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).toBe(0); - expect(result.stderr).toContain( - "_acme-challenge.www.urgsimurksi.xyz TXT -> dx8wOwXMeAgc7uOQ3q0RlSQKvGl_HhcIsph_9PqwQYw", - ); - }); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("400"); + }), + signal, + ), + ); + + testBehaviour.skipIf(isRecording)("pending verification in debug mode", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "domains", + "activate", + "--debug", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toMatch(/HTTP.*POST:/); + expect(result.stderr).toContain("400"); + }), + signal, + ), + ); + + testBehaviour.skipIf(isRecording)("verification completed", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["domains", "activate", "--yes", `--remote=${PROJECT_REF}`]); + + expect(result.exitCode).toBe(0); + expect(result.stderr).toContain(`completed`); + expect(result.stderr).toContain(`at ${CONFIGURED_CNAME}`); + }), + signal, + ), + ); - testBehaviour.skipIf(isRecording)("verification completed", async ({ run }) => { - const result = await run(["domains", "reverify", `--remote=${PROJECT_REF}`]); + testBehaviour.skipIf(isRecording)("domain activated", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["domains", "activate", "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("400"); - }); - }); - describe("domains:activate", () => { - testBehaviour("custom domain disabled", async ({ run }) => { - const result = await run(["domains", "activate", "--yes", `--remote=${PROJECT_REF}`]); - - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("400"); - }); - - testBehaviour.skipIf(isRecording)("no custom domain", async ({ run }) => { - const result = await run(["domains", "activate", "--yes", `--remote=${PROJECT_REF}`]); - - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("400"); - }); - - testBehaviour.skipIf(isRecording)("pending verification", async ({ run }) => { - const result = await run(["domains", "activate", "--yes", `--remote=${PROJECT_REF}`]); - - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("400"); - }); - - testBehaviour.skipIf(isRecording)("pending verification in debug mode", async ({ run }) => { - const result = await run([ - "domains", - "activate", - "--debug", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toMatch(/HTTP.*POST:/); - expect(result.stderr).toContain("400"); - }); - - testBehaviour.skipIf(isRecording)("verification completed", async ({ run }) => { - const result = await run(["domains", "activate", "--yes", `--remote=${PROJECT_REF}`]); - - expect(result.exitCode).toBe(0); - expect(result.stderr).toContain(`completed`); - expect(result.stderr).toContain(`at ${CONFIGURED_CNAME}`); - }); - - testBehaviour.skipIf(isRecording)("domain activated", async ({ run }) => { - const result = await run(["domains", "activate", "--yes", `--remote=${PROJECT_REF}`]); - - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("400"); - }); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("400"); + }), + signal, + ), + ); }); describe("domains:delete", () => { - testBehaviour("custom domain disabled", async ({ run }) => { - const result = await run(["domains", "delete", "--yes", `--remote=${PROJECT_REF}`]); + testBehaviour("custom domain disabled", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["domains", "delete", "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("400"); - }); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("400"); + }), + signal, + ), + ); - testBehaviour.skipIf(isRecording)("no custom domain", async ({ run }) => { - const result = await run(["domains", "delete", "--yes", `--remote=${PROJECT_REF}`]); + testBehaviour.skipIf(isRecording)("no custom domain", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["domains", "delete", "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("400"); - }); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("400"); + }), + signal, + ), + ); - testBehaviour.skipIf(isRecording)("pending verification", async ({ run }) => { - const result = await run(["domains", "delete", "--yes", `--remote=${PROJECT_REF}`]); + testBehaviour.skipIf(isRecording)("pending verification", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["domains", "delete", "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).toBe(0); - }); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); }); }); diff --git a/apps/cli-e2e/src/tests/functions.e2e.test.ts b/apps/cli-e2e/src/tests/functions.e2e.test.ts index af2a08db8e..fc2d40390d 100644 --- a/apps/cli-e2e/src/tests/functions.e2e.test.ts +++ b/apps/cli-e2e/src/tests/functions.e2e.test.ts @@ -1,351 +1,453 @@ +import { Effect, Schema } from "effect"; import { describe, expect } from "vitest"; import { isRecording, PROJECT_REF } from "./env.ts"; -import { testBehaviour } from "./test-context.ts"; +import { decodeJsonOutput, postControl, runE2e, testBehaviour } from "./test-context.ts"; const FUNCTION_NAME = "hello-world"; describe("functions", () => { describe("functions:list", () => { - testBehaviour("renders fixture data in output", async ({ run, projectRef }) => { - const result = await run(["functions", "list", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("NAME"); - expect(result.stdout).toContain("STATUS"); - }); + testBehaviour("renders fixture data in output", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["functions", "list", `--remote=${projectRef}`]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("NAME"); + expect(result.stdout).toContain("STATUS"); + }), + signal, + ), + ); - testBehaviour("returns json output with --output json", async ({ run, projectRef }) => { - const result = await run(["functions", "list", "--output", "json", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - const parsed = JSON.parse(result.stdout) as unknown[]; - expect(Array.isArray(parsed)).toBe(true); - }); + testBehaviour("returns json output with --output json", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "functions", + "list", + "--output", + "json", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + const parsed = yield* decodeJsonOutput(Schema.Unknown)(result.stdout); + expect(Array.isArray(parsed)).toBe(true); + }), + signal, + ), + ); - testBehaviour("includes debug output with --debug", async ({ run, projectRef }) => { - const result = await run(["functions", "list", "--debug", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(result.stderr).toMatch(/HTTP.*GET:/); - }); + testBehaviour("includes debug output with --debug", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["functions", "list", "--debug", `--remote=${projectRef}`]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toMatch(/HTTP.*GET:/); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run(["functions", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - }); + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run(["functions", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run(["functions", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run(["functions", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run(["functions", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run(["functions", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run(["functions", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run(["functions", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); describe("functions:deploy", () => { // Deploy bundles function files locally before any API call, so error injection tests // pre-create the function with `functions new` first. - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await run(["functions", "new", FUNCTION_NAME]); - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "functions", - "deploy", - FUNCTION_NAME, - "--use-api", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* run(["functions", "new", FUNCTION_NAME]); + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "functions", + "deploy", + FUNCTION_NAME, + "--use-api", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await run(["functions", "new", FUNCTION_NAME]); - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run([ - "functions", - "deploy", - FUNCTION_NAME, - "--use-api", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* run(["functions", "new", FUNCTION_NAME]); + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run([ + "functions", + "deploy", + FUNCTION_NAME, + "--use-api", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await run(["functions", "new", FUNCTION_NAME]); - await fetch(`${apiUrl}/_ctrl/rate-limit`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - path: `/v1/projects/${PROJECT_REF}/functions/deploy`, - retryAfterSeconds: 0, + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* run(["functions", "new", FUNCTION_NAME]); + yield* postControl(`${apiUrl}/_ctrl/rate-limit`, { + path: `/v1/projects/${PROJECT_REF}/functions/deploy`, + retryAfterSeconds: 0, + }); + const result = yield* run([ + "functions", + "deploy", + FUNCTION_NAME, + "--use-api", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); }), - }); - const result = await run([ - "functions", - "deploy", - FUNCTION_NAME, - "--use-api", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); + signal, + ), + ); - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await run(["functions", "new", FUNCTION_NAME]); - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run([ - "functions", - "deploy", - FUNCTION_NAME, - "--use-api", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* run(["functions", "new", FUNCTION_NAME]); + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run([ + "functions", + "deploy", + FUNCTION_NAME, + "--use-api", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); describe("functions:delete", () => { testBehaviour.skipIf(isRecording)( "deletes function successfully", - async ({ run, projectRef }) => { - const result = await run([ - "functions", - "delete", - FUNCTION_NAME, - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Deleted Function"); - }, + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "functions", + "delete", + FUNCTION_NAME, + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Deleted Function"); + }), + signal, + ), ); - testBehaviour("exits non-zero on 404 function not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Function not found" } }), - }); - const result = await run([ - "functions", - "delete", - FUNCTION_NAME, - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("does not exist"); - }); + testBehaviour("exits non-zero on 404 function not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Function not found" }, + }); + const result = yield* run([ + "functions", + "delete", + FUNCTION_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("does not exist"); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "functions", - "delete", - FUNCTION_NAME, - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "functions", + "delete", + FUNCTION_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run([ - "functions", - "delete", - FUNCTION_NAME, - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run([ + "functions", + "delete", + FUNCTION_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run([ - "functions", - "delete", - FUNCTION_NAME, - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run([ + "functions", + "delete", + FUNCTION_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run([ - "functions", - "delete", - FUNCTION_NAME, - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run([ + "functions", + "delete", + FUNCTION_NAME, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); describe("functions:download", () => { testBehaviour.skipIf(isRecording)( "downloads function successfully", - async ({ run, projectRef }) => { - const result = await run([ - "functions", - "download", - FUNCTION_NAME, - "--use-api", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stderr).toContain("Downloaded Function"); - }, + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "functions", + "download", + FUNCTION_NAME, + "--use-api", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toContain("Downloaded Function"); + }), + signal, + ), ); - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "functions", - "download", - FUNCTION_NAME, - "--use-api", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "functions", + "download", + FUNCTION_NAME, + "--use-api", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run([ - "functions", - "download", - FUNCTION_NAME, - "--use-api", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run([ + "functions", + "download", + FUNCTION_NAME, + "--use-api", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 404 function not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Function not found" } }), - }); - const result = await run([ - "functions", - "download", - FUNCTION_NAME, - "--use-api", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Function not found"); - }); + testBehaviour("exits non-zero on 404 function not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Function not found" }, + }); + const result = yield* run([ + "functions", + "download", + FUNCTION_NAME, + "--use-api", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Function not found"); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run([ - "functions", - "download", - FUNCTION_NAME, - "--use-api", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run([ + "functions", + "download", + FUNCTION_NAME, + "--use-api", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); describe("functions:new", () => { - testBehaviour("successfully creates a new function", async ({ run }) => { - const result = await run(["functions", "new", "testFunction"]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Created new Function at supabase/functions/testFunction"); - }); + testBehaviour("successfully creates a new function", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["functions", "new", "testFunction"]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain( + "Created new Function at supabase/functions/testFunction", + ); + }), + signal, + ), + ); }); }); diff --git a/apps/cli-e2e/src/tests/gen.e2e.test.ts b/apps/cli-e2e/src/tests/gen.e2e.test.ts index 95a24b5eec..f6a1bd7bc3 100644 --- a/apps/cli-e2e/src/tests/gen.e2e.test.ts +++ b/apps/cli-e2e/src/tests/gen.e2e.test.ts @@ -1,6 +1,7 @@ +import { Effect, Schema } from "effect"; import { describe, expect } from "vitest"; import { isRecording, PROJECT_REF } from "./env.ts"; -import { testBehaviour } from "./test-context.ts"; +import { decodeJsonOutput, postControl, runE2e, testBehaviour } from "./test-context.ts"; function decodeJwtPart(part: string): Record { const padded = part + "=".repeat((4 - (part.length % 4)) % 4); @@ -11,147 +12,204 @@ describe("gen", () => { describe("gen:types", () => { testBehaviour.skipIf(isRecording)( "generates typescript types from project", - async ({ run, projectRef }) => { - // #5212 — with CLAUDECODE=1, piped/redirected output must not include the - // plugin hint (would break `gen types > file.ts` and similar captures). - const result = await run(["gen", "types", `--remote=${projectRef}`], { - env: { CLAUDECODE: "1" }, - }); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("export type Json"); - expect(result.stdout).toContain("export type Database"); - expect(result.stdout).not.toMatch(/claude-code-hint/); - expect(result.stderr).not.toMatch(/claude-code-hint/); - }, + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + // #5212 — with CLAUDECODE=1, piped/redirected output must not include the + // plugin hint (would break `gen types > file.ts` and similar captures). + const result = yield* run(["gen", "types", `--remote=${projectRef}`], { + env: { CLAUDECODE: "1" }, + }); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("export type Json"); + expect(result.stdout).toContain("export type Database"); + expect(result.stdout).not.toMatch(/claude-code-hint/); + expect(result.stderr).not.toMatch(/claude-code-hint/); + }), + signal, + ), ); testBehaviour.skipIf(isRecording)( "includes debug output with --debug", - async ({ run, projectRef }) => { - const result = await run(["gen", "types", "--debug", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(result.stderr).toMatch(/HTTP.*GET:/); - }, + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["gen", "types", "--debug", `--remote=${projectRef}`]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toMatch(/HTTP.*GET:/); + }), + signal, + ), ); - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run(["gen", "types", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run(["gen", "types", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run(["gen", "types", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run(["gen", "types", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 404 project not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), - }); - const result = await run(["gen", "types", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Project not found"); - }); + testBehaviour("exits non-zero on 404 project not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Project not found" }, + }); + const result = yield* run(["gen", "types", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Project not found"); + }), + signal, + ), + ); testBehaviour( "exits non-zero when the local stack is not running", - async ({ runNoProjectId }) => { - const result = await runNoProjectId(["gen", "types"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Target: local stack"); - expect(result.stderr).toContain("supabase start is not running"); - }, + ({ runNoProjectId, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* runNoProjectId(["gen", "types"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Target: local stack"); + expect(result.stderr).toContain("supabase start is not running"); + }), + signal, + ), ); }); describe("gen:signing-key", () => { - testBehaviour("generates ES256 signing key by default", async ({ run }) => { - const result = await run(["gen", "signing-key"]); - expect(result.exitCode).toBe(0); - const key = JSON.parse(result.stdout) as Record; - expect(key["kty"]).toBe("EC"); - expect(key["alg"]).toBe("ES256"); - expect(key["crv"]).toBe("P-256"); - expect(key["use"]).toBe("sig"); - expect(typeof key["d"]).toBe("string"); - expect((key["d"] as string).length).toBeGreaterThan(0); - }); + testBehaviour("generates ES256 signing key by default", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["gen", "signing-key"]); + expect(result.exitCode).toBe(0); + const key = yield* decodeJsonOutput(Schema.Record(Schema.String, Schema.Unknown))( + result.stdout, + ); + expect(key["kty"]).toBe("EC"); + expect(key["alg"]).toBe("ES256"); + expect(key["crv"]).toBe("P-256"); + expect(key["use"]).toBe("sig"); + expect(typeof key["d"]).toBe("string"); + expect((key["d"] as string).length).toBeGreaterThan(0); + }), + signal, + ), + ); - testBehaviour("generates RS256 signing key with --algorithm RS256", async ({ run }) => { - const result = await run(["gen", "signing-key", "--algorithm", "RS256"]); - expect(result.exitCode).toBe(0); - const key = JSON.parse(result.stdout) as Record; - expect(key["kty"]).toBe("RSA"); - expect(key["alg"]).toBe("RS256"); - expect(key["use"]).toBe("sig"); - expect(typeof key["n"]).toBe("string"); - expect((key["n"] as string).length).toBeGreaterThan(0); - }); + testBehaviour("generates RS256 signing key with --algorithm RS256", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["gen", "signing-key", "--algorithm", "RS256"]); + expect(result.exitCode).toBe(0); + const key = yield* decodeJsonOutput(Schema.Record(Schema.String, Schema.Unknown))( + result.stdout, + ); + expect(key["kty"]).toBe("RSA"); + expect(key["alg"]).toBe("RS256"); + expect(key["use"]).toBe("sig"); + expect(typeof key["n"]).toBe("string"); + expect((key["n"] as string).length).toBeGreaterThan(0); + }), + signal, + ), + ); }); describe("gen:bearer-jwt", () => { - testBehaviour("exits non-zero without --role", async ({ run }) => { - const result = await run(["gen", "bearer-jwt"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain('"role"'); - }); + testBehaviour("exits non-zero without --role", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["gen", "bearer-jwt"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain('"role"'); + }), + signal, + ), + ); - testBehaviour("generates bearer jwt for anon role", async ({ run }) => { - const result = await run(["gen", "bearer-jwt", "--role", "anon"]); - expect(result.exitCode).toBe(0); - const parts = result.stdout.trim().split("."); - expect(parts).toHaveLength(3); - const header = decodeJwtPart(parts[0]!); - expect(header["alg"]).toBe("ES256"); - expect(typeof header["kid"]).toBe("string"); - const payload = decodeJwtPart(parts[1]!); - expect(payload["role"]).toBe("anon"); - expect(typeof payload["exp"]).toBe("number"); - expect(typeof payload["iat"]).toBe("number"); - }); + testBehaviour("generates bearer jwt for anon role", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["gen", "bearer-jwt", "--role", "anon"]); + expect(result.exitCode).toBe(0); + const parts = result.stdout.trim().split("."); + expect(parts).toHaveLength(3); + const header = decodeJwtPart(parts[0]!); + expect(header["alg"]).toBe("ES256"); + expect(typeof header["kid"]).toBe("string"); + const payload = decodeJwtPart(parts[1]!); + expect(payload["role"]).toBe("anon"); + expect(typeof payload["exp"]).toBe("number"); + expect(typeof payload["iat"]).toBe("number"); + }), + signal, + ), + ); - testBehaviour("generates bearer jwt with custom validity", async ({ run }) => { - const result = await run(["gen", "bearer-jwt", "--role", "anon", "--valid-for", "1h"]); - expect(result.exitCode).toBe(0); - const parts = result.stdout.trim().split("."); - expect(parts).toHaveLength(3); - const payload = decodeJwtPart(parts[1]!); - expect(payload["role"]).toBe("anon"); - expect((payload["exp"] as number) - (payload["iat"] as number)).toBe(3600); - }); + testBehaviour("generates bearer jwt with custom validity", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["gen", "bearer-jwt", "--role", "anon", "--valid-for", "1h"]); + expect(result.exitCode).toBe(0); + const parts = result.stdout.trim().split("."); + expect(parts).toHaveLength(3); + const payload = decodeJwtPart(parts[1]!); + expect(payload["role"]).toBe("anon"); + expect((payload["exp"] as number) - (payload["iat"] as number)).toBe(3600); + }), + signal, + ), + ); testBehaviour( "generates bearer jwt for authenticated role with custom sub", - async ({ run }) => { - const result = await run([ - "gen", - "bearer-jwt", - "--role", - "authenticated", - "--sub", - "user-123", - ]); - expect(result.exitCode).toBe(0); - const parts = result.stdout.trim().split("."); - expect(parts).toHaveLength(3); - const payload = decodeJwtPart(parts[1]!); - expect(payload["role"]).toBe("authenticated"); - expect(payload["sub"]).toBe("user-123"); - }, + ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "gen", + "bearer-jwt", + "--role", + "authenticated", + "--sub", + "user-123", + ]); + expect(result.exitCode).toBe(0); + const parts = result.stdout.trim().split("."); + expect(parts).toHaveLength(3); + const payload = decodeJwtPart(parts[1]!); + expect(payload["role"]).toBe("authenticated"); + expect(payload["sub"]).toBe("user-123"); + }), + signal, + ), ); }); }); diff --git a/apps/cli-e2e/src/tests/inspect.e2e.test.ts b/apps/cli-e2e/src/tests/inspect.e2e.test.ts index bdb9bd5311..12f3e4d9d2 100644 --- a/apps/cli-e2e/src/tests/inspect.e2e.test.ts +++ b/apps/cli-e2e/src/tests/inspect.e2e.test.ts @@ -1,24 +1,19 @@ -import { mkdirSync, readdirSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { Effect, FileSystem, Path } from "effect"; import { describe, expect } from "vitest"; -import { testBehaviour } from "./test-context.ts"; +import { postControl, runE2e, testBehaviour } from "./test-context.ts"; -function setupInspectWorkspace(dir: string, pgPort: number): void { - mkdirSync(join(dir, "supabase"), { recursive: true }); - writeFileSync( - join(dir, "supabase", "config.toml"), +const setupInspectWorkspace = Effect.fnUntraced(function* (dir: string, pgPort: number) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fs.makeDirectory(path.join(dir, "supabase"), { recursive: true }); + yield* fs.writeFileString( + path.join(dir, "supabase", "config.toml"), ['project_id = "test-project"', "", "[db]", `port = ${pgPort}`].join("\n"), ); -} +}); -async function setPgFixture(apiUrl: string, key: string): Promise { - const res = await fetch(`${apiUrl}/_ctrl/pg-fixture`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ key }), - }); - if (!res.ok) throw new Error(`Failed to set PG fixture "${key}": ${await res.text()}`); -} +const setPgFixture = (apiUrl: string, key: string) => + postControl(`${apiUrl}/_ctrl/pg-fixture`, { key }); const SUBCOMMANDS = [ { name: "db-stats", fixtureKey: "db-stats", assertValue: "42 MB" }, @@ -37,70 +32,96 @@ const SUBCOMMANDS = [ ] as const; describe("inspect:flags", () => { - testBehaviour("rejects --remote with --local", async ({ run, workspace, pgMockPort }) => { - setupInspectWorkspace(workspace.path, pgMockPort); - const result = await run([ - "inspect", - "db", - "db-stats", - "--local", - "--remote=postgresql://postgres:postgres@localhost:5432/postgres", - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain( - "Only one of --remote, --linked, --db-url and --local can be set; got --remote and --local.", - ); - }); + testBehaviour("rejects --remote with --local", ({ run, workspace, pgMockPort, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupInspectWorkspace(workspace.path, pgMockPort); + const result = yield* run([ + "inspect", + "db", + "db-stats", + "--local", + "--remote=postgresql://postgres:postgres@localhost:5432/postgres", + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain( + "Only one of --remote, --linked, --db-url and --local can be set; got --remote and --local.", + ); + }), + signal, + ), + ); }); for (const { name, fixtureKey, assertValue } of SUBCOMMANDS) { describe(`inspect:db:${name}`, () => { testBehaviour( "renders query results as a table", - async ({ run, workspace, apiUrl, pgMockPort }) => { - setupInspectWorkspace(workspace.path, pgMockPort); - await setPgFixture(apiUrl, fixtureKey); - const result = await run(["inspect", "db", name]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain(assertValue); - }, + ({ run, workspace, apiUrl, pgMockPort, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupInspectWorkspace(workspace.path, pgMockPort); + yield* setPgFixture(apiUrl, fixtureKey); + const result = yield* run(["inspect", "db", name]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain(assertValue); + }), + signal, + ), ); - testBehaviour("exits non-zero on connection refused", async ({ run }) => { - const result = await run([ - "inspect", - "db", - name, - "--remote=postgresql://postgres:postgres@localhost:1/postgres", - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).not.toBe(""); - }); + testBehaviour("exits non-zero on connection refused", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "inspect", + "db", + name, + "--remote=postgresql://postgres:postgres@localhost:1/postgres", + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).not.toBe(""); + }), + signal, + ), + ); }); } describe("inspect:report", () => { - testBehaviour("saves CSV files on success", async ({ run, workspace, pgMockPort }) => { - setupInspectWorkspace(workspace.path, pgMockPort); - const outDir = join(workspace.path, "report-out"); - const result = await run(["inspect", "report", "--output-dir", outDir]); - expect(result.exitCode).toBe(0); - expect(result.stderr).toContain("Reports saved to"); - const subdirs = readdirSync(outDir); - expect(subdirs.length).toBe(1); - const dateDir = subdirs[0]!; - const csvFiles = readdirSync(join(outDir, dateDir)); - expect(csvFiles.length).toBeGreaterThan(0); - expect(csvFiles.every((f) => f.endsWith(".csv"))).toBe(true); - }); + testBehaviour("saves CSV files on success", ({ run, workspace, pgMockPort, signal }) => + runE2e( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* setupInspectWorkspace(workspace.path, pgMockPort); + const outDir = path.join(workspace.path, "report-out"); + const result = yield* run(["inspect", "report", "--output-dir", outDir]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toContain("Reports saved to"); + const subdirs = yield* fs.readDirectory(outDir); + expect(subdirs.length).toBe(1); + const dateDir = subdirs[0]!; + const csvFiles = yield* fs.readDirectory(path.join(outDir, dateDir)); + expect(csvFiles.length).toBeGreaterThan(0); + expect(csvFiles.every((f) => f.endsWith(".csv"))).toBe(true); + }), + signal, + ), + ); - testBehaviour("exits non-zero on connection refused", async ({ run }) => { - const result = await run([ - "inspect", - "report", - "--remote=postgresql://postgres:postgres@localhost:1/postgres", - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).not.toBe(""); - }); + testBehaviour("exits non-zero on connection refused", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "inspect", + "report", + "--remote=postgresql://postgres:postgres@localhost:1/postgres", + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).not.toBe(""); + }), + signal, + ), + ); }); diff --git a/apps/cli-e2e/src/tests/link-project.ts b/apps/cli-e2e/src/tests/link-project.ts index 513658c3c7..c405530943 100644 --- a/apps/cli-e2e/src/tests/link-project.ts +++ b/apps/cli-e2e/src/tests/link-project.ts @@ -1,13 +1,14 @@ -import { mkdirSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { Effect, FileSystem, Path } from "effect"; /** Writes `project-ref` and a stub `pooler-url` so `--remote` skips the live Postgres probe. */ -export function linkProject(dir: string, ref: string): void { - const tempDir = join(dir, "supabase", ".temp"); - mkdirSync(tempDir, { recursive: true }); - writeFileSync(join(tempDir, "project-ref"), ref); - writeFileSync( - join(tempDir, "pooler-url"), +export const linkProject = Effect.fnUntraced(function* (dir: string, ref: string) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const tempDir = path.join(dir, "supabase", ".temp"); + yield* fs.makeDirectory(tempDir, { recursive: true }); + yield* fs.writeFileString(path.join(tempDir, "project-ref"), ref); + yield* fs.writeFileString( + path.join(tempDir, "pooler-url"), `postgresql://postgres.${ref}:[YOUR-PASSWORD]@aws-0-eu-central-1.pooler.supabase.com:6543/postgres`, ); -} +}); diff --git a/apps/cli-e2e/src/tests/migrations.e2e.test.ts b/apps/cli-e2e/src/tests/migrations.e2e.test.ts index 3180c38ae3..a18e0bb383 100644 --- a/apps/cli-e2e/src/tests/migrations.e2e.test.ts +++ b/apps/cli-e2e/src/tests/migrations.e2e.test.ts @@ -1,91 +1,155 @@ -import { mkdirSync, readdirSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { Effect, FileSystem, Path } from "effect"; import { describe, expect } from "vitest"; -import { testBehaviour } from "./test-context.ts"; +import { runE2e, testBehaviour } from "./test-context.ts"; const MIGRATION_NAME = "my_change"; describe("migrations", () => { describe("migration:new", () => { - testBehaviour("creates timestamped sql file", async ({ run, workspace }) => { - const result = await run(["migration", "new", MIGRATION_NAME]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Created new migration at"); - const files = readdirSync(join(workspace.path, "supabase", "migrations")); - expect(files.some((f) => f.endsWith(`_${MIGRATION_NAME}.sql`))).toBe(true); - }); + testBehaviour("creates timestamped sql file", ({ run, workspace, signal }) => + runE2e( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const result = yield* run(["migration", "new", MIGRATION_NAME]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Created new migration at"); + const files = yield* fs.readDirectory( + path.join(workspace.path, "supabase", "migrations"), + ); + expect(files.some((f) => f.endsWith(`_${MIGRATION_NAME}.sql`))).toBe(true); + }), + signal, + ), + ); - testBehaviour("exits non-zero without name argument", async ({ run }) => { - const result = await run(["migration", "new"]); - expect(result.exitCode).not.toBe(0); - // A missing positional argument's usage block prints to stderr only. - expect(result.stdout).toBe(""); - expect(result.stderr).toContain("migration name"); - }); + testBehaviour("exits non-zero without name argument", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["migration", "new"]); + expect(result.exitCode).not.toBe(0); + // A missing positional argument's usage block prints to stderr only. + expect(result.stdout).toBe(""); + expect(result.stderr).toContain("migration name"); + }), + signal, + ), + ); }); describe("migration:list", () => { - testBehaviour("exits non-zero on connection refused", async ({ run }) => { - const result = await run(["migration", "list"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("failed to connect"); - }); + testBehaviour("exits non-zero on connection refused", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["migration", "list"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("failed to connect"); + }), + signal, + ), + ); }); describe("migration:up", () => { - testBehaviour("exits non-zero on connection refused", async ({ run }) => { - const result = await run(["migration", "up"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("failed to connect"); - }); + testBehaviour("exits non-zero on connection refused", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["migration", "up"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("failed to connect"); + }), + signal, + ), + ); }); describe("migration:down", () => { - testBehaviour("exits non-zero on connection refused", async ({ run }) => { - const result = await run(["migration", "down"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("failed to connect"); - }); + testBehaviour("exits non-zero on connection refused", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["migration", "down"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("failed to connect"); + }), + signal, + ), + ); - testBehaviour("exits non-zero on connection refused with --last 2", async ({ run }) => { - const result = await run(["migration", "down", "--last", "2"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("failed to connect"); - }); + testBehaviour("exits non-zero on connection refused with --last 2", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["migration", "down", "--last", "2"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("failed to connect"); + }), + signal, + ), + ); }); describe("migration:repair", () => { - testBehaviour("exits non-zero when --status flag is missing", async ({ run }) => { - const result = await run(["migration", "repair", "20230101000000"]); - expect(result.exitCode).not.toBe(0); - // A missing required flag's error prints once, without a duplicate - // usage dump, and spells the flag name without its `--` prefix. - expect(result.stderr).toContain('"status" not set'); - }); + testBehaviour("exits non-zero when --status flag is missing", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["migration", "repair", "20230101000000"]); + expect(result.exitCode).not.toBe(0); + // A missing required flag's error prints once, without a duplicate + // usage dump, and spells the flag name without its `--` prefix. + expect(result.stderr).toContain('"status" not set'); + }), + signal, + ), + ); - testBehaviour("exits non-zero on connection refused", async ({ run, workspace }) => { - const migrationsDir = join(workspace.path, "supabase", "migrations"); - mkdirSync(migrationsDir, { recursive: true }); - writeFileSync(join(migrationsDir, "20230101000000_x.sql"), "select 1;\n"); - const result = await run(["migration", "repair", "--status", "applied", "20230101000000"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("failed to connect"); - }); + testBehaviour("exits non-zero on connection refused", ({ run, workspace, signal }) => + runE2e( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const migrationsDir = path.join(workspace.path, "supabase", "migrations"); + yield* fs.makeDirectory(migrationsDir, { recursive: true }); + yield* fs.writeFileString( + path.join(migrationsDir, "20230101000000_x.sql"), + "select 1;\n", + ); + const result = yield* run([ + "migration", + "repair", + "--status", + "applied", + "20230101000000", + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("failed to connect"); + }), + signal, + ), + ); }); describe("migration:squash", () => { - testBehaviour("exits non-zero on connection refused", async ({ run }) => { - const result = await run(["migration", "squash"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).not.toBe(""); - }); + testBehaviour("exits non-zero on connection refused", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["migration", "squash"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).not.toBe(""); + }), + signal, + ), + ); }); describe("migration:fetch", () => { - testBehaviour("exits non-zero on connection refused", async ({ run }) => { - const result = await run(["migration", "fetch"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("failed to connect"); - }); + testBehaviour("exits non-zero on connection refused", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["migration", "fetch"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("failed to connect"); + }), + signal, + ), + ); }); }); diff --git a/apps/cli-e2e/src/tests/network-security.e2e.test.ts b/apps/cli-e2e/src/tests/network-security.e2e.test.ts index 5e19ae3803..8e80199da4 100644 --- a/apps/cli-e2e/src/tests/network-security.e2e.test.ts +++ b/apps/cli-e2e/src/tests/network-security.e2e.test.ts @@ -1,683 +1,883 @@ +import { Effect } from "effect"; import { describe, expect } from "vitest"; import { PROJECT_REF } from "./env.ts"; -import { testBehaviour } from "./test-context.ts"; +import { postControl, runE2e, testBehaviour } from "./test-context.ts"; describe("network-bans", () => { describe("network-bans:get", () => { - testBehaviour("renders fixture data in output", async ({ run, projectRef }) => { - const result = await run(["network-bans", "get", "--experimental", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - }); - - testBehaviour("returns json output with --output json", async ({ run, projectRef }) => { - const result = await run([ - "network-bans", - "get", - "--experimental", - "--output", - "json", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - }); - - testBehaviour("includes debug output with --debug", async ({ run, projectRef }) => { - const result = await run([ - "network-bans", - "get", - "--experimental", - "--debug", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stderr).toMatch(/HTTP.*GET:/); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "network-bans", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run([ - "network-bans", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); - - testBehaviour("exits non-zero on 404 project not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), - }); - const result = await run([ - "network-bans", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Project not found"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run([ - "network-bans", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run([ - "network-bans", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + testBehaviour("renders fixture data in output", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "network-bans", + "get", + "--experimental", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); + + testBehaviour("returns json output with --output json", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "network-bans", + "get", + "--experimental", + "--output", + "json", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); + + testBehaviour("includes debug output with --debug", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "network-bans", + "get", + "--experimental", + "--debug", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toMatch(/HTTP.*GET:/); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "network-bans", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run([ + "network-bans", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 project not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Project not found" }, + }); + const result = yield* run([ + "network-bans", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Project not found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run([ + "network-bans", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run([ + "network-bans", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); describe("network-bans:remove", () => { - testBehaviour("removes IP from ban list", async ({ run, projectRef }) => { - const result = await run([ - "network-bans", - "remove", - "--experimental", - "--db-unban-ip", - "1.2.3.4", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - }); - - testBehaviour("removes multiple IPs from ban list", async ({ run, projectRef }) => { - const result = await run([ - "network-bans", - "remove", - "--experimental", - "--db-unban-ip", - "1.2.3.4", - "--db-unban-ip", - "5.6.7.8", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - }); - - testBehaviour("exits non-zero with invalid IP address", async ({ run }) => { - const result = await run([ - "network-bans", - "remove", - "--experimental", - "--db-unban-ip", - "invalid-ip", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("invalid IP address"); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "network-bans", - "remove", - "--experimental", - "--db-unban-ip", - "1.2.3.4", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 404 project not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), - }); - const result = await run([ - "network-bans", - "remove", - "--experimental", - "--db-unban-ip", - "1.2.3.4", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Project not found"); - }); + testBehaviour("removes IP from ban list", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "network-bans", + "remove", + "--experimental", + "--db-unban-ip", + "1.2.3.4", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); + + testBehaviour("removes multiple IPs from ban list", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "network-bans", + "remove", + "--experimental", + "--db-unban-ip", + "1.2.3.4", + "--db-unban-ip", + "5.6.7.8", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); + + testBehaviour("exits non-zero with invalid IP address", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "network-bans", + "remove", + "--experimental", + "--db-unban-ip", + "invalid-ip", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("invalid IP address"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "network-bans", + "remove", + "--experimental", + "--db-unban-ip", + "1.2.3.4", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 project not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Project not found" }, + }); + const result = yield* run([ + "network-bans", + "remove", + "--experimental", + "--db-unban-ip", + "1.2.3.4", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Project not found"); + }), + signal, + ), + ); }); }); describe("network-restrictions", () => { describe("network-restrictions:get", () => { - testBehaviour("renders fixture data in output", async ({ run, projectRef }) => { - const result = await run([ - "network-restrictions", - "get", - "--experimental", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - }); - - testBehaviour("returns json output with --output json", async ({ run, projectRef }) => { - const result = await run([ - "network-restrictions", - "get", - "--experimental", - "--output", - "json", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "network-restrictions", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run([ - "network-restrictions", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); - - testBehaviour("exits non-zero on 404 project not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), - }); - const result = await run([ - "network-restrictions", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Project not found"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run([ - "network-restrictions", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run([ - "network-restrictions", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + testBehaviour("renders fixture data in output", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "network-restrictions", + "get", + "--experimental", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); + + testBehaviour("returns json output with --output json", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "network-restrictions", + "get", + "--experimental", + "--output", + "json", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "network-restrictions", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run([ + "network-restrictions", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 project not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Project not found" }, + }); + const result = yield* run([ + "network-restrictions", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Project not found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run([ + "network-restrictions", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run([ + "network-restrictions", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); describe("network-restrictions:update", () => { - testBehaviour("sets CIDR allowlist", async ({ run, projectRef }) => { - const result = await run([ - "network-restrictions", - "update", - "--experimental", - "--db-allow-cidr", - "0.0.0.0/0", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - }); - - testBehaviour("appends to existing restrictions", async ({ run, projectRef }) => { - const result = await run([ - "network-restrictions", - "update", - "--experimental", - "--append", - "--db-allow-cidr", - "8.8.8.0/24", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - }); - - testBehaviour("bypasses CIDR validation checks", async ({ run, projectRef }) => { - const result = await run([ - "network-restrictions", - "update", - "--experimental", - "--bypass-cidr-checks", - "--db-allow-cidr", - "0.0.0.0/0", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - }); - - testBehaviour("exits non-zero with invalid CIDR format", async ({ run }) => { - const result = await run([ - "network-restrictions", - "update", - "--experimental", - "--db-allow-cidr", - "not-a-cidr", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("failed to parse IP"); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "network-restrictions", - "update", - "--experimental", - "--db-allow-cidr", - "0.0.0.0/0", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 404 project not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), - }); - const result = await run([ - "network-restrictions", - "update", - "--experimental", - "--db-allow-cidr", - "0.0.0.0/0", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Project not found"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run([ - "network-restrictions", - "update", - "--experimental", - "--db-allow-cidr", - "0.0.0.0/0", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run([ - "network-restrictions", - "update", - "--experimental", - "--db-allow-cidr", - "0.0.0.0/0", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + testBehaviour("sets CIDR allowlist", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "network-restrictions", + "update", + "--experimental", + "--db-allow-cidr", + "0.0.0.0/0", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); + + testBehaviour("appends to existing restrictions", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "network-restrictions", + "update", + "--experimental", + "--append", + "--db-allow-cidr", + "8.8.8.0/24", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); + + testBehaviour("bypasses CIDR validation checks", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "network-restrictions", + "update", + "--experimental", + "--bypass-cidr-checks", + "--db-allow-cidr", + "0.0.0.0/0", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); + + testBehaviour("exits non-zero with invalid CIDR format", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "network-restrictions", + "update", + "--experimental", + "--db-allow-cidr", + "not-a-cidr", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("failed to parse IP"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "network-restrictions", + "update", + "--experimental", + "--db-allow-cidr", + "0.0.0.0/0", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 project not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Project not found" }, + }); + const result = yield* run([ + "network-restrictions", + "update", + "--experimental", + "--db-allow-cidr", + "0.0.0.0/0", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Project not found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run([ + "network-restrictions", + "update", + "--experimental", + "--db-allow-cidr", + "0.0.0.0/0", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run([ + "network-restrictions", + "update", + "--experimental", + "--db-allow-cidr", + "0.0.0.0/0", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); }); describe("ssl-enforcement", () => { describe("ssl-enforcement:get", () => { - testBehaviour("renders fixture data in output", async ({ run, projectRef }) => { - const result = await run([ - "ssl-enforcement", - "get", - "--experimental", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - }); - - testBehaviour("returns json output with --output json", async ({ run, projectRef }) => { - const result = await run([ - "ssl-enforcement", - "get", - "--experimental", - "--output", - "json", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "ssl-enforcement", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run([ - "ssl-enforcement", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); - - testBehaviour("exits non-zero on 404 project not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), - }); - const result = await run([ - "ssl-enforcement", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Project not found"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run([ - "ssl-enforcement", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run([ - "ssl-enforcement", - "get", - "--experimental", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + testBehaviour("renders fixture data in output", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "ssl-enforcement", + "get", + "--experimental", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); + + testBehaviour("returns json output with --output json", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "ssl-enforcement", + "get", + "--experimental", + "--output", + "json", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "ssl-enforcement", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run([ + "ssl-enforcement", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 project not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Project not found" }, + }); + const result = yield* run([ + "ssl-enforcement", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Project not found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run([ + "ssl-enforcement", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run([ + "ssl-enforcement", + "get", + "--experimental", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); describe("ssl-enforcement:update", () => { - testBehaviour("enables SSL enforcement", async ({ run, projectRef }) => { - const result = await run([ - "ssl-enforcement", - "update", - "--experimental", - "--enable-db-ssl-enforcement", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - }); - - testBehaviour("disables SSL enforcement", async ({ run, projectRef }) => { - const result = await run([ - "ssl-enforcement", - "update", - "--experimental", - "--disable-db-ssl-enforcement", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - }); - - testBehaviour("exits non-zero with mutually exclusive flags", async ({ run }) => { - const result = await run([ - "ssl-enforcement", - "update", - "--experimental", - "--enable-db-ssl-enforcement", - "--disable-db-ssl-enforcement", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - }); - - testBehaviour("exits non-zero with no flags provided", async ({ run }) => { - const result = await run([ - "ssl-enforcement", - "update", - "--experimental", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "ssl-enforcement", - "update", - "--experimental", - "--enable-db-ssl-enforcement", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run([ - "ssl-enforcement", - "update", - "--experimental", - "--enable-db-ssl-enforcement", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); - - testBehaviour("exits non-zero on 404 project not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), - }); - const result = await run([ - "ssl-enforcement", - "update", - "--experimental", - "--enable-db-ssl-enforcement", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Project not found"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run([ - "ssl-enforcement", - "update", - "--experimental", - "--enable-db-ssl-enforcement", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run([ - "ssl-enforcement", - "update", - "--experimental", - "--enable-db-ssl-enforcement", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + testBehaviour("enables SSL enforcement", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "ssl-enforcement", + "update", + "--experimental", + "--enable-db-ssl-enforcement", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); + + testBehaviour("disables SSL enforcement", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "ssl-enforcement", + "update", + "--experimental", + "--disable-db-ssl-enforcement", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); + + testBehaviour("exits non-zero with mutually exclusive flags", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "ssl-enforcement", + "update", + "--experimental", + "--enable-db-ssl-enforcement", + "--disable-db-ssl-enforcement", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); + + testBehaviour("exits non-zero with no flags provided", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "ssl-enforcement", + "update", + "--experimental", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "ssl-enforcement", + "update", + "--experimental", + "--enable-db-ssl-enforcement", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run([ + "ssl-enforcement", + "update", + "--experimental", + "--enable-db-ssl-enforcement", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 project not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Project not found" }, + }); + const result = yield* run([ + "ssl-enforcement", + "update", + "--experimental", + "--enable-db-ssl-enforcement", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Project not found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run([ + "ssl-enforcement", + "update", + "--experimental", + "--enable-db-ssl-enforcement", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run([ + "ssl-enforcement", + "update", + "--experimental", + "--enable-db-ssl-enforcement", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); }); diff --git a/apps/cli-e2e/src/tests/orgs.e2e.test.ts b/apps/cli-e2e/src/tests/orgs.e2e.test.ts index 74964819b3..4f362f6507 100644 --- a/apps/cli-e2e/src/tests/orgs.e2e.test.ts +++ b/apps/cli-e2e/src/tests/orgs.e2e.test.ts @@ -1,157 +1,222 @@ +import { Effect, Schema } from "effect"; import { describe, expect } from "vitest"; import { isRecording } from "./env.ts"; -import { testBehaviour } from "./test-context.ts"; +import { decodeJsonOutput, postControl, runE2e, testBehaviour } from "./test-context.ts"; const ORG_NAME = "My Test Org"; describe("orgs", () => { describe("orgs:list", () => { - testBehaviour("renders org data", async ({ run }) => { - const result = await run(["orgs", "list"]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("ID"); - expect(result.stdout).toMatch(/[a-z]{20}/); - }); - - testBehaviour("returns json output with --output json", async ({ run }) => { - const result = await run(["orgs", "list", "--output", "json"]); - expect(result.exitCode).toBe(0); - const parsed = JSON.parse(result.stdout) as unknown[]; - expect(Array.isArray(parsed)).toBe(true); - expect(parsed.length).toBeGreaterThan(0); - expect(parsed[0]).toMatchObject({ id: expect.any(String), name: expect.any(String) }); - }); - - testBehaviour("includes debug output with --debug", async ({ run }) => { - const result = await run(["orgs", "list", "--debug"]); - expect(result.exitCode).toBe(0); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run(["orgs", "list"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run(["orgs", "list"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run(["orgs", "list"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run(["orgs", "list"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + testBehaviour("renders org data", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["orgs", "list"]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("ID"); + expect(result.stdout).toMatch(/[a-z]{20}/); + }), + signal, + ), + ); + + testBehaviour("returns json output with --output json", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["orgs", "list", "--output", "json"]); + expect(result.exitCode).toBe(0); + const parsed = yield* decodeJsonOutput(Schema.Unknown)(result.stdout); + expect(Array.isArray(parsed)).toBe(true); + /* Narrows the type only: the assertion above already fails a non-array. */ + if (!Array.isArray(parsed)) return; + expect(parsed.length).toBeGreaterThan(0); + expect(parsed[0]).toMatchObject({ id: expect.any(String), name: expect.any(String) }); + }), + signal, + ), + ); + + testBehaviour("includes debug output with --debug", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["orgs", "list", "--debug"]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run(["orgs", "list"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run(["orgs", "list"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run(["orgs", "list"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run(["orgs", "list"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); describe("orgs:create", () => { - testBehaviour.skipIf(isRecording)("creates organization with name", async ({ run }) => { - const result = await run(["orgs", "create", ORG_NAME, "--yes"]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Created organization:"); - expect(result.stdout).toMatch(/[a-z]{20}/); - }); - - testBehaviour("exits non-zero without name in non-TTY", async ({ run }) => { - const result = await run(["orgs", "create", "--yes"]); - expect(result.exitCode).not.toBe(0); - }); - - testBehaviour("exits non-zero on 409 name conflict", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - status: 409, - body: { message: "Organization name already in use" }, + testBehaviour.skipIf(isRecording)("creates organization with name", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["orgs", "create", ORG_NAME, "--yes"]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Created organization:"); + expect(result.stdout).toMatch(/[a-z]{20}/); + }), + signal, + ), + ); + + testBehaviour("exits non-zero without name in non-TTY", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["orgs", "create", "--yes"]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 409 name conflict", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 409, + body: { message: "Organization name already in use" }, + }); + const result = yield* run(["orgs", "create", ORG_NAME, "--yes"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Organization name already in use"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 403 plan limit reached", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Organization limit reached" }, + }); + const result = yield* run(["orgs", "create", ORG_NAME, "--yes"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Organization limit reached"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 422 validation error", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 422, + body: { message: "Invalid organization name" }, + }); + const result = yield* run(["orgs", "create", ORG_NAME, "--yes"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid organization name"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run(["orgs", "create", ORG_NAME, "--yes"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run(["orgs", "create", ORG_NAME, "--yes"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run(["orgs", "create", ORG_NAME, "--yes"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); }), - }); - const result = await run(["orgs", "create", ORG_NAME, "--yes"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Organization name already in use"); - }); - - testBehaviour("exits non-zero on 403 plan limit reached", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Organization limit reached" } }), - }); - const result = await run(["orgs", "create", ORG_NAME, "--yes"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Organization limit reached"); - }); - - testBehaviour("exits non-zero on 422 validation error", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 422, body: { message: "Invalid organization name" } }), - }); - const result = await run(["orgs", "create", ORG_NAME, "--yes"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid organization name"); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run(["orgs", "create", ORG_NAME, "--yes"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run(["orgs", "create", ORG_NAME, "--yes"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run(["orgs", "create", ORG_NAME, "--yes"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + signal, + ), + ); }); }); diff --git a/apps/cli-e2e/src/tests/project-lifecycle.e2e.test.ts b/apps/cli-e2e/src/tests/project-lifecycle.e2e.test.ts index 9c68da3322..e3e3bf27df 100644 --- a/apps/cli-e2e/src/tests/project-lifecycle.e2e.test.ts +++ b/apps/cli-e2e/src/tests/project-lifecycle.e2e.test.ts @@ -1,63 +1,107 @@ -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { Effect, FileSystem, Path } from "effect"; import { describe, expect, inject, test } from "vitest"; -import { createHarness, exec, makeTempDir } from "@supabase/cli-test-helpers"; +import { createHarness, makeTempDir } from "@supabase/cli-test-helpers"; import { ACCESS_TOKEN, isRecording, PROJECT_REF } from "./env.ts"; -import { testBehaviour } from "./test-context.ts"; +import { execCli, postControl, runE2e, testBehaviour } from "./test-context.ts"; describe("init", () => { - testBehaviour("creates supabase/config.toml and exits zero", async ({ run, workspace }) => { - const result = await run(["init"]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Finished supabase init"); - expect(existsSync(join(workspace.path, "supabase", "config.toml"))).toBe(true); - }); + testBehaviour("creates supabase/config.toml and exits zero", ({ run, workspace, signal }) => + runE2e( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const result = yield* run(["init"]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Finished supabase init"); + expect(yield* fs.exists(path.join(workspace.path, "supabase", "config.toml"))).toBe(true); + }), + signal, + ), + ); testBehaviour( "exits non-zero if config.toml already exists without --force", - async ({ run, workspace }) => { - mkdirSync(join(workspace.path, "supabase"), { recursive: true }); - writeFileSync(join(workspace.path, "supabase", "config.toml"), "# existing config\n"); - const result = await run(["init"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("force"); - }, + ({ run, workspace, signal }) => + runE2e( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fs.makeDirectory(path.join(workspace.path, "supabase"), { recursive: true }); + yield* fs.writeFileString( + path.join(workspace.path, "supabase", "config.toml"), + "# existing config\n", + ); + const result = yield* run(["init"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("force"); + }), + signal, + ), ); - testBehaviour("exits zero with --force when config.toml exists", async ({ run, workspace }) => { - mkdirSync(join(workspace.path, "supabase"), { recursive: true }); - writeFileSync(join(workspace.path, "supabase", "config.toml"), "# existing config\n"); - const result = await run(["init", "--force"]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Finished supabase init"); - expect(existsSync(join(workspace.path, "supabase", "config.toml"))).toBe(true); - }); + testBehaviour("exits zero with --force when config.toml exists", ({ run, workspace, signal }) => + runE2e( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fs.makeDirectory(path.join(workspace.path, "supabase"), { recursive: true }); + yield* fs.writeFileString( + path.join(workspace.path, "supabase", "config.toml"), + "# existing config\n", + ); + const result = yield* run(["init", "--force"]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Finished supabase init"); + expect(yield* fs.exists(path.join(workspace.path, "supabase", "config.toml"))).toBe(true); + }), + signal, + ), + ); testBehaviour( "creates VS Code settings with --with-vscode-settings", - async ({ run, workspace }) => { - const result = await run(["init", "--with-vscode-settings"]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Generated VS Code settings"); - expect(existsSync(join(workspace.path, ".vscode", "settings.json"))).toBe(true); - }, + ({ run, workspace, signal }) => + runE2e( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const result = yield* run(["init", "--with-vscode-settings"]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Generated VS Code settings"); + expect(yield* fs.exists(path.join(workspace.path, ".vscode", "settings.json"))).toBe( + true, + ); + }), + signal, + ), ); testBehaviour( "creates IntelliJ settings with --with-intellij-settings", - async ({ run, workspace }) => { - const result = await run(["init", "--with-intellij-settings"]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Generated IntelliJ settings"); - expect(existsSync(join(workspace.path, ".idea", "deno.xml"))).toBe(true); - }, + ({ run, workspace, signal }) => + runE2e( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const result = yield* run(["init", "--with-intellij-settings"]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Generated IntelliJ settings"); + expect(yield* fs.exists(path.join(workspace.path, ".idea", "deno.xml"))).toBe(true); + }), + signal, + ), ); - testBehaviour("includes debug output with --debug", async ({ run }) => { - const result = await run(["init", "--debug"]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Finished supabase init"); - }); + testBehaviour("includes debug output with --debug", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["init", "--debug"]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Finished supabase init"); + }), + signal, + ), + ); }); describe("link", () => { @@ -65,92 +109,142 @@ describe("link", () => { // SUPABASE_PROJECT_ID, which the CLI accepts as a substitute for // --project-ref in non-TTY mode. A raw test omits it so the CLI correctly // requires the --project-ref flag. - test("exits non-zero without --project-ref in non-TTY", async () => { + test("exits non-zero without --project-ref in non-TTY", ({ signal }) => { const serverUrl = inject("replayServerUrl") as string; - const dir = await makeTempDir("cli-e2e-link-no-ref-"); - await using _ = dir; - const harness = createHarness({ - apiUrl: serverUrl, - accessToken: ACCESS_TOKEN, - cwd: dir.path, - }); - const result = await exec(harness, ["link"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("project-ref"); + return runE2e( + Effect.acquireUseRelease( + Effect.promise(() => makeTempDir("cli-e2e-link-no-ref-")), + (dir) => + Effect.gen(function* () { + const harness = createHarness({ + apiUrl: serverUrl, + accessToken: ACCESS_TOKEN, + cwd: dir.path, + }); + const result = yield* execCli(harness, ["link"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("project-ref"); + }), + (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()), + ), + signal, + ); }); // The testBehaviour run fixture always injects SUPABASE_PROJECT_ID, which the // CLI accepts in place of --project-ref, so link proceeds to the API and succeeds. - testBehaviour("links when only SUPABASE_PROJECT_ID is set in non-TTY", async ({ run }) => { - const result = await run(["link"]); - expect(result.exitCode).toBe(0); - }); + testBehaviour("links when only SUPABASE_PROJECT_ID is set in non-TTY", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["link"]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run(["link", "--project-ref", PROJECT_REF]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run(["link", "--project-ref", PROJECT_REF]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run(["link", "--project-ref", PROJECT_REF]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run(["link", "--project-ref", PROJECT_REF]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run(["link", "--project-ref", PROJECT_REF]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run(["link", "--project-ref", PROJECT_REF]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); // link's concurrent service calls (after project-status and api-keys) fail // silently, so only the first two sequential calls need fixture entries. testBehaviour.skipIf(isRecording)( "links project successfully", - async ({ run, projectRef, workspace }) => { - const result = await run(["link", "--project-ref", projectRef]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Finished supabase link"); - expect(existsSync(join(workspace.path, "supabase", ".temp", "project-ref"))).toBe(true); - expect( - readFileSync(join(workspace.path, "supabase", ".temp", "storage-migration"), "utf8"), - ).toBe("operation-ergonomics"); - }, + ({ run, projectRef, workspace, signal }) => + runE2e( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const result = yield* run(["link", "--project-ref", projectRef]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Finished supabase link"); + expect( + yield* fs.exists(path.join(workspace.path, "supabase", ".temp", "project-ref")), + ).toBe(true); + expect( + new TextDecoder("utf-8", { ignoreBOM: true }).decode( + yield* fs.readFile( + path.join(workspace.path, "supabase", ".temp", "storage-migration"), + ), + ), + ).toBe("operation-ergonomics"); + }), + signal, + ), ); testBehaviour.skipIf(isRecording)( "--skip-pooler uses direct connection", - async ({ run, projectRef, workspace }) => { - const result = await run(["link", "--project-ref", projectRef, "--skip-pooler"]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Finished supabase link"); - expect(existsSync(join(workspace.path, "supabase", ".temp", "project-ref"))).toBe(true); - }, + ({ run, projectRef, workspace, signal }) => + runE2e( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const result = yield* run(["link", "--project-ref", projectRef, "--skip-pooler"]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Finished supabase link"); + expect( + yield* fs.exists(path.join(workspace.path, "supabase", ".temp", "project-ref")), + ).toBe(true); + }), + signal, + ), ); }); describe("unlink", () => { - testBehaviour("exits non-zero when project not linked", async ({ run }) => { - const result = await run(["unlink"]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("supabase link"); - }); + testBehaviour("exits non-zero when project not linked", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["unlink"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("supabase link"); + }), + signal, + ), + ); // The success path is omitted: unlink deletes a keyring entry on success, and // Linux CI (no D-Bus session bus) makes that call exit 1. See diff --git a/apps/cli-e2e/src/tests/projects.e2e.test.ts b/apps/cli-e2e/src/tests/projects.e2e.test.ts index 3dd0f694ae..858a03ace2 100644 --- a/apps/cli-e2e/src/tests/projects.e2e.test.ts +++ b/apps/cli-e2e/src/tests/projects.e2e.test.ts @@ -1,317 +1,418 @@ +import { Effect, Schema } from "effect"; import { describe, expect } from "vitest"; import { isRecording, PROJECT_REF } from "./env.ts"; -import { testBehaviour } from "./test-context.ts"; +import { decodeJsonOutput, postControl, runE2e, testBehaviour } from "./test-context.ts"; describe("projects", () => { describe("projects:list", () => { - testBehaviour("renders project list", async ({ run }) => { - const result = await run(["projects", "list"]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toMatch(/[a-z]{20}/); - expect(result.stdout).toContain("REFERENCE ID"); - }); + testBehaviour("renders project list", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["projects", "list"]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/[a-z]{20}/); + expect(result.stdout).toContain("REFERENCE ID"); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run(["projects", "list"]); - expect(result.exitCode).not.toBe(0); - }); + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run(["projects", "list"]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); - testBehaviour("returns json output with --output json", async ({ run }) => { - const result = await run(["projects", "list", "--output", "json"]); - expect(result.exitCode).toBe(0); - const parsed = JSON.parse(result.stdout) as unknown[]; - expect(Array.isArray(parsed)).toBe(true); - expect(parsed.length).toBeGreaterThan(0); - expect(parsed[0]).toMatchObject({ name: expect.any(String), ref: expect.any(String) }); - }); + testBehaviour("returns json output with --output json", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["projects", "list", "--output", "json"]); + expect(result.exitCode).toBe(0); + const parsed = yield* decodeJsonOutput(Schema.Unknown)(result.stdout); + expect(Array.isArray(parsed)).toBe(true); + /* Narrows the type only: the assertion above already fails a non-array. */ + if (!Array.isArray(parsed)) return; + expect(parsed.length).toBeGreaterThan(0); + expect(parsed[0]).toMatchObject({ name: expect.any(String), ref: expect.any(String) }); + }), + signal, + ), + ); - testBehaviour("includes debug output with --debug", async ({ run }) => { - const result = await run(["projects", "list", "--debug"]); - expect(result.exitCode).toBe(0); - }); + testBehaviour("includes debug output with --debug", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["projects", "list", "--debug"]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run(["projects", "list"]); - expect(result.exitCode).not.toBe(0); - }); + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run(["projects", "list"]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run(["projects", "list"]); - expect(result.exitCode).not.toBe(0); - }); + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run(["projects", "list"]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run(["projects", "list"]); - expect(result.exitCode).not.toBe(0); - }); + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run(["projects", "list"]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); }); describe("projects:api-keys", () => { - testBehaviour("shows default and anon keys", async ({ run, projectRef }) => { - const result = await run(["projects", "api-keys", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("NAME"); - expect(result.stdout).toContain("KEY VALUE"); - expect(result.stdout).toContain("anon"); - expect(result.stdout).toContain("default"); - }); + testBehaviour("shows default and anon keys", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["projects", "api-keys", `--remote=${projectRef}`]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("NAME"); + expect(result.stdout).toContain("KEY VALUE"); + expect(result.stdout).toContain("anon"); + expect(result.stdout).toContain("default"); + }), + signal, + ), + ); - testBehaviour("returns json output with --output json", async ({ run, projectRef }) => { - const result = await run([ - "projects", - "api-keys", - "--output", - "json", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - const parsed = JSON.parse(result.stdout) as unknown[]; - expect(Array.isArray(parsed)).toBe(true); - expect(parsed).toEqual(expect.arrayContaining([expect.objectContaining({ name: "anon" })])); - }); + testBehaviour("returns json output with --output json", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "projects", + "api-keys", + "--output", + "json", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + const parsed = yield* decodeJsonOutput(Schema.Unknown)(result.stdout); + expect(Array.isArray(parsed)).toBe(true); + expect(parsed).toEqual( + expect.arrayContaining([expect.objectContaining({ name: "anon" })]), + ); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run(["projects", "api-keys", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - }); + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run(["projects", "api-keys", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run(["projects", "api-keys", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - }); + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run(["projects", "api-keys", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 404 project not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), - }); - const result = await run(["projects", "api-keys", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - }); + testBehaviour("exits non-zero on 404 project not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Project not found" }, + }); + const result = yield* run(["projects", "api-keys", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); }); describe("projects:create", () => { - testBehaviour("creates project with required flags", async ({ run, orgId }) => { - const result = await run([ - "projects", - "create", - "my-project", - "--org-id", - orgId, - "--db-password", - "password123", - "--region", - "us-east-1", - "--size", - "micro", - "--yes", - ]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("REFERENCE ID"); - }); + testBehaviour("creates project with required flags", ({ run, orgId, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "projects", + "create", + "my-project", + "--org-id", + orgId, + "--db-password", + "password123", + "--region", + "us-east-1", + "--size", + "micro", + "--yes", + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("REFERENCE ID"); + }), + signal, + ), + ); - testBehaviour("exits non-zero without required flags in non-TTY", async ({ run }) => { - const result = await run(["projects", "create", "--org-id", "test-org-id", "--yes"]); - expect(result.exitCode).not.toBe(0); - }); + testBehaviour("exits non-zero without required flags in non-TTY", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["projects", "create", "--org-id", "test-org-id", "--yes"]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 409 name conflict", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 409, body: { message: "Project name already in use" } }), - }); - const result = await run([ - "projects", - "create", - "my-project", - "--org-id", - "test-org-id", - "--db-password", - "password123", - "--region", - "us-east-1", - "--yes", - ]); - expect(result.exitCode).not.toBe(0); - }); + testBehaviour("exits non-zero on 409 name conflict", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 409, + body: { message: "Project name already in use" }, + }); + const result = yield* run([ + "projects", + "create", + "my-project", + "--org-id", + "test-org-id", + "--db-password", + "password123", + "--region", + "us-east-1", + "--yes", + ]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 422 validation error", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - status: 422, - body: { - message: "Validation failed", - errors: [{ field: "region", message: "Invalid region" }], - }, + testBehaviour("exits non-zero on 422 validation error", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 422, + body: { + message: "Validation failed", + errors: [{ field: "region", message: "Invalid region" }], + }, + }); + const result = yield* run([ + "projects", + "create", + "my-project", + "--org-id", + "test-org-id", + "--db-password", + "password123", + "--region", + "us-east-1", + "--yes", + ]); + expect(result.exitCode).not.toBe(0); }), - }); - const result = await run([ - "projects", - "create", - "my-project", - "--org-id", - "test-org-id", - "--db-password", - "password123", - "--region", - "us-east-1", - "--yes", - ]); - expect(result.exitCode).not.toBe(0); - }); + signal, + ), + ); - testBehaviour("exits non-zero on 403 no org access", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run([ - "projects", - "create", - "my-project", - "--org-id", - "test-org-id", - "--db-password", - "password123", - "--region", - "us-east-1", - "--yes", - ]); - expect(result.exitCode).not.toBe(0); - }); + testBehaviour("exits non-zero on 403 no org access", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run([ + "projects", + "create", + "my-project", + "--org-id", + "test-org-id", + "--db-password", + "password123", + "--region", + "us-east-1", + "--yes", + ]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "projects", - "create", - "my-project", - "--org-id", - "test-org-id", - "--db-password", - "password123", - "--region", - "us-east-1", - "--yes", - ]); - expect(result.exitCode).not.toBe(0); - }); + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "projects", + "create", + "my-project", + "--org-id", + "test-org-id", + "--db-password", + "password123", + "--region", + "us-east-1", + "--yes", + ]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run([ - "projects", - "create", - "my-project", - "--org-id", - "test-org-id", - "--db-password", - "password123", - "--region", - "us-east-1", - "--yes", - ]); - expect(result.exitCode).not.toBe(0); - }); + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run([ + "projects", + "create", + "my-project", + "--org-id", + "test-org-id", + "--db-password", + "password123", + "--region", + "us-east-1", + "--yes", + ]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); }); describe("projects:delete", () => { testBehaviour.skipIf(isRecording)( "returns 400 when project not ready for deletion", - async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - status: 400, - body: { message: "Project not ready for deletion." }, + ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 400, + body: { message: "Project not ready for deletion." }, + }); + const result = yield* run(["projects", "delete", PROJECT_REF, "--yes"]); + expect(result.exitCode).not.toBe(0); }), - }); - const result = await run(["projects", "delete", PROJECT_REF, "--yes"]); - expect(result.exitCode).not.toBe(0); - }, + signal, + ), ); testBehaviour.skipIf(isRecording)( "deletes project with --yes flag", - async ({ run, projectRef }) => { - const result = await run(["projects", "delete", projectRef, "--yes"]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Deleted project"); - }, + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["projects", "delete", projectRef, "--yes"]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Deleted project"); + }), + signal, + ), ); - testBehaviour("exits non-zero on 404 project not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), - }); - const result = await run(["projects", "delete", PROJECT_REF, "--yes"]); - expect(result.exitCode).not.toBe(0); - }); + testBehaviour("exits non-zero on 404 project not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Project not found" }, + }); + const result = yield* run(["projects", "delete", PROJECT_REF, "--yes"]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run(["projects", "delete", PROJECT_REF, "--yes"]); - expect(result.exitCode).not.toBe(0); - }); + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run(["projects", "delete", PROJECT_REF, "--yes"]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run(["projects", "delete", PROJECT_REF, "--yes"]); - expect(result.exitCode).not.toBe(0); - }); + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run(["projects", "delete", PROJECT_REF, "--yes"]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); }); }); diff --git a/apps/cli-e2e/src/tests/request-log.ts b/apps/cli-e2e/src/tests/request-log.ts index 4b62c26abb..dc172c1bff 100644 --- a/apps/cli-e2e/src/tests/request-log.ts +++ b/apps/cli-e2e/src/tests/request-log.ts @@ -1,12 +1,15 @@ -export interface RequestEntry { - method: string; - pathname: string; - headers: Record; - body: unknown; -} +import { Effect, Schema } from "effect"; +import { HttpClient, HttpClientResponse } from "effect/http"; + +const RequestEntry = Schema.Struct({ + method: Schema.String, + pathname: Schema.String, + headers: Schema.Record(Schema.String, Schema.String), + body: Schema.Unknown, +}); /** The requests the replay server received since the last test cleared its log. */ -export async function getRequestLog(apiUrl: string): Promise { - const res = await fetch(`${apiUrl}/_ctrl/requests`); - return res.json() as Promise; -} +export const getRequestLog = Effect.fnUntraced(function* (apiUrl: string) { + const res = yield* HttpClient.get(`${apiUrl}/_ctrl/requests`); + return yield* HttpClientResponse.schemaBodyJson(Schema.Array(RequestEntry))(res); +}); diff --git a/apps/cli-e2e/src/tests/secrets.e2e.test.ts b/apps/cli-e2e/src/tests/secrets.e2e.test.ts index a3d2924351..28e8b5a61d 100644 --- a/apps/cli-e2e/src/tests/secrets.e2e.test.ts +++ b/apps/cli-e2e/src/tests/secrets.e2e.test.ts @@ -1,206 +1,325 @@ -import { writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { Effect, FileSystem, Path, Schema } from "effect"; import { describe, expect } from "vitest"; import { PROJECT_REF } from "./env.ts"; -import { testBehaviour } from "./test-context.ts"; +import { decodeJsonOutput, postControl, runE2e, testBehaviour } from "./test-context.ts"; describe("secrets", () => { describe("secrets:list", () => { - testBehaviour("renders fixture data in output", async ({ run, projectRef }) => { - const result = await run(["secrets", "list", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("NAME"); - expect(result.stdout).toContain("DIGEST"); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run(["secrets", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - }); - - testBehaviour("returns json output with --output json", async ({ run, projectRef }) => { - const result = await run(["secrets", "list", "--output", "json", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - const parsed = JSON.parse(result.stdout) as unknown[]; - expect(Array.isArray(parsed)).toBe(true); - }); - - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run(["secrets", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - }); - - testBehaviour("exits non-zero on 404 project not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), - }); - const result = await run(["secrets", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run(["secrets", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run(["secrets", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - }); - - testBehaviour("includes debug output with --debug", async ({ run, projectRef }) => { - const result = await run(["secrets", "list", "--debug", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - }); + testBehaviour("renders fixture data in output", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["secrets", "list", `--remote=${projectRef}`]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("NAME"); + expect(result.stdout).toContain("DIGEST"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run(["secrets", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); + + testBehaviour("returns json output with --output json", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "secrets", + "list", + "--output", + "json", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + const parsed = yield* decodeJsonOutput(Schema.Unknown)(result.stdout); + expect(Array.isArray(parsed)).toBe(true); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run(["secrets", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 project not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Project not found" }, + }); + const result = yield* run(["secrets", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run(["secrets", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run(["secrets", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); + + testBehaviour("includes debug output with --debug", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["secrets", "list", "--debug", `--remote=${projectRef}`]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); }); describe("secrets:set", () => { - testBehaviour("sets a single secret", async ({ run, projectRef }) => { - const result = await run(["secrets", "set", "FOO=bar", "--yes", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Finished"); - }); - - testBehaviour("sets multiple secrets", async ({ run, projectRef }) => { - const result = await run([ - "secrets", - "set", - "FOO=bar", - "BAZ=qux", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Finished"); - }); - - testBehaviour("sets secrets from env file", async ({ run, workspace, projectRef }) => { - writeFileSync(join(workspace.path, ".env.local"), "FOO=bar\n"); - const result = await run([ - "secrets", - "set", - "--env-file", - ".env.local", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Finished"); - }); - - testBehaviour("exits non-zero on 422 invalid name", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - status: 422, - body: { message: "Validation failed", errors: [{ message: "Invalid secret name" }] }, - }), - }); - const result = await run(["secrets", "set", "FOO=bar", "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - }); - - testBehaviour("exits non-zero when env file not found", async ({ run }) => { - const result = await run([ - "secrets", - "set", - "--env-file", - "nonexistent.env", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run(["secrets", "set", "FOO=bar", "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - }); + testBehaviour("sets a single secret", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "secrets", + "set", + "FOO=bar", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Finished"); + }), + signal, + ), + ); + + testBehaviour("sets multiple secrets", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "secrets", + "set", + "FOO=bar", + "BAZ=qux", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Finished"); + }), + signal, + ), + ); + + testBehaviour("sets secrets from env file", ({ run, workspace, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fs.writeFileString(path.join(workspace.path, ".env.local"), "FOO=bar\n"); + const result = yield* run([ + "secrets", + "set", + "--env-file", + ".env.local", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Finished"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 422 invalid name", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 422, + body: { message: "Validation failed", errors: [{ message: "Invalid secret name" }] }, + }); + const result = yield* run([ + "secrets", + "set", + "FOO=bar", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); + + testBehaviour("exits non-zero when env file not found", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "secrets", + "set", + "--env-file", + "nonexistent.env", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "secrets", + "set", + "FOO=bar", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); }); describe("secrets:unset", () => { - testBehaviour("removes a secret", async ({ run, projectRef }) => { - const result = await run(["secrets", "unset", "FOO", "--yes", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Finished"); - }); - - testBehaviour("removes multiple secrets", async ({ run, projectRef }) => { - await run(["secrets", "set", "FOO=bar", "BAR=baz", "--yes", `--remote=${projectRef}`]); - const result = await run([ - "secrets", - "unset", - "FOO", - "BAR", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Finished"); - }); - - testBehaviour("exits non-zero on 404", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Secret not found" } }), - }); - const result = await run([ - "secrets", - "unset", - "NONEXISTENT", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run(["secrets", "unset", "FOO", "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run(["secrets", "unset", "FOO", "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - }); + testBehaviour("removes a secret", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["secrets", "unset", "FOO", "--yes", `--remote=${projectRef}`]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Finished"); + }), + signal, + ), + ); + + testBehaviour("removes multiple secrets", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + yield* run(["secrets", "set", "FOO=bar", "BAR=baz", "--yes", `--remote=${projectRef}`]); + const result = yield* run([ + "secrets", + "unset", + "FOO", + "BAR", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Finished"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Secret not found" }, + }); + const result = yield* run([ + "secrets", + "unset", + "NONEXISTENT", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "secrets", + "unset", + "FOO", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run([ + "secrets", + "unset", + "FOO", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + }), + signal, + ), + ); }); }); diff --git a/apps/cli-e2e/src/tests/sso.e2e.test.ts b/apps/cli-e2e/src/tests/sso.e2e.test.ts index af6a369fc4..f117212d3b 100644 --- a/apps/cli-e2e/src/tests/sso.e2e.test.ts +++ b/apps/cli-e2e/src/tests/sso.e2e.test.ts @@ -1,8 +1,7 @@ -import { writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { Effect, FileSystem, Path, Schema } from "effect"; import { describe, expect } from "vitest"; import { isRecording, PROJECT_REF, PROVIDER_ID } from "./env.ts"; -import { testBehaviour } from "./test-context.ts"; +import { decodeJsonOutput, postControl, runE2e, testBehaviour } from "./test-context.ts"; const MINIMAL_SAML_XML = ` @@ -13,94 +12,141 @@ const MINIMAL_SAML_XML = ` describe("sso", () => { describe("sso:list", () => { - testBehaviour("renders fixture data in output", async ({ run, projectRef }) => { - const result = await run(["sso", "list", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("IDENTITY PROVIDER ID"); - }); - - testBehaviour("returns json output with --output json", async ({ run, projectRef }) => { - const result = await run(["sso", "list", "--output", "json", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(JSON.parse(result.stdout)).toMatchObject({ providers: [] }); - }); - - testBehaviour("includes debug output with --debug", async ({ run, projectRef }) => { - const result = await run(["sso", "list", "--debug", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(result.stderr).toMatch(/HTTP.*GET:/); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run(["sso", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run(["sso", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); - - testBehaviour("exits non-zero on 404 project not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "Project not found" } }), - }); - const result = await run(["sso", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("SAML 2.0 support is not enabled"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run(["sso", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run(["sso", "list", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + testBehaviour("renders fixture data in output", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["sso", "list", `--remote=${projectRef}`]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("IDENTITY PROVIDER ID"); + }), + signal, + ), + ); + + testBehaviour("returns json output with --output json", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["sso", "list", "--output", "json", `--remote=${projectRef}`]); + expect(result.exitCode).toBe(0); + expect(yield* decodeJsonOutput(Schema.Unknown)(result.stdout)).toMatchObject({ + providers: [], + }); + }), + signal, + ), + ); + + testBehaviour("includes debug output with --debug", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["sso", "list", "--debug", `--remote=${projectRef}`]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toMatch(/HTTP.*GET:/); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run(["sso", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run(["sso", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 project not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "Project not found" }, + }); + const result = yield* run(["sso", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("SAML 2.0 support is not enabled"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run(["sso", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run(["sso", "list", `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); describe("sso:info", () => { - testBehaviour("renders fixture data in output", async ({ run, projectRef }) => { - const result = await run(["sso", "info", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("supabase.co/auth/v1/sso/saml/acs"); - }); - - testBehaviour("returns json output with --output json", async ({ run, projectRef }) => { - const result = await run(["sso", "info", "--output", "json", `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(JSON.parse(result.stdout)).toMatchObject({ - acs_url: expect.stringContaining("supabase.co/auth/v1/sso/saml/acs"), - }); - }); + testBehaviour("renders fixture data in output", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["sso", "info", `--remote=${projectRef}`]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("supabase.co/auth/v1/sso/saml/acs"); + }), + signal, + ), + ); + + testBehaviour("returns json output with --output json", ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["sso", "info", "--output", "json", `--remote=${projectRef}`]); + expect(result.exitCode).toBe(0); + expect(yield* decodeJsonOutput(Schema.Unknown)(result.stdout)).toMatchObject({ + acs_url: expect.stringContaining("supabase.co/auth/v1/sso/saml/acs"), + }); + }), + signal, + ), + ); // sso info makes no API calls — no error injection tests needed }); @@ -108,531 +154,716 @@ describe("sso", () => { describe("sso:show", () => { testBehaviour.skipIf(isRecording)( "renders fixture data in output", - async ({ run, projectRef }) => { - const result = await run(["sso", "show", PROVIDER_ID, `--remote=${projectRef}`]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("example.com"); - }, + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["sso", "show", PROVIDER_ID, `--remote=${projectRef}`]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("example.com"); + }), + signal, + ), ); testBehaviour.skipIf(isRecording)( "returns json output with --output json", - async ({ run, projectRef }) => { - const result = await run([ - "sso", - "show", - PROVIDER_ID, - "--output", - "json", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("example.com"); - }, + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "sso", + "show", + PROVIDER_ID, + "--output", + "json", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("example.com"); + }), + signal, + ), ); testBehaviour.skipIf(isRecording)( "shows raw SAML metadata XML with --metadata", - async ({ run, projectRef }) => { - const result = await run([ - "sso", - "show", - PROVIDER_ID, - "--metadata", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("EntityDescriptor"); - }, - ); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run(["sso", "show", PROVIDER_ID, `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run(["sso", "show", PROVIDER_ID, `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); - - testBehaviour("exits non-zero on 404 provider not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "SSO Identity Provider not found" } }), - }); - const result = await run(["sso", "show", PROVIDER_ID, `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("could not be found"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run(["sso", "show", PROVIDER_ID, `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run(["sso", "show", PROVIDER_ID, `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "sso", + "show", + PROVIDER_ID, + "--metadata", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("EntityDescriptor"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run(["sso", "show", PROVIDER_ID, `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run(["sso", "show", PROVIDER_ID, `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 provider not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "SSO Identity Provider not found" }, + }); + const result = yield* run(["sso", "show", PROVIDER_ID, `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("could not be found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run(["sso", "show", PROVIDER_ID, `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run(["sso", "show", PROVIDER_ID, `--remote=${PROJECT_REF}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); describe("sso:add", () => { testBehaviour( "adds SAML provider via metadata file", - async ({ run, projectRef, workspace }) => { - const metadataPath = join(workspace.path, "saml.xml"); - writeFileSync(metadataPath, MINIMAL_SAML_XML); - const result = await run([ - "sso", - "add", - "--type", - "saml", - "--metadata-file", - metadataPath, - "--domains", - "example.com", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("example.com"); - }, - ); - - testBehaviour("exits non-zero without --type", async ({ run }) => { - const result = await run([ - "sso", - "add", - "--metadata-url", - "https://example.com/saml/metadata", - "--skip-url-validation", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain('"type"'); - }); + ({ run, projectRef, workspace, signal }) => + runE2e( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const metadataPath = path.join(workspace.path, "saml.xml"); + yield* fs.writeFileString(metadataPath, MINIMAL_SAML_XML); + const result = yield* run([ + "sso", + "add", + "--type", + "saml", + "--metadata-file", + metadataPath, + "--domains", + "example.com", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("example.com"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero without --type", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "sso", + "add", + "--metadata-url", + "https://example.com/saml/metadata", + "--skip-url-validation", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain('"type"'); + }), + signal, + ), + ); testBehaviour( "exits non-zero with both --metadata-url and --metadata-file", - async ({ run, workspace }) => { - const metadataPath = join(workspace.path, "saml.xml"); - writeFileSync(metadataPath, MINIMAL_SAML_XML); - const result = await run([ - "sso", - "add", - "--type", - "saml", - "--metadata-url", - "https://example.com/saml/metadata", - "--metadata-file", - metadataPath, - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("metadata"); - }, - ); - - testBehaviour("exits non-zero with unreachable --metadata-url", async ({ run }) => { - const result = await run([ - "sso", - "add", - "--type", - "saml", - "--metadata-url", - "http://localhost:19999/saml.xml", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("HTTPS"); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl, workspace }) => { - const metadataPath = join(workspace.path, "saml.xml"); - writeFileSync(metadataPath, MINIMAL_SAML_XML); - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "sso", - "add", - "--type", - "saml", - "--metadata-file", - metadataPath, - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 403", async ({ run, apiUrl, workspace }) => { - const metadataPath = join(workspace.path, "saml.xml"); - writeFileSync(metadataPath, MINIMAL_SAML_XML); - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run([ - "sso", - "add", - "--type", - "saml", - "--metadata-file", - metadataPath, - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); - - testBehaviour("exits non-zero on 422 invalid metadata", async ({ run, apiUrl, workspace }) => { - const metadataPath = join(workspace.path, "saml.xml"); - writeFileSync(metadataPath, MINIMAL_SAML_XML); - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - status: 422, - body: { message: "Invalid SAML metadata" }, - }), - }); - const result = await run([ - "sso", - "add", - "--type", - "saml", - "--metadata-file", - metadataPath, - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid SAML metadata"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl, workspace }) => { - const metadataPath = join(workspace.path, "saml.xml"); - writeFileSync(metadataPath, MINIMAL_SAML_XML); - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run([ - "sso", - "add", - "--type", - "saml", - "--metadata-file", - metadataPath, - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl, workspace }) => { - const metadataPath = join(workspace.path, "saml.xml"); - writeFileSync(metadataPath, MINIMAL_SAML_XML); - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run([ - "sso", - "add", - "--type", - "saml", - "--metadata-file", - metadataPath, - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + ({ run, workspace, signal }) => + runE2e( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const metadataPath = path.join(workspace.path, "saml.xml"); + yield* fs.writeFileString(metadataPath, MINIMAL_SAML_XML); + const result = yield* run([ + "sso", + "add", + "--type", + "saml", + "--metadata-url", + "https://example.com/saml/metadata", + "--metadata-file", + metadataPath, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("metadata"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero with unreachable --metadata-url", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "sso", + "add", + "--type", + "saml", + "--metadata-url", + "http://localhost:19999/saml.xml", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("HTTPS"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, workspace, signal }) => + runE2e( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const metadataPath = path.join(workspace.path, "saml.xml"); + yield* fs.writeFileString(metadataPath, MINIMAL_SAML_XML); + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "sso", + "add", + "--type", + "saml", + "--metadata-file", + metadataPath, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 403", ({ run, apiUrl, workspace, signal }) => + runE2e( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const metadataPath = path.join(workspace.path, "saml.xml"); + yield* fs.writeFileString(metadataPath, MINIMAL_SAML_XML); + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run([ + "sso", + "add", + "--type", + "saml", + "--metadata-file", + metadataPath, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 422 invalid metadata", ({ run, apiUrl, workspace, signal }) => + runE2e( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const metadataPath = path.join(workspace.path, "saml.xml"); + yield* fs.writeFileString(metadataPath, MINIMAL_SAML_XML); + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 422, + body: { message: "Invalid SAML metadata" }, + }); + const result = yield* run([ + "sso", + "add", + "--type", + "saml", + "--metadata-file", + metadataPath, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid SAML metadata"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, workspace, signal }) => + runE2e( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const metadataPath = path.join(workspace.path, "saml.xml"); + yield* fs.writeFileString(metadataPath, MINIMAL_SAML_XML); + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run([ + "sso", + "add", + "--type", + "saml", + "--metadata-file", + metadataPath, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, workspace, signal }) => + runE2e( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const metadataPath = path.join(workspace.path, "saml.xml"); + yield* fs.writeFileString(metadataPath, MINIMAL_SAML_XML); + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run([ + "sso", + "add", + "--type", + "saml", + "--metadata-file", + metadataPath, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); describe("sso:update", () => { testBehaviour.skipIf(isRecording)( "appends domain with --add-domains", - async ({ run, projectRef }) => { - const result = await run([ - "sso", - "update", - PROVIDER_ID, - "--add-domains", - "example.com", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("example.com"); - }, + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "sso", + "update", + PROVIDER_ID, + "--add-domains", + "example.com", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("example.com"); + }), + signal, + ), ); testBehaviour.skipIf(isRecording)( "replaces domains with --domains", - async ({ run, projectRef }) => { - const result = await run([ - "sso", - "update", - PROVIDER_ID, - "--domains", - "new.com", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("new.com"); - }, + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "sso", + "update", + PROVIDER_ID, + "--domains", + "new.com", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("new.com"); + }), + signal, + ), ); testBehaviour.skipIf(isRecording)( "removes domain with --remove-domains", - async ({ run, projectRef }) => { - const result = await run([ - "sso", - "update", - PROVIDER_ID, - "--remove-domains", - "example.com", - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("IDENTITY PROVIDER ID"); - }, + ({ run, projectRef, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "sso", + "update", + PROVIDER_ID, + "--remove-domains", + "example.com", + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("IDENTITY PROVIDER ID"); + }), + signal, + ), ); testBehaviour.skipIf(isRecording)( "updates metadata via metadata file", - async ({ run, projectRef, workspace }) => { - const metadataPath = join(workspace.path, "saml.xml"); - writeFileSync(metadataPath, MINIMAL_SAML_XML); - const result = await run([ - "sso", - "update", - PROVIDER_ID, - "--metadata-file", - metadataPath, - "--yes", - `--remote=${projectRef}`, - ]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("EntityDescriptor"); - }, - ); - - testBehaviour("exits non-zero with --domains and --add-domains", async ({ run }) => { - const result = await run([ - "sso", - "update", - PROVIDER_ID, - "--domains", - "a.com", - "--add-domains", - "b.com", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("domains"); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run([ - "sso", - "update", - PROVIDER_ID, - "--add-domains", - "example.com", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run([ - "sso", - "update", - PROVIDER_ID, - "--add-domains", - "example.com", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); - - testBehaviour("exits non-zero on 404 provider not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "SSO Identity Provider not found" } }), - }); - const result = await run([ - "sso", - "update", - PROVIDER_ID, - "--add-domains", - "example.com", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("could not be found"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run([ - "sso", - "update", - PROVIDER_ID, - "--add-domains", - "example.com", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run([ - "sso", - "update", - PROVIDER_ID, - "--add-domains", - "example.com", - "--yes", - `--remote=${PROJECT_REF}`, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + ({ run, projectRef, workspace, signal }) => + runE2e( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const metadataPath = path.join(workspace.path, "saml.xml"); + yield* fs.writeFileString(metadataPath, MINIMAL_SAML_XML); + const result = yield* run([ + "sso", + "update", + PROVIDER_ID, + "--metadata-file", + metadataPath, + "--yes", + `--remote=${projectRef}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("EntityDescriptor"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero with --domains and --add-domains", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "sso", + "update", + PROVIDER_ID, + "--domains", + "a.com", + "--add-domains", + "b.com", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("domains"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "sso", + "update", + PROVIDER_ID, + "--add-domains", + "example.com", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run([ + "sso", + "update", + PROVIDER_ID, + "--add-domains", + "example.com", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 provider not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "SSO Identity Provider not found" }, + }); + const result = yield* run([ + "sso", + "update", + PROVIDER_ID, + "--add-domains", + "example.com", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("could not be found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run([ + "sso", + "update", + PROVIDER_ID, + "--add-domains", + "example.com", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run([ + "sso", + "update", + PROVIDER_ID, + "--add-domains", + "example.com", + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); describe("sso:remove", () => { - testBehaviour.skipIf(isRecording)("removes a provider", async ({ run }) => { - const result = await run(["sso", "remove", PROVIDER_ID, "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("example.com"); - }); - - testBehaviour("exits non-zero on 401", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 401, body: { message: "Invalid token" } }), - }); - const result = await run(["sso", "remove", PROVIDER_ID, "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Invalid token"); - }); - - testBehaviour("exits non-zero on 403", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 403, body: { message: "Forbidden" } }), - }); - const result = await run(["sso", "remove", PROVIDER_ID, "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Forbidden"); - }); - - testBehaviour("exits non-zero on 404 provider not found", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 404, body: { message: "SSO Identity Provider not found" } }), - }); - const result = await run(["sso", "remove", PROVIDER_ID, "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("could not be found"); - }); - - testBehaviour("exits non-zero on 429", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 429, body: { message: "Too Many Requests" } }), - }); - const result = await run(["sso", "remove", PROVIDER_ID, "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Too Many Requests"); - }); - - testBehaviour("exits non-zero on 500", async ({ run, apiUrl }) => { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: 500, body: { message: "Internal Server Error" } }), - }); - const result = await run(["sso", "remove", PROVIDER_ID, "--yes", `--remote=${PROJECT_REF}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Internal Server Error"); - }); + testBehaviour.skipIf(isRecording)("removes a provider", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run([ + "sso", + "remove", + PROVIDER_ID, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("example.com"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 401", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 401, + body: { message: "Invalid token" }, + }); + const result = yield* run([ + "sso", + "remove", + PROVIDER_ID, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid token"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 403", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 403, + body: { message: "Forbidden" }, + }); + const result = yield* run([ + "sso", + "remove", + PROVIDER_ID, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Forbidden"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 404 provider not found", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 404, + body: { message: "SSO Identity Provider not found" }, + }); + const result = yield* run([ + "sso", + "remove", + PROVIDER_ID, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("could not be found"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 429", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 429, + body: { message: "Too Many Requests" }, + }); + const result = yield* run([ + "sso", + "remove", + PROVIDER_ID, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Too Many Requests"); + }), + signal, + ), + ); + + testBehaviour("exits non-zero on 500", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* postControl(`${apiUrl}/_ctrl/error-all`, { + status: 500, + body: { message: "Internal Server Error" }, + }); + const result = yield* run([ + "sso", + "remove", + PROVIDER_ID, + "--yes", + `--remote=${PROJECT_REF}`, + ]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Internal Server Error"); + }), + signal, + ), + ); }); }); diff --git a/apps/cli-e2e/src/tests/stack.e2e.test.ts b/apps/cli-e2e/src/tests/stack.e2e.test.ts index d36ea2ae23..ec0b7b41a4 100644 --- a/apps/cli-e2e/src/tests/stack.e2e.test.ts +++ b/apps/cli-e2e/src/tests/stack.e2e.test.ts @@ -1,9 +1,8 @@ -import { mkdirSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { Effect, FileSystem, Path } from "effect"; import { describe, expect, inject, test } from "vitest"; -import { createHarness, exec, type CLIResult } from "@supabase/cli-test-helpers"; +import { createHarness, type CLIResult } from "@supabase/cli-test-helpers"; import { getRequestLog } from "./request-log.ts"; -import { testBehaviour } from "./test-context.ts"; +import { type CliExecError, execCli, runE2e, testBehaviour } from "./test-context.ts"; import { ACCESS_TOKEN } from "./env.ts"; // A guaranteed-unreachable TCP address — connection is refused immediately. @@ -11,10 +10,15 @@ import { ACCESS_TOKEN } from "./env.ts"; const UNREACHABLE_DOCKER_HOST = "tcp://localhost:1"; // Minimal config.toml required by start/stop/status. -function setupStackWorkspace(dir: string): void { - mkdirSync(join(dir, "supabase"), { recursive: true }); - writeFileSync(join(dir, "supabase", "config.toml"), 'project_id = "test-project"\n'); -} +const setupStackWorkspace = Effect.fnUntraced(function* (dir: string) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fs.makeDirectory(path.join(dir, "supabase"), { recursive: true }); + yield* fs.writeFileString( + path.join(dir, "supabase", "config.toml"), + 'project_id = "test-project"\n', + ); +}); // Extends testBehaviour with a `stackRun` fixture that automatically passes // DOCKER_HOST to the CLI subprocess, pointing it at the relay server. @@ -23,11 +27,14 @@ function setupStackWorkspace(dir: string): void { // The optional `dockerHost` override lets individual tests substitute an // unreachable host to simulate Docker being unavailable. interface StackFixtures { - stackRun: (cmd: string[], opts?: { dockerHost?: string }) => Promise; + stackRun: ( + cmd: string[], + opts?: { dockerHost?: string }, + ) => Effect.Effect; } const testStack = testBehaviour.extend({ - stackRun: async ({ workspace }, use) => { + stackRun: ({ runGuard, workspace }, use) => { const serverUrl = inject("replayServerUrl") as string; const dockerHostUrl = inject("dockerHostUrl") as string; const harness = createHarness({ @@ -35,65 +42,90 @@ const testStack = testBehaviour.extend({ accessToken: ACCESS_TOKEN, cwd: workspace.path, }); - await use((cmd, opts) => - exec(harness, cmd, { env: { DOCKER_HOST: opts?.dockerHost ?? dockerHostUrl } }), + return use((cmd, opts) => + runGuard.track( + execCli(harness, cmd, { env: { DOCKER_HOST: opts?.dockerHost ?? dockerHostUrl } }), + ), ); }, }); // `services` prints a static service matrix, so DOCKER_HOST is not needed. describe("services", () => { - testBehaviour("lists known service images", async ({ run }) => { - const result = await run(["services"]); - expect(result.exitCode).toBe(0); - // Output is a pipe-separated markdown table; verify well-known image names appear. - expect(result.stdout).toContain("postgres"); - expect(result.stdout).toContain("gotrue"); - expect(result.stdout).toContain("storage"); - }); + testBehaviour("lists known service images", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["services"]); + expect(result.exitCode).toBe(0); + // Output is a pipe-separated markdown table; verify well-known image names appear. + expect(result.stdout).toContain("postgres"); + expect(result.stdout).toContain("gotrue"); + expect(result.stdout).toContain("storage"); + }), + signal, + ), + ); }); // `status` resolves and prints the current linked project/branch on stdout // before any Docker/daemon work runs, in every output mode. describe("status", () => { - testStack("exits 1 when stack is not running", async ({ workspace, stackRun }) => { - setupStackWorkspace(workspace.path); - const result = await stackRun(["status"]); - expect(result.exitCode).toBe(1); - expect(result.stderr).toMatch(/no such container/i); - }); + testStack("exits 1 when stack is not running", ({ workspace, stackRun, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStackWorkspace(workspace.path); + const result = yield* stackRun(["status"]); + expect(result.exitCode).toBe(1); + expect(result.stderr).toMatch(/no such container/i); + }), + signal, + ), + ); }); describe("stop", () => { - testStack("succeeds when stack is not running", async ({ workspace, stackRun }) => { - setupStackWorkspace(workspace.path); - const result = await stackRun(["stop"]); - expect(result.exitCode).toBe(0); - }); + testStack("succeeds when stack is not running", ({ workspace, stackRun, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStackWorkspace(workspace.path); + const result = yield* stackRun(["stop"]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); // Flag validation happens before the command runs, so no Docker or API calls are made. testStack( "exits 1 with mutual-exclusion error for --project-id and --all", - async ({ workspace, stackRun }) => { - setupStackWorkspace(workspace.path); - const result = await stackRun(["stop", "--project-id", "test-project", "--all"]); - expect(result.exitCode).toBe(1); - expect(result.stderr).toMatch(/mutually exclusive|if any flags in the group.*are set/i); - }, + ({ workspace, stackRun, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStackWorkspace(workspace.path); + const result = yield* stackRun(["stop", "--project-id", "test-project", "--all"]); + expect(result.exitCode).toBe(1); + expect(result.stderr).toMatch(/mutually exclusive|if any flags in the group.*are set/i); + }), + signal, + ), ); }); describe("start", () => { testStack( "exits 1 with Docker error when Docker is unavailable", - async ({ workspace, stackRun }) => { - setupStackWorkspace(workspace.path); - // Use an unreachable host so connection fails immediately without waiting - // for a timeout. The relay has no special handling for this case. - const result = await stackRun(["start"], { dockerHost: UNREACHABLE_DOCKER_HOST }); - expect(result.exitCode).toBe(1); - expect(result.stderr.length).toBeGreaterThan(0); - }, + ({ workspace, stackRun, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStackWorkspace(workspace.path); + // Use an unreachable host so connection fails immediately without waiting + // for a timeout. The relay has no special handling for this case. + const result = yield* stackRun(["start"], { dockerHost: UNREACHABLE_DOCKER_HOST }); + expect(result.exitCode).toBe(1); + expect(result.stderr.length).toBeGreaterThan(0); + }), + signal, + ), ); // Must run in sequence in a shared workspace so status/stop see the stack start brought up. @@ -105,27 +137,34 @@ describe("start", () => { // `seed buckets` makes storage HTTP calls (not Docker), so plain testBehaviour // with `run` is correct. describe("seed buckets", () => { - testBehaviour("creates buckets defined in config", async ({ workspace, run, apiUrl }) => { - mkdirSync(join(workspace.path, "supabase"), { recursive: true }); - writeFileSync( - join(workspace.path, "supabase", "config.toml"), - [ - 'project_id = "test-project"', - "", - "[api]", - // Point the local stack API at the relay server so bucket creation - // calls are captured. - `port = ${new URL(apiUrl).port}`, - "", - "[storage.buckets.my-bucket]", - "public = false", - ].join("\n"), - ); - const result = await run(["seed", "buckets"]); - expect(result.exitCode).toBe(0); - const requests = await getRequestLog(apiUrl); - expect(requests.some((r) => r.method === "POST" && r.pathname === "/storage/v1/bucket")).toBe( - true, - ); - }); + testBehaviour("creates buckets defined in config", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fs.makeDirectory(path.join(workspace.path, "supabase"), { recursive: true }); + yield* fs.writeFileString( + path.join(workspace.path, "supabase", "config.toml"), + [ + 'project_id = "test-project"', + "", + "[api]", + // Point the local stack API at the relay server so bucket creation + // calls are captured. + `port = ${new URL(apiUrl).port}`, + "", + "[storage.buckets.my-bucket]", + "public = false", + ].join("\n"), + ); + const result = yield* run(["seed", "buckets"]); + expect(result.exitCode).toBe(0); + const requests = yield* getRequestLog(apiUrl); + expect( + requests.some((r) => r.method === "POST" && r.pathname === "/storage/v1/bucket"), + ).toBe(true); + }), + signal, + ), + ); }); diff --git a/apps/cli-e2e/src/tests/storage.e2e.test.ts b/apps/cli-e2e/src/tests/storage.e2e.test.ts index 21f1144fb8..e0b9173a1a 100644 --- a/apps/cli-e2e/src/tests/storage.e2e.test.ts +++ b/apps/cli-e2e/src/tests/storage.e2e.test.ts @@ -1,438 +1,583 @@ -import { mkdirSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { Effect, FileSystem, Path } from "effect"; +import { HttpClient } from "effect/http"; import { describe, expect } from "vitest"; import { getRequestLog } from "./request-log.ts"; -import { testBehaviour } from "./test-context.ts"; +import { postControl, runE2e, testBehaviour } from "./test-context.ts"; const BUCKET = "cli-e2e-bucket"; const EXPERIMENTAL_FLAGS = ["--experimental"]; -function setupStorageWorkspace(dir: string, relayUrl: string): void { - mkdirSync(join(dir, "supabase"), { recursive: true }); - writeFileSync( - join(dir, "supabase", "config.toml"), +const setupStorageWorkspace = Effect.fnUntraced(function* (dir: string, relayUrl: string) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fs.makeDirectory(path.join(dir, "supabase"), { recursive: true }); + yield* fs.writeFileString( + path.join(dir, "supabase", "config.toml"), ['project_id = "test-project"', "", "[api]", `external_url = "${relayUrl}"`].join("\n"), ); - writeFileSync(join(dir, "upload.txt"), "test upload content"); -} + yield* fs.writeFileString(path.join(dir, "upload.txt"), "test upload content"); +}); -async function injectGlobalError(apiUrl: string, status: number, message: string): Promise { - await fetch(`${apiUrl}/_ctrl/error-all`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status, body: { message } }), - }); -} +const injectGlobalError = (apiUrl: string, status: number, message: string) => + postControl(`${apiUrl}/_ctrl/error-all`, { status, body: { message } }); -async function clearOverrides(apiUrl: string): Promise { - await fetch(`${apiUrl}/_ctrl/overrides`, { method: "DELETE" }); -} +const clearOverrides = (apiUrl: string) => HttpClient.del(`${apiUrl}/_ctrl/overrides`); describe("storage ls", () => { - testBehaviour("lists objects in bucket", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - const result = await run(["storage", "ls", ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/`]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("hello.txt"); - const requests = await getRequestLog(apiUrl); - expect( - requests.some( - (r) => r.method === "POST" && r.pathname === `/storage/v1/object/list/${BUCKET}`, - ), - ).toBe(true); - }); + testBehaviour("lists objects in bucket", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + const result = yield* run(["storage", "ls", ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/`]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("hello.txt"); + const requests = yield* getRequestLog(apiUrl); + expect( + requests.some( + (r) => r.method === "POST" && r.pathname === `/storage/v1/object/list/${BUCKET}`, + ), + ).toBe(true); + }), + signal, + ), + ); - testBehaviour("lists objects recursively", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - const result = await run(["storage", "ls", ...EXPERIMENTAL_FLAGS, "-r", `ss:///${BUCKET}/`]); - expect(result.exitCode).toBe(0); - const requests = await getRequestLog(apiUrl); - expect( - requests.some( - (r) => r.method === "POST" && r.pathname === `/storage/v1/object/list/${BUCKET}`, - ), - ).toBe(true); - }); + testBehaviour("lists objects recursively", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + const result = yield* run([ + "storage", + "ls", + ...EXPERIMENTAL_FLAGS, + "-r", + `ss:///${BUCKET}/`, + ]); + expect(result.exitCode).toBe(0); + const requests = yield* getRequestLog(apiUrl); + expect( + requests.some( + (r) => r.method === "POST" && r.pathname === `/storage/v1/object/list/${BUCKET}`, + ), + ).toBe(true); + }), + signal, + ), + ); - testBehaviour("exits 1 on 401", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - await injectGlobalError(apiUrl, 401, "Invalid token"); - const result = await run(["storage", "ls", ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/`]); - expect(result.exitCode).toBe(1); - expect(result.stderr).toContain("Invalid token"); - await clearOverrides(apiUrl); - }); + testBehaviour("exits 1 on 401", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + yield* injectGlobalError(apiUrl, 401, "Invalid token"); + const result = yield* run(["storage", "ls", ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/`]); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain("Invalid token"); + yield* clearOverrides(apiUrl); + }), + signal, + ), + ); - testBehaviour("exits 1 on 403", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - await injectGlobalError(apiUrl, 403, "Forbidden"); - const result = await run(["storage", "ls", ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/`]); - expect(result.exitCode).toBe(1); - expect(result.stderr.length).toBeGreaterThan(0); - await clearOverrides(apiUrl); - }); + testBehaviour("exits 1 on 403", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + yield* injectGlobalError(apiUrl, 403, "Forbidden"); + const result = yield* run(["storage", "ls", ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/`]); + expect(result.exitCode).toBe(1); + expect(result.stderr.length).toBeGreaterThan(0); + yield* clearOverrides(apiUrl); + }), + signal, + ), + ); - testBehaviour("exits 1 on 429", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - await injectGlobalError(apiUrl, 429, "Too Many Requests"); - const result = await run(["storage", "ls", ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/`]); - expect(result.exitCode).toBe(1); - expect(result.stderr.length).toBeGreaterThan(0); - await clearOverrides(apiUrl); - }); + testBehaviour("exits 1 on 429", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + yield* injectGlobalError(apiUrl, 429, "Too Many Requests"); + const result = yield* run(["storage", "ls", ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/`]); + expect(result.exitCode).toBe(1); + expect(result.stderr.length).toBeGreaterThan(0); + yield* clearOverrides(apiUrl); + }), + signal, + ), + ); - testBehaviour("exits 1 on 500", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - await injectGlobalError(apiUrl, 500, "Internal Server Error"); - const result = await run(["storage", "ls", ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/`]); - expect(result.exitCode).toBe(1); - expect(result.stderr.length).toBeGreaterThan(0); - await clearOverrides(apiUrl); - }); + testBehaviour("exits 1 on 500", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + yield* injectGlobalError(apiUrl, 500, "Internal Server Error"); + const result = yield* run(["storage", "ls", ...EXPERIMENTAL_FLAGS, `ss:///${BUCKET}/`]); + expect(result.exitCode).toBe(1); + expect(result.stderr.length).toBeGreaterThan(0); + yield* clearOverrides(apiUrl); + }), + signal, + ), + ); }); describe("storage cp", () => { - testBehaviour("uploads local file to storage", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - const result = await run([ - "storage", - "cp", - ...EXPERIMENTAL_FLAGS, - "upload.txt", - `ss:///${BUCKET}/upload.txt`, - ]); - expect(result.exitCode).toBe(0); - const requests = await getRequestLog(apiUrl); - expect( - requests.some( - (r) => r.method === "POST" && r.pathname === `/storage/v1/object/${BUCKET}/upload.txt`, - ), - ).toBe(true); - }); + testBehaviour("uploads local file to storage", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + const result = yield* run([ + "storage", + "cp", + ...EXPERIMENTAL_FLAGS, + "upload.txt", + `ss:///${BUCKET}/upload.txt`, + ]); + expect(result.exitCode).toBe(0); + const requests = yield* getRequestLog(apiUrl); + expect( + requests.some( + (r) => r.method === "POST" && r.pathname === `/storage/v1/object/${BUCKET}/upload.txt`, + ), + ).toBe(true); + }), + signal, + ), + ); - testBehaviour("passes --cache-control header on upload", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - const result = await run([ - "storage", - "cp", - ...EXPERIMENTAL_FLAGS, - "--cache-control", - "no-cache", - "upload.txt", - `ss:///${BUCKET}/cached.txt`, - ]); - expect(result.exitCode).toBe(0); - const requests = await getRequestLog(apiUrl); - const uploadReq = requests.find( - (r) => r.method === "POST" && r.pathname.startsWith(`/storage/v1/object/${BUCKET}/`), - ); - expect(uploadReq).toBeDefined(); - expect(uploadReq?.headers["cache-control"]).toBe("no-cache"); - }); + testBehaviour("passes --cache-control header on upload", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + const result = yield* run([ + "storage", + "cp", + ...EXPERIMENTAL_FLAGS, + "--cache-control", + "no-cache", + "upload.txt", + `ss:///${BUCKET}/cached.txt`, + ]); + expect(result.exitCode).toBe(0); + const requests = yield* getRequestLog(apiUrl); + const uploadReq = requests.find( + (r) => r.method === "POST" && r.pathname.startsWith(`/storage/v1/object/${BUCKET}/`), + ); + expect(uploadReq).toBeDefined(); + expect(uploadReq?.headers["cache-control"]).toBe("no-cache"); + }), + signal, + ), + ); - testBehaviour("passes --content-type header on upload", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - const result = await run([ - "storage", - "cp", - ...EXPERIMENTAL_FLAGS, - "--content-type", - "application/json", - "upload.txt", - `ss:///${BUCKET}/typed.txt`, - ]); - expect(result.exitCode).toBe(0); - const requests = await getRequestLog(apiUrl); - const uploadReq = requests.find( - (r) => r.method === "POST" && r.pathname.startsWith(`/storage/v1/object/${BUCKET}/`), - ); - expect(uploadReq).toBeDefined(); - expect(uploadReq?.headers["content-type"]).toContain("application/json"); - }); + testBehaviour("passes --content-type header on upload", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + const result = yield* run([ + "storage", + "cp", + ...EXPERIMENTAL_FLAGS, + "--content-type", + "application/json", + "upload.txt", + `ss:///${BUCKET}/typed.txt`, + ]); + expect(result.exitCode).toBe(0); + const requests = yield* getRequestLog(apiUrl); + const uploadReq = requests.find( + (r) => r.method === "POST" && r.pathname.startsWith(`/storage/v1/object/${BUCKET}/`), + ); + expect(uploadReq).toBeDefined(); + expect(uploadReq?.headers["content-type"]).toContain("application/json"); + }), + signal, + ), + ); - testBehaviour("exits 1 when source file not found", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - const result = await run([ - "storage", - "cp", - ...EXPERIMENTAL_FLAGS, - "nonexistent.txt", - `ss:///${BUCKET}/x.txt`, - ]); - expect(result.exitCode).toBe(1); - }); + testBehaviour("exits 1 when source file not found", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + const result = yield* run([ + "storage", + "cp", + ...EXPERIMENTAL_FLAGS, + "nonexistent.txt", + `ss:///${BUCKET}/x.txt`, + ]); + expect(result.exitCode).toBe(1); + }), + signal, + ), + ); - testBehaviour("exits 1 on 401", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - await injectGlobalError(apiUrl, 401, "Invalid token"); - const result = await run([ - "storage", - "cp", - ...EXPERIMENTAL_FLAGS, - "upload.txt", - `ss:///${BUCKET}/upload.txt`, - ]); - expect(result.exitCode).toBe(1); - expect(result.stderr).toContain("Invalid token"); - await clearOverrides(apiUrl); - }); + testBehaviour("exits 1 on 401", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + yield* injectGlobalError(apiUrl, 401, "Invalid token"); + const result = yield* run([ + "storage", + "cp", + ...EXPERIMENTAL_FLAGS, + "upload.txt", + `ss:///${BUCKET}/upload.txt`, + ]); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain("Invalid token"); + yield* clearOverrides(apiUrl); + }), + signal, + ), + ); - testBehaviour("exits 1 on 403", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - await injectGlobalError(apiUrl, 403, "Forbidden"); - const result = await run([ - "storage", - "cp", - ...EXPERIMENTAL_FLAGS, - "upload.txt", - `ss:///${BUCKET}/upload.txt`, - ]); - expect(result.exitCode).toBe(1); - expect(result.stderr.length).toBeGreaterThan(0); - await clearOverrides(apiUrl); - }); + testBehaviour("exits 1 on 403", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + yield* injectGlobalError(apiUrl, 403, "Forbidden"); + const result = yield* run([ + "storage", + "cp", + ...EXPERIMENTAL_FLAGS, + "upload.txt", + `ss:///${BUCKET}/upload.txt`, + ]); + expect(result.exitCode).toBe(1); + expect(result.stderr.length).toBeGreaterThan(0); + yield* clearOverrides(apiUrl); + }), + signal, + ), + ); - testBehaviour("exits 1 on 429", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - await injectGlobalError(apiUrl, 429, "Too Many Requests"); - const result = await run([ - "storage", - "cp", - ...EXPERIMENTAL_FLAGS, - "upload.txt", - `ss:///${BUCKET}/upload.txt`, - ]); - expect(result.exitCode).toBe(1); - expect(result.stderr.length).toBeGreaterThan(0); - await clearOverrides(apiUrl); - }); + testBehaviour("exits 1 on 429", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + yield* injectGlobalError(apiUrl, 429, "Too Many Requests"); + const result = yield* run([ + "storage", + "cp", + ...EXPERIMENTAL_FLAGS, + "upload.txt", + `ss:///${BUCKET}/upload.txt`, + ]); + expect(result.exitCode).toBe(1); + expect(result.stderr.length).toBeGreaterThan(0); + yield* clearOverrides(apiUrl); + }), + signal, + ), + ); - testBehaviour("exits 1 on 500", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - await injectGlobalError(apiUrl, 500, "Internal Server Error"); - const result = await run([ - "storage", - "cp", - ...EXPERIMENTAL_FLAGS, - "upload.txt", - `ss:///${BUCKET}/upload.txt`, - ]); - expect(result.exitCode).toBe(1); - expect(result.stderr.length).toBeGreaterThan(0); - await clearOverrides(apiUrl); - }); + testBehaviour("exits 1 on 500", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + yield* injectGlobalError(apiUrl, 500, "Internal Server Error"); + const result = yield* run([ + "storage", + "cp", + ...EXPERIMENTAL_FLAGS, + "upload.txt", + `ss:///${BUCKET}/upload.txt`, + ]); + expect(result.exitCode).toBe(1); + expect(result.stderr.length).toBeGreaterThan(0); + yield* clearOverrides(apiUrl); + }), + signal, + ), + ); - testBehaviour("downloads file from storage", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - const result = await run([ - "storage", - "cp", - ...EXPERIMENTAL_FLAGS, - `ss:///${BUCKET}/hello.txt`, - "hello-download.txt", - ]); - expect(result.exitCode).toBe(0); - }); + testBehaviour("downloads file from storage", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + const result = yield* run([ + "storage", + "cp", + ...EXPERIMENTAL_FLAGS, + `ss:///${BUCKET}/hello.txt`, + "hello-download.txt", + ]); + expect(result.exitCode).toBe(0); + }), + signal, + ), + ); }); describe("storage mv", () => { - testBehaviour("moves file within bucket", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - // Upload source file so the move has something to move in staging. - await run([ - "storage", - "cp", - ...EXPERIMENTAL_FLAGS, - "upload.txt", - `ss:///${BUCKET}/mv-source.txt`, - ]); - const result = await run([ - "storage", - "mv", - ...EXPERIMENTAL_FLAGS, - `ss:///${BUCKET}/mv-source.txt`, - `ss:///${BUCKET}/mv-dest.txt`, - ]); - expect(result.exitCode).toBe(0); - const requests = await getRequestLog(apiUrl); - const moveReq = requests.find( - (r) => r.method === "POST" && r.pathname === "/storage/v1/object/move", - ); - expect(moveReq).toBeDefined(); - expect(moveReq?.body).toMatchObject({ - bucketId: BUCKET, - sourceKey: "mv-source.txt", - destinationKey: "mv-dest.txt", - }); - }); + testBehaviour("moves file within bucket", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + // Upload source file so the move has something to move in staging. + yield* run([ + "storage", + "cp", + ...EXPERIMENTAL_FLAGS, + "upload.txt", + `ss:///${BUCKET}/mv-source.txt`, + ]); + const result = yield* run([ + "storage", + "mv", + ...EXPERIMENTAL_FLAGS, + `ss:///${BUCKET}/mv-source.txt`, + `ss:///${BUCKET}/mv-dest.txt`, + ]); + expect(result.exitCode).toBe(0); + const requests = yield* getRequestLog(apiUrl); + const moveReq = requests.find( + (r) => r.method === "POST" && r.pathname === "/storage/v1/object/move", + ); + expect(moveReq).toBeDefined(); + expect(moveReq?.body).toMatchObject({ + bucketId: BUCKET, + sourceKey: "mv-source.txt", + destinationKey: "mv-dest.txt", + }); + }), + signal, + ), + ); - testBehaviour("exits 1 on 401", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - await injectGlobalError(apiUrl, 401, "Invalid token"); - const result = await run([ - "storage", - "mv", - ...EXPERIMENTAL_FLAGS, - `ss:///${BUCKET}/a.txt`, - `ss:///${BUCKET}/b.txt`, - ]); - expect(result.exitCode).toBe(1); - expect(result.stderr).toContain("Invalid token"); - await clearOverrides(apiUrl); - }); + testBehaviour("exits 1 on 401", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + yield* injectGlobalError(apiUrl, 401, "Invalid token"); + const result = yield* run([ + "storage", + "mv", + ...EXPERIMENTAL_FLAGS, + `ss:///${BUCKET}/a.txt`, + `ss:///${BUCKET}/b.txt`, + ]); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain("Invalid token"); + yield* clearOverrides(apiUrl); + }), + signal, + ), + ); - testBehaviour("exits 1 on 403", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - await injectGlobalError(apiUrl, 403, "Forbidden"); - const result = await run([ - "storage", - "mv", - ...EXPERIMENTAL_FLAGS, - `ss:///${BUCKET}/a.txt`, - `ss:///${BUCKET}/b.txt`, - ]); - expect(result.exitCode).toBe(1); - expect(result.stderr.length).toBeGreaterThan(0); - await clearOverrides(apiUrl); - }); + testBehaviour("exits 1 on 403", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + yield* injectGlobalError(apiUrl, 403, "Forbidden"); + const result = yield* run([ + "storage", + "mv", + ...EXPERIMENTAL_FLAGS, + `ss:///${BUCKET}/a.txt`, + `ss:///${BUCKET}/b.txt`, + ]); + expect(result.exitCode).toBe(1); + expect(result.stderr.length).toBeGreaterThan(0); + yield* clearOverrides(apiUrl); + }), + signal, + ), + ); - testBehaviour("exits 1 on 429", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - await injectGlobalError(apiUrl, 429, "Too Many Requests"); - const result = await run([ - "storage", - "mv", - ...EXPERIMENTAL_FLAGS, - `ss:///${BUCKET}/a.txt`, - `ss:///${BUCKET}/b.txt`, - ]); - expect(result.exitCode).toBe(1); - expect(result.stderr.length).toBeGreaterThan(0); - await clearOverrides(apiUrl); - }); + testBehaviour("exits 1 on 429", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + yield* injectGlobalError(apiUrl, 429, "Too Many Requests"); + const result = yield* run([ + "storage", + "mv", + ...EXPERIMENTAL_FLAGS, + `ss:///${BUCKET}/a.txt`, + `ss:///${BUCKET}/b.txt`, + ]); + expect(result.exitCode).toBe(1); + expect(result.stderr.length).toBeGreaterThan(0); + yield* clearOverrides(apiUrl); + }), + signal, + ), + ); - testBehaviour("exits 1 on 500", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - await injectGlobalError(apiUrl, 500, "Internal Server Error"); - const result = await run([ - "storage", - "mv", - ...EXPERIMENTAL_FLAGS, - `ss:///${BUCKET}/a.txt`, - `ss:///${BUCKET}/b.txt`, - ]); - expect(result.exitCode).toBe(1); - expect(result.stderr.length).toBeGreaterThan(0); - await clearOverrides(apiUrl); - }); + testBehaviour("exits 1 on 500", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + yield* injectGlobalError(apiUrl, 500, "Internal Server Error"); + const result = yield* run([ + "storage", + "mv", + ...EXPERIMENTAL_FLAGS, + `ss:///${BUCKET}/a.txt`, + `ss:///${BUCKET}/b.txt`, + ]); + expect(result.exitCode).toBe(1); + expect(result.stderr.length).toBeGreaterThan(0); + yield* clearOverrides(apiUrl); + }), + signal, + ), + ); }); describe("storage rm", () => { - testBehaviour("removes a file from storage", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - // Upload the file to remove. - await run([ - "storage", - "cp", - ...EXPERIMENTAL_FLAGS, - "upload.txt", - `ss:///${BUCKET}/rm-target.txt`, - ]); - const result = await run([ - "storage", - "rm", - "--yes", - ...EXPERIMENTAL_FLAGS, - `ss:///${BUCKET}/rm-target.txt`, - ]); - expect(result.exitCode).toBe(0); - const requests = await getRequestLog(apiUrl); - const rmReq = requests.find( - (r) => r.method === "DELETE" && r.pathname === `/storage/v1/object/${BUCKET}`, - ); - expect(rmReq).toBeDefined(); - expect(rmReq?.body).toMatchObject({ prefixes: ["rm-target.txt"] }); - }); + testBehaviour("removes a file from storage", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + // Upload the file to remove. + yield* run([ + "storage", + "cp", + ...EXPERIMENTAL_FLAGS, + "upload.txt", + `ss:///${BUCKET}/rm-target.txt`, + ]); + const result = yield* run([ + "storage", + "rm", + "--yes", + ...EXPERIMENTAL_FLAGS, + `ss:///${BUCKET}/rm-target.txt`, + ]); + expect(result.exitCode).toBe(0); + const requests = yield* getRequestLog(apiUrl); + const rmReq = requests.find( + (r) => r.method === "DELETE" && r.pathname === `/storage/v1/object/${BUCKET}`, + ); + expect(rmReq).toBeDefined(); + expect(rmReq?.body).toMatchObject({ prefixes: ["rm-target.txt"] }); + }), + signal, + ), + ); - testBehaviour("removes multiple files", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - writeFileSync(join(workspace.path, "file2.txt"), "second file"); - await run(["storage", "cp", ...EXPERIMENTAL_FLAGS, "upload.txt", `ss:///${BUCKET}/rm-a.txt`]); - await run(["storage", "cp", ...EXPERIMENTAL_FLAGS, "file2.txt", `ss:///${BUCKET}/rm-b.txt`]); - const result = await run([ - "storage", - "rm", - "--yes", - ...EXPERIMENTAL_FLAGS, - `ss:///${BUCKET}/rm-a.txt`, - `ss:///${BUCKET}/rm-b.txt`, - ]); - expect(result.exitCode).toBe(0); - const requests = await getRequestLog(apiUrl); - const rmReq = requests.find( - (r) => r.method === "DELETE" && r.pathname === `/storage/v1/object/${BUCKET}`, - ); - expect(rmReq).toBeDefined(); - expect(rmReq?.body).toMatchObject({ prefixes: ["rm-a.txt", "rm-b.txt"] }); - }); + testBehaviour("removes multiple files", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* setupStorageWorkspace(workspace.path, apiUrl); + yield* fs.writeFileString(path.join(workspace.path, "file2.txt"), "second file"); + yield* run([ + "storage", + "cp", + ...EXPERIMENTAL_FLAGS, + "upload.txt", + `ss:///${BUCKET}/rm-a.txt`, + ]); + yield* run([ + "storage", + "cp", + ...EXPERIMENTAL_FLAGS, + "file2.txt", + `ss:///${BUCKET}/rm-b.txt`, + ]); + const result = yield* run([ + "storage", + "rm", + "--yes", + ...EXPERIMENTAL_FLAGS, + `ss:///${BUCKET}/rm-a.txt`, + `ss:///${BUCKET}/rm-b.txt`, + ]); + expect(result.exitCode).toBe(0); + const requests = yield* getRequestLog(apiUrl); + const rmReq = requests.find( + (r) => r.method === "DELETE" && r.pathname === `/storage/v1/object/${BUCKET}`, + ); + expect(rmReq).toBeDefined(); + expect(rmReq?.body).toMatchObject({ prefixes: ["rm-a.txt", "rm-b.txt"] }); + }), + signal, + ), + ); - testBehaviour("exits 1 on 401", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - await injectGlobalError(apiUrl, 401, "Invalid token"); - const result = await run([ - "storage", - "rm", - "--yes", - ...EXPERIMENTAL_FLAGS, - `ss:///${BUCKET}/file.txt`, - ]); - expect(result.exitCode).toBe(1); - expect(result.stderr).toContain("Invalid token"); - await clearOverrides(apiUrl); - }); + testBehaviour("exits 1 on 401", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + yield* injectGlobalError(apiUrl, 401, "Invalid token"); + const result = yield* run([ + "storage", + "rm", + "--yes", + ...EXPERIMENTAL_FLAGS, + `ss:///${BUCKET}/file.txt`, + ]); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain("Invalid token"); + yield* clearOverrides(apiUrl); + }), + signal, + ), + ); - testBehaviour("exits 1 on 403", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - await injectGlobalError(apiUrl, 403, "Forbidden"); - const result = await run([ - "storage", - "rm", - "--yes", - ...EXPERIMENTAL_FLAGS, - `ss:///${BUCKET}/file.txt`, - ]); - expect(result.exitCode).toBe(1); - expect(result.stderr.length).toBeGreaterThan(0); - await clearOverrides(apiUrl); - }); + testBehaviour("exits 1 on 403", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + yield* injectGlobalError(apiUrl, 403, "Forbidden"); + const result = yield* run([ + "storage", + "rm", + "--yes", + ...EXPERIMENTAL_FLAGS, + `ss:///${BUCKET}/file.txt`, + ]); + expect(result.exitCode).toBe(1); + expect(result.stderr.length).toBeGreaterThan(0); + yield* clearOverrides(apiUrl); + }), + signal, + ), + ); - testBehaviour("exits 1 on 429", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - await injectGlobalError(apiUrl, 429, "Too Many Requests"); - const result = await run([ - "storage", - "rm", - "--yes", - ...EXPERIMENTAL_FLAGS, - `ss:///${BUCKET}/file.txt`, - ]); - expect(result.exitCode).toBe(1); - expect(result.stderr.length).toBeGreaterThan(0); - await clearOverrides(apiUrl); - }); + testBehaviour("exits 1 on 429", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + yield* injectGlobalError(apiUrl, 429, "Too Many Requests"); + const result = yield* run([ + "storage", + "rm", + "--yes", + ...EXPERIMENTAL_FLAGS, + `ss:///${BUCKET}/file.txt`, + ]); + expect(result.exitCode).toBe(1); + expect(result.stderr.length).toBeGreaterThan(0); + yield* clearOverrides(apiUrl); + }), + signal, + ), + ); - testBehaviour("exits 1 on 500", async ({ workspace, run, apiUrl }) => { - setupStorageWorkspace(workspace.path, apiUrl); - await injectGlobalError(apiUrl, 500, "Internal Server Error"); - const result = await run([ - "storage", - "rm", - "--yes", - ...EXPERIMENTAL_FLAGS, - `ss:///${BUCKET}/file.txt`, - ]); - expect(result.exitCode).toBe(1); - expect(result.stderr.length).toBeGreaterThan(0); - await clearOverrides(apiUrl); - }); + testBehaviour("exits 1 on 500", ({ workspace, run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* setupStorageWorkspace(workspace.path, apiUrl); + yield* injectGlobalError(apiUrl, 500, "Internal Server Error"); + const result = yield* run([ + "storage", + "rm", + "--yes", + ...EXPERIMENTAL_FLAGS, + `ss:///${BUCKET}/file.txt`, + ]); + expect(result.exitCode).toBe(1); + expect(result.stderr.length).toBeGreaterThan(0); + yield* clearOverrides(apiUrl); + }), + signal, + ), + ); }); diff --git a/apps/cli-e2e/src/tests/target-selection.e2e.test.ts b/apps/cli-e2e/src/tests/target-selection.e2e.test.ts index 183a5f691d..c43305a73b 100644 --- a/apps/cli-e2e/src/tests/target-selection.e2e.test.ts +++ b/apps/cli-e2e/src/tests/target-selection.e2e.test.ts @@ -1,29 +1,46 @@ +import { Effect } from "effect"; import { describe, expect } from "vitest"; import { linkProject } from "./link-project.ts"; import { getRequestLog } from "./request-log.ts"; -import { testBehaviour } from "./test-context.ts"; +import { runE2e, testBehaviour } from "./test-context.ts"; const DEAD_DB_URL = "postgresql://postgres:postgres@localhost:1/postgres"; describe("remote target selection", () => { testBehaviour( "reads a space-separated --remote as --remote=", - async ({ run, projectRef, workspace, apiUrl }) => { - linkProject(workspace.path, projectRef); - const result = await run(["db", "advisors", "--type", "security", "--remote", projectRef]); - expect(result.exitCode).toBe(0); - const requests = await getRequestLog(apiUrl); - expect(requests.map((r) => `${r.method} ${r.pathname}`)).toContain( - `GET /v1/projects/${projectRef}/advisors/security`, - ); - }, + ({ run, projectRef, workspace, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + yield* linkProject(workspace.path, projectRef); + const result = yield* run([ + "db", + "advisors", + "--type", + "security", + "--remote", + projectRef, + ]); + expect(result.exitCode).toBe(0); + const requests = yield* getRequestLog(apiUrl); + expect(requests.map((r) => `${r.method} ${r.pathname}`)).toContain( + `GET /v1/projects/${projectRef}/advisors/security`, + ); + }), + signal, + ), ); - testBehaviour("rejects --remote= on a hosted command", async ({ run, apiUrl }) => { - const result = await run(["secrets", "list", `--remote=${DEAD_DB_URL}`]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("--remote takes a project ref or a branch name."); - const requests = await getRequestLog(apiUrl); - expect(requests.filter((r) => r.pathname.endsWith("/secrets"))).toEqual([]); - }); + testBehaviour("rejects --remote= on a hosted command", ({ run, apiUrl, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["secrets", "list", `--remote=${DEAD_DB_URL}`]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("--remote takes a project ref or a branch name."); + const requests = yield* getRequestLog(apiUrl); + expect(requests.filter((r) => r.pathname.endsWith("/secrets"))).toEqual([]); + }), + signal, + ), + ); }); diff --git a/apps/cli-e2e/src/tests/telemetry.e2e.test.ts b/apps/cli-e2e/src/tests/telemetry.e2e.test.ts index 9d6bc4e942..6470256ac2 100644 --- a/apps/cli-e2e/src/tests/telemetry.e2e.test.ts +++ b/apps/cli-e2e/src/tests/telemetry.e2e.test.ts @@ -1,63 +1,105 @@ -import { chmodSync, readFileSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { Effect, FileSystem, Path } from "effect"; import { describe, expect } from "vitest"; -import { testBehaviour } from "./test-context.ts"; +import { runE2e, testBehaviour } from "./test-context.ts"; describe("telemetry", () => { describe("telemetry:enable", () => { - testBehaviour("enables telemetry", async ({ run }) => { - const result = await run(["telemetry", "enable"]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Telemetry is enabled."); - }); + testBehaviour("enables telemetry", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["telemetry", "enable"]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Telemetry is enabled."); + }), + signal, + ), + ); - testBehaviour("exits non-zero on unwritable config dir", async ({ run, workspace }) => { - chmodSync(workspace.path, 0o555); - try { - const result = await run(["telemetry", "enable"]); - expect(result.exitCode).not.toBe(0); - } finally { - chmodSync(workspace.path, 0o755); - } - }); + testBehaviour("exits non-zero on unwritable config dir", ({ run, workspace, signal }) => + runE2e( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + yield* Effect.acquireUseRelease( + fs.chmod(workspace.path, 0o555), + () => + Effect.gen(function* () { + const result = yield* run(["telemetry", "enable"]); + expect(result.exitCode).not.toBe(0); + }), + () => fs.chmod(workspace.path, 0o755), + ); + }), + signal, + ), + ); }); describe("telemetry:disable", () => { - testBehaviour("disables telemetry", async ({ run }) => { - const result = await run(["telemetry", "disable"]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Telemetry is disabled."); - }); + testBehaviour("disables telemetry", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["telemetry", "disable"]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Telemetry is disabled."); + }), + signal, + ), + ); }); describe("telemetry:status", () => { - testBehaviour("shows current telemetry state", async ({ run }) => { - const result = await run(["telemetry", "status"]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toMatch(/Telemetry is (enabled|disabled)\./); - }); + testBehaviour("shows current telemetry state", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + const result = yield* run(["telemetry", "status"]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/Telemetry is (enabled|disabled)\./); + }), + signal, + ), + ); - testBehaviour("round-trip: enable then status shows enabled", async ({ run }) => { - await run(["telemetry", "enable"]); - const result = await run(["telemetry", "status"]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Telemetry is enabled."); - }); + testBehaviour("round-trip: enable then status shows enabled", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + yield* run(["telemetry", "enable"]); + const result = yield* run(["telemetry", "status"]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Telemetry is enabled."); + }), + signal, + ), + ); - testBehaviour("round-trip: disable then status shows disabled", async ({ run }) => { - await run(["telemetry", "disable"]); - const result = await run(["telemetry", "status"]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Telemetry is disabled."); - }); + testBehaviour("round-trip: disable then status shows disabled", ({ run, signal }) => + runE2e( + Effect.gen(function* () { + yield* run(["telemetry", "disable"]); + const result = yield* run(["telemetry", "status"]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Telemetry is disabled."); + }), + signal, + ), + ); - testBehaviour("handles corrupted config gracefully", async ({ run, workspace }) => { - const telemetryPath = join(workspace.path, "telemetry.json"); - writeFileSync(telemetryPath, "{{not valid json}}"); - const result = await run(["telemetry", "status"]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("Telemetry is enabled."); - expect(() => JSON.parse(readFileSync(telemetryPath, "utf8"))).not.toThrow(); - }); + testBehaviour("handles corrupted config gracefully", ({ run, workspace, signal }) => + runE2e( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const telemetryPath = path.join(workspace.path, "telemetry.json"); + yield* fs.writeFileString(telemetryPath, "{{not valid json}}"); + const result = yield* run(["telemetry", "status"]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("Telemetry is enabled."); + const telemetry = new TextDecoder("utf-8", { ignoreBOM: true }).decode( + yield* fs.readFile(telemetryPath), + ); + expect(() => JSON.parse(telemetry)).not.toThrow(); + }), + signal, + ), + ); }); }); diff --git a/apps/cli-e2e/src/tests/test-context.ts b/apps/cli-e2e/src/tests/test-context.ts index 4a856f97d6..92e22b1d64 100644 --- a/apps/cli-e2e/src/tests/test-context.ts +++ b/apps/cli-e2e/src/tests/test-context.ts @@ -1,8 +1,12 @@ +import { BunFileSystem, BunPath } from "@effect/platform-bun"; +import { Data, Effect, Formatter, Layer, Schema } from "effect"; +import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http"; import { inject, test } from "vitest"; import { createHarness, exec, makeTempDir, + type CLIHarness, type CLIResult, type TempDir, } from "@supabase/cli-test-helpers"; @@ -22,12 +26,79 @@ function scenarioSlug(task: { name: string; suite?: { name: string } | null }): type ExecOptions = NonNullable[2]>; +/** The CLI subprocess could not be spawned or awaited. */ +export class CliExecError extends Data.TaggedError("CliExecError")<{ + readonly message: string; + readonly cause: unknown; +}> {} + +/** The replay server refused to load the test's scenario. */ +class ScenarioLoadError extends Data.Error<{ readonly message: string }> {} + +/** CLI stdout did not decode as the expected JSON. */ +class JsonOutputError extends Data.Error<{ readonly message: string }> {} + +/** A test body passed without executing its CLI runner. */ +class RunNotExecutedError extends Data.Error<{ readonly message: string }> {} + +/** Runs the compiled CLI against a harness target. */ +export const execCli = (harness: CLIHarness, cmd: string[], execOpts?: ExecOptions) => + Effect.tryPromise({ + try: () => exec(harness, cmd, execOpts), + catch: (cause) => + new CliExecError({ message: cause instanceof Error ? cause.message : String(cause), cause }), + }); + +const sendControl = (url: string, body: unknown) => + HttpClientRequest.post(url).pipe( + HttpClientRequest.bodyJson(body), + Effect.flatMap(HttpClient.execute), + ); + +/** POSTs a JSON body to a replay server control endpoint; a non-2xx answer fails. */ +export const postControl = (url: string, body: unknown) => + sendControl(url, body).pipe(Effect.flatMap(HttpClientResponse.filterStatusOk)); + +const httpLayer = Layer.mergeAll( + FetchHttpClient.layer, + Layer.succeed(HttpClient.TracerPropagationEnabled, false), +); + +const e2eLayer = Layer.mergeAll(BunFileSystem.layer, BunPath.layer, httpLayer); + +/** Runs a test body at the Vitest edge with the file system, path, and HTTP services. */ +export const runE2e = ( + body: Effect.Effect>, + signal?: AbortSignal, +): Promise => Effect.runPromise(body.pipe(Effect.provide(e2eLayer)), { signal }); + +/** Decodes CLI stdout as JSON; a failure keeps the schema message plus a bounded stdout excerpt. */ +export const decodeJsonOutput = + (schema: S) => + (stdout: string) => + Schema.decodeEffect(Schema.fromJsonString(schema))(stdout).pipe( + Effect.mapError( + (error) => + new JsonOutputError({ + message: `${error.message}\n stdout: ${Formatter.format(stdout.slice(0, 200))}`, + }), + ), + ); + +const ScenarioFailure = Schema.Struct({ message: Schema.String }); + +/** Marks a CLI run as executed for the current test. */ +interface RunGuard { + readonly track: (effect: Effect.Effect) => Effect.Effect; +} + interface BehaviourFixtures { + runGuard: RunGuard; projectRef: string; orgId: string; workspace: TempDir; - run: (cmd: string[], execOpts?: ExecOptions) => Promise; - runNoProjectId: (cmd: string[]) => Promise; + run: (cmd: string[], execOpts?: ExecOptions) => Effect.Effect; + runNoProjectId: (cmd: string[]) => Effect.Effect; apiUrl: string; storageBucket: string; pgMockPort: number; @@ -47,42 +118,63 @@ interface BehaviourFixtures { * server knows which ordered interaction sequence to serve. Auto-clears the * request log, error overrides, and active scenario after every test. */ export const testBehaviour = test.extend({ - projectRef: async ({ task: _task }, use) => { - await use(inject("projectRef") as string); + /* Fails a test that passed without executing a CLI run, e.g. because its body returned the + Effect without `runE2e`. Checked once the test finished, after every fixture cleaned up. */ + runGuard: ({ task, onTestFinished }, use) => { + let executed = false; + onTestFinished(() => { + if (!executed && task.result?.state === "pass") { + return Effect.runPromise( + new RunNotExecutedError({ + message: `"${task.name}" passed without executing its run Effect; return runE2e(Effect.gen(...), signal) from the test body`, + }), + ); + } + }); + return use({ + track: (effect) => + Effect.suspend(() => { + executed = true; + return effect; + }), + }); }, - orgId: async ({ task: _task }, use) => { - await use(inject("orgId") as string); - }, + projectRef: ({ task: _task }, use) => use(inject("projectRef")), + + orgId: ({ task: _task }, use) => use(inject("orgId")), - workspace: async ({ task }, use) => { + workspace: ({ task }, use) => { const serverUrl = inject("replayServerUrl"); const slug = scenarioSlug(task); - // Truncate to 40 chars to keep temp dir names manageable. - const dir = await makeTempDir(`cli-e2e-${slug.slice(0, 40)}-`); + return Effect.runPromise( + Effect.gen(function* () { + // Truncate to 40 chars to keep temp dir names manageable. + const dir = yield* Effect.promise(() => makeTempDir(`cli-e2e-${slug.slice(0, 40)}-`)); - const res = await fetch(`${serverUrl}/_ctrl/scenario`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name: slug }), - }); - if (!res.ok) { - await dir[Symbol.asyncDispose](); - const { message } = (await res.json()) as { message: string }; - throw new Error(message); - } - - await use(dir); - - await dir[Symbol.asyncDispose](); - await Promise.all([ - fetch(`${serverUrl}/_ctrl/requests`, { method: "DELETE" }), - fetch(`${serverUrl}/_ctrl/overrides`, { method: "DELETE" }), - fetch(`${serverUrl}/_ctrl/scenario`, { method: "DELETE" }), - ]); + const res = yield* sendControl(`${serverUrl}/_ctrl/scenario`, { name: slug }); + if (res.status < 200 || res.status >= 300) { + yield* Effect.promise(() => dir[Symbol.asyncDispose]()); + const { message } = yield* HttpClientResponse.schemaBodyJson(ScenarioFailure)(res); + return yield* new ScenarioLoadError({ message }); + } + + yield* Effect.promise(() => use(dir)); + + yield* Effect.promise(() => dir[Symbol.asyncDispose]()); + yield* Effect.all( + [ + HttpClient.del(`${serverUrl}/_ctrl/requests`), + HttpClient.del(`${serverUrl}/_ctrl/overrides`), + HttpClient.del(`${serverUrl}/_ctrl/scenario`), + ], + { concurrency: "unbounded", discard: true }, + ); + }).pipe(Effect.provide(httpLayer)), + ); }, - run: async ({ workspace }, use) => { + run: ({ runGuard, workspace }, use) => { const serverUrl = inject("replayServerUrl"); const harness = createHarness({ apiUrl: serverUrl, @@ -90,28 +182,22 @@ export const testBehaviour = test.extend({ cwd: workspace.path, projectId: inject("projectRef") as string, }); - await use((cmd, execOpts) => exec(harness, cmd, execOpts)); + return use((cmd, execOpts) => runGuard.track(execCli(harness, cmd, execOpts))); }, - runNoProjectId: async ({ workspace }, use) => { + runNoProjectId: ({ runGuard, workspace }, use) => { const serverUrl = inject("replayServerUrl"); const harness = createHarness({ apiUrl: serverUrl, accessToken: ACCESS_TOKEN, cwd: workspace.path, }); - await use((cmd) => exec(harness, cmd)); + return use((cmd) => runGuard.track(execCli(harness, cmd))); }, - storageBucket: async ({ task: _task }, use) => { - await use(inject("storageBucket") as string); - }, + storageBucket: ({ task: _task }, use) => use(inject("storageBucket")), - apiUrl: async ({ task: _task }, use) => { - await use(inject("replayServerUrl")); - }, + apiUrl: ({ task: _task }, use) => use(inject("replayServerUrl")), - pgMockPort: async ({ task: _task }, use) => { - await use(inject("pgMockPort") as number); - }, + pgMockPort: ({ task: _task }, use) => use(inject("pgMockPort")), });