-
Notifications
You must be signed in to change notification settings - Fork 496
Expand file tree
/
Copy pathstack.e2e.test.ts
More file actions
182 lines (160 loc) · 7.07 KB
/
Copy pathstack.e2e.test.ts
File metadata and controls
182 lines (160 loc) · 7.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, inject, test } from "vitest";
import { createHarness, exec, runParity, type CLIResult } from "@supabase/cli-test-helpers";
import { testBehaviour, testParity } from "./test-context.ts";
import { ACCESS_TOKEN, isRecording, TARGET } from "./env.ts";
// A guaranteed-unreachable TCP address — connection is refused immediately.
// Used to simulate Docker being unavailable without relying on any external state.
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');
}
// Extends testBehaviour with a `stackRun` fixture that automatically passes
// DOCKER_HOST to the CLI subprocess, pointing it at the relay server.
// In record mode the relay forwards Docker SDK calls to the real Docker socket;
// in replay mode it serves pre-recorded Docker API fixtures.
// 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<CLIResult>;
}
const testStack = testBehaviour.extend<StackFixtures>({
stackRun: async ({ workspace }, use) => {
const serverUrl = inject("replayServerUrl") as string;
const dockerHostUrl = inject("dockerHostUrl") as string;
const harness = createHarness(TARGET, {
apiUrl: serverUrl,
accessToken: ACCESS_TOKEN,
cwd: workspace.path,
});
await use((cmd, opts) =>
exec(harness, cmd, { env: { DOCKER_HOST: opts?.dockerHost ?? dockerHostUrl } }),
);
},
});
function testParityStack(cmd: string[], opts?: { workspaceSetup?: (dir: string) => void }): void {
const label = `parity: ${cmd.join(" ")}`;
test.skipIf(isRecording)(label, async () => {
const serverUrl = inject("replayServerUrl") as string;
const dockerHostUrl = inject("dockerHostUrl") as string;
await runParity(
{
apiUrl: serverUrl,
accessToken: ACCESS_TOKEN,
workspaceSetup: opts?.workspaceSetup,
extraEnv: { DOCKER_HOST: dockerHostUrl },
},
cmd,
);
});
}
// ---------------------------------------------------------------------------
// services
// ---------------------------------------------------------------------------
// `services` prints a baked-in Go-parity 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");
});
testParity(["services"], { normalizeVersions: false });
});
// ---------------------------------------------------------------------------
// status
// ---------------------------------------------------------------------------
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);
});
testParityStack(["status"], { workspaceSetup: setupStackWorkspace });
});
// ---------------------------------------------------------------------------
// stop
// ---------------------------------------------------------------------------
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);
});
// cobra's MarkFlagsMutuallyExclusive validates this before the command runs —
// 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);
},
);
testParityStack(["stop"], { workspaceSetup: setupStackWorkspace });
});
// ---------------------------------------------------------------------------
// start
// ---------------------------------------------------------------------------
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);
},
);
// start → status → status --override-name → stop lifecycle test.
// These must run in sequence in a single shared workspace so that status
// and stop see the stack that start brought up.
// TODO: record these in an environment where the full Supabase Docker stack starts
// cleanly through the TCP relay proxy (vector health check fails on this machine).
test.todo("start → status → stop lifecycle");
test.todo("starts with --exclude studio and stops cleanly");
test.todo("parity: start");
});
// ---------------------------------------------------------------------------
// seed buckets
// ---------------------------------------------------------------------------
// `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 fetch(`${apiUrl}/_ctrl/requests`).then(
(r) => r.json() as Promise<Array<{ method: string; pathname: string }>>,
);
expect(requests.some((r) => r.method === "POST" && r.pathname === "/storage/v1/bucket")).toBe(
true,
);
});
testParity(["seed", "buckets"], { workspaceSetup: setupStackWorkspace });
});