Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 46 additions & 45 deletions apps/cli/src/legacy/commands/db/pull/pull.live.test.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,69 @@
import { mkdir, readdir, unlink, writeFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";
import { mkdir, readdir, readFile, unlink } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";

import { requireLiveSuccess, test, throwWithCleanup } from "../../../../../tests/helpers/live.ts";

test("pulls the remote schema after a local migration is applied", async ({
cli,
project,
workspace,
}) => {
const version = `${Date.now()}${Math.floor(Math.random() * 10_000)
.toString()
.padStart(4, "0")}`;
// `db pull` exits non-zero when the diff comes back empty (the in-sync
// finding, see IN_SYNC_SUGGESTION in pull.handler.ts), so the journey seeds a remote-only
// marker table through `db query` — no local migration and no history row.
// The marker cannot exist in the freshly provisioned shadow, so the diff is
// never empty regardless of engine and the pull deterministically writes it.
test("pulls the remote schema into an initial migration", async ({ cli, project, workspace }) => {
const marker = `e2e_pull_${randomUUID().slice(0, 8)}`;
const migrations = join(workspace.path, "supabase", "migrations");
await mkdir(migrations, { recursive: true });
const existingMigrations = new Set(await readdir(migrations));
const migrationFile = join(migrations, `${version}_e2e_pull.sql`);
await writeFile(migrationFile, `create table if not exists e2e_pull_${version} (id int);\n`);

let targetError: unknown;
const cleanupErrors: Array<unknown> = [];
try {
const pushed = await cli(["db", "push", "--db-url", project.dbUrl, "--yes"]);
requireLiveSuccess(pushed, "db push setup");
const seeded = await cli([
"db",
"query",
`create table if not exists ${marker} (id int)`,
"--db-url",
project.dbUrl,
]);
requireLiveSuccess(seeded, "db query setup for db pull");

const result = await cli(["db", "pull", "--db-url", project.dbUrl, "--yes"]);
expect(result.exitCode, result.stderr).toBe(0);
expect(`${result.stdout}${result.stderr}`).not.toMatch(
/dial|no route|connection refused|could not connect|server closed the connection|i\/o timeout/i,

expect(result.stderr, result.stderr).toContain("Schema written to");
const generated = (await readdir(migrations)).filter((file) => !existingMigrations.has(file));
expect(generated.length, result.stderr).toBeGreaterThan(0);
const pulled = await Promise.all(
generated.map((file) => readFile(join(migrations, file), "utf8")),
);
expect(pulled.join("\n"), result.stderr).toContain(marker);
} catch (error) {
targetError = error;
}

const cleanupErrors: Array<unknown> = [];
// Remove all migrations created by this test before resetting. This
// includes both the seed migration and the migration generated by
// `db pull`; resetting with only the generated grant statements left
// behind can reference a table that no longer exists.
let currentMigrations: ReadonlyArray<string> = [];
try {
currentMigrations = await readdir(migrations);
} catch (error) {
cleanupErrors.push(error);
}
for (const file of currentMigrations.filter((candidate) => !existingMigrations.has(candidate))) {
} finally {
// Remove the generated migration before resetting so the reset replays an
// empty local set and restores the baseline schema, dropping the marker.
let currentMigrations: ReadonlyArray<string> = [];
try {
await unlink(join(migrations, file));
currentMigrations = await readdir(migrations);
} catch (error) {
cleanupErrors.push(
new Error(
`db pull cleanup could not remove test migration ${join(migrations, file)}: ${
error instanceof Error ? error.message : String(error)
}`,
),
);
cleanupErrors.push(error);
}
for (const file of currentMigrations.filter(
(candidate) => !existingMigrations.has(candidate),
)) {
try {
await unlink(join(migrations, file));
} catch (error) {
cleanupErrors.push(error);
}
}
try {
const reset = await cli(["db", "reset", "--db-url", project.dbUrl, "--yes"]);
requireLiveSuccess(reset, "db reset cleanup after db pull");
} catch (error) {
cleanupErrors.push(error);
}
}

try {
const reset = await cli(["db", "reset", "--db-url", project.dbUrl, "--yes"]);
requireLiveSuccess(reset, "db reset cleanup after db pull");
} catch (error) {
cleanupErrors.push(error);
}

throwWithCleanup(targetError, cleanupErrors);
});
23 changes: 23 additions & 0 deletions apps/cli/src/legacy/commands/services/services.live.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { expect } from "vitest";

import { requireLiveSuccess, test } from "../../../../tests/helpers/live.ts";

test("merges remote versions from the linked live project into services output", async ({
cli,
project,
}) => {
const linked = await cli(["link", "--project-ref", project.ref, "--skip-pooler"]);
requireLiveSuccess(linked, "link setup for services");

// One remote-backed invocation is the live golden path; cross-format
// rendering is integration-tested with fixed remote data.
const json = await cli(["services", "-o", "json"]);
expect(json.exitCode, json.stderr).toBe(0);
const rows = JSON.parse(json.stdout) as Array<{ name: string; local: string; remote: string }>;
expect(rows, json.stdout).toHaveLength(10);
const postgres = rows.find((row) => row.name === "supabase/postgres");
if (postgres === undefined) {
throw new Error(`supabase/postgres row missing from services json:\n${json.stdout}`);
}
expect(postgres.remote.length, json.stdout).toBeGreaterThan(0);
});
27 changes: 9 additions & 18 deletions apps/cli/src/legacy/commands/storage/cp/cp.live.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,13 @@ import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";

import { requireLiveSuccess, test, throwWithCleanup } from "../../../../../tests/helpers/live.ts";

const STORAGE_FLAGS = ["--linked", "--experimental"];

async function removeObject(
cli: (args: string[]) => Promise<{ exitCode: number; stdout: string; stderr: string }>,
remote: string,
): Promise<void> {
const removed = await cli(["storage", "rm", remote, "--yes", ...STORAGE_FLAGS]);
if (
removed.exitCode !== 0 &&
!/not found|does not exist/i.test(`${removed.stdout}\n${removed.stderr}`)
) {
throw new Error(`storage rm cleanup failed:\n${removed.stdout}\n${removed.stderr}`);
}
}
import {
removeStorageLiveObject,
requireLiveSuccess,
storageLiveFlags,
test,
throwWithCleanup,
} from "../../../../../tests/helpers/live.ts";

test("copies a local file to the remote bucket", async ({ cli, project, workspace }) => {
const suffix = randomUUID().slice(0, 8);
Expand All @@ -34,13 +25,13 @@ test("copies a local file to the remote bucket", async ({ cli, project, workspac
});
requireLiveSuccess(linked, "link setup for storage cp");

const result = await cli(["storage", "cp", local, remote, ...STORAGE_FLAGS]);
const result = await cli(["storage", "cp", local, remote, ...storageLiveFlags]);
expect(result.exitCode, result.stderr).toBe(0);
} catch (error) {
targetError = error;
} finally {
try {
await removeObject(cli, remote);
await removeStorageLiveObject(cli, remote);
} catch (error) {
cleanupError = error;
}
Expand Down
29 changes: 10 additions & 19 deletions apps/cli/src/legacy/commands/storage/ls/ls.live.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,13 @@ import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";

import { requireLiveSuccess, test, throwWithCleanup } from "../../../../../tests/helpers/live.ts";

const STORAGE_FLAGS = ["--linked", "--experimental"];

async function removeObject(
cli: (args: string[]) => Promise<{ exitCode: number; stdout: string; stderr: string }>,
remote: string,
): Promise<void> {
const removed = await cli(["storage", "rm", remote, "--yes", ...STORAGE_FLAGS]);
if (
removed.exitCode !== 0 &&
!/not found|does not exist/i.test(`${removed.stdout}\n${removed.stderr}`)
) {
throw new Error(`storage rm cleanup failed:\n${removed.stdout}\n${removed.stderr}`);
}
}
import {
removeStorageLiveObject,
requireLiveSuccess,
storageLiveFlags,
test,
throwWithCleanup,
} from "../../../../../tests/helpers/live.ts";

test("lists an uploaded object", async ({ cli, project, workspace }) => {
const suffix = randomUUID().slice(0, 8);
Expand All @@ -33,22 +24,22 @@ test("lists an uploaded object", async ({ cli, project, workspace }) => {
env: { SUPABASE_DB_PASSWORD: project.dbPassword },
});
requireLiveSuccess(linked, "link setup for storage ls");
const uploaded = await cli(["storage", "cp", local, remote, ...STORAGE_FLAGS]);
const uploaded = await cli(["storage", "cp", local, remote, ...storageLiveFlags]);
requireLiveSuccess(uploaded, "storage cp setup for storage ls");

const result = await cli([
"storage",
"ls",
`ss:///${project.storageBucket}/`,
...STORAGE_FLAGS,
...storageLiveFlags,
]);
expect(result.exitCode, result.stderr).toBe(0);
expect(result.stdout).toContain(`upload-${suffix}.txt`);
} catch (error) {
targetError = error;
} finally {
try {
await removeObject(cli, remote);
await removeStorageLiveObject(cli, remote);
} catch (error) {
cleanupError = error;
}
Expand Down
56 changes: 56 additions & 0 deletions apps/cli/src/legacy/commands/storage/mv/mv.live.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";

import {
removeStorageLiveObject,
requireLiveSuccess,
storageLiveFlags,
test,
throwWithCleanup,
} from "../../../../../tests/helpers/live.ts";

test("moves an uploaded object to a new path", async ({ cli, project, workspace }) => {
const suffix = randomUUID().slice(0, 8);
const local = join(workspace.path, `mv-src-${suffix}.txt`);
const source = `ss:///${project.storageBucket}/mv-src-${suffix}.txt`;
const destination = `ss:///${project.storageBucket}/mv-dst-${suffix}.txt`;
await writeFile(local, "live-e2e storage payload\n");

let targetError: unknown;
const cleanupErrors: Array<unknown> = [];
try {
const linked = await cli(["link", "--project-ref", project.ref], {
env: { SUPABASE_DB_PASSWORD: project.dbPassword },
});
requireLiveSuccess(linked, "link setup for storage mv");
const uploaded = await cli(["storage", "cp", local, source, ...storageLiveFlags]);
requireLiveSuccess(uploaded, "storage cp setup for storage mv");

const moved = await cli(["storage", "mv", source, destination, ...storageLiveFlags]);
expect(moved.exitCode, moved.stderr).toBe(0);
expect(moved.stderr, moved.stderr).toContain("Moving object:");

const listed = await cli([
"storage",
"ls",
`ss:///${project.storageBucket}/`,
...storageLiveFlags,
]);
requireLiveSuccess(listed, "storage ls proof for storage mv");
expect(listed.stdout).toContain(`mv-dst-${suffix}.txt`);
expect(listed.stdout).not.toContain(`mv-src-${suffix}.txt`);
} catch (error) {
targetError = error;
} finally {
for (const remote of [destination, source]) {
try {
await removeStorageLiveObject(cli, remote);
} catch (error) {
cleanupErrors.push(error);
}
}
}
throwWithCleanup(targetError, cleanupErrors);
});
29 changes: 10 additions & 19 deletions apps/cli/src/legacy/commands/storage/rm/rm.live.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,13 @@ import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";

import { requireLiveSuccess, test, throwWithCleanup } from "../../../../../tests/helpers/live.ts";

const STORAGE_FLAGS = ["--linked", "--experimental"];

async function removeObject(
cli: (args: string[]) => Promise<{ exitCode: number; stdout: string; stderr: string }>,
remote: string,
): Promise<void> {
const removed = await cli(["storage", "rm", remote, "--yes", ...STORAGE_FLAGS]);
if (
removed.exitCode !== 0 &&
!/not found|does not exist/i.test(`${removed.stdout}\n${removed.stderr}`)
) {
throw new Error(`storage rm cleanup failed:\n${removed.stdout}\n${removed.stderr}`);
}
}
import {
removeStorageLiveObject,
requireLiveSuccess,
storageLiveFlags,
test,
throwWithCleanup,
} from "../../../../../tests/helpers/live.ts";

test("removes an uploaded object", async ({ cli, project, workspace }) => {
const suffix = randomUUID().slice(0, 8);
Expand All @@ -33,16 +24,16 @@ test("removes an uploaded object", async ({ cli, project, workspace }) => {
env: { SUPABASE_DB_PASSWORD: project.dbPassword },
});
requireLiveSuccess(linked, "link setup for storage rm");
const uploaded = await cli(["storage", "cp", local, remote, ...STORAGE_FLAGS]);
const uploaded = await cli(["storage", "cp", local, remote, ...storageLiveFlags]);
requireLiveSuccess(uploaded, "storage cp setup for storage rm");

const result = await cli(["storage", "rm", remote, "--yes", ...STORAGE_FLAGS]);
const result = await cli(["storage", "rm", remote, "--yes", ...storageLiveFlags]);
expect(result.exitCode, result.stderr).toBe(0);
} catch (error) {
targetError = error;
} finally {
try {
await removeObject(cli, remote);
await removeStorageLiveObject(cli, remote);
} catch (error) {
cleanupError = error;
}
Expand Down
22 changes: 22 additions & 0 deletions apps/cli/tests/helpers/live.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,28 @@ export function requireLiveSuccess(
}
}

/** Flags every storage live test passes: the suite links the shared project
* and the storage command family is experimental-gated. */
export const storageLiveFlags: ReadonlyArray<string> = ["--linked", "--experimental"];

/**
* Best-effort exact-object cleanup for storage live tests: removes one owned
* remote object, tolerating an already-removed target so teardown stays
* idempotent across the moved/renamed paths a test may leave behind.
*/
export async function removeStorageLiveObject(
cli: (args: string[]) => Promise<{ exitCode: number; stdout: string; stderr: string }>,
remote: string,
): Promise<void> {
const removed = await cli(["storage", "rm", remote, "--yes", ...storageLiveFlags]);
if (
removed.exitCode !== 0 &&
!/not found|does not exist/i.test(`${removed.stdout}\n${removed.stderr}`)
) {
throw new Error(`storage rm cleanup failed:\n${removed.stdout}\n${removed.stderr}`);
}
}

/** Rethrow a target failure without discarding failures from exact cleanup. */
export function throwWithCleanup(primary: unknown, cleanup: ReadonlyArray<unknown>): void {
if (primary !== undefined) {
Expand Down
Loading