Skip to content

Commit 6840b8a

Browse files
authored
test(cli): use new api keys in live e2e (#7459)
## TL;DR fixes live e2e setup on projects with legacy api keys disabled by picking the publishable and secret keys the same way the cli does ## ref - spotted in the last 9 runs: https://github.com/supabase/cli/actions/workflows/live-e2e.yml - fixed now (tested): https://github.com/supabase/cli/actions/runs/41609496013
1 parent cf9c0e9 commit 6840b8a

2 files changed

Lines changed: 114 additions & 12 deletions

File tree

‎apps/cli/tests/helpers/live-project.integration.test.ts‎

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as HttpClientResponse from "effect/http/HttpClientResponse";
77
import { afterEach, describe, expect, it, vi } from "vitest";
88

99
import { cliConfigProviderLayer } from "../../src/shared/config/cli-config-provider.layer.ts";
10-
import { provisionLiveEnvironment } from "./live-project.ts";
10+
import { cleanupLiveEnvironment, provisionLiveEnvironment } from "./live-project.ts";
1111

1212
const ref = "abcdefghijklmnopqrst";
1313
function jsonResponse(
@@ -137,4 +137,111 @@ describe("live project provisioning", () => {
137137
),
138138
);
139139
});
140+
141+
it("provisions with the new API keys when the project's legacy keys are disabled", () => {
142+
vi.stubEnv("SUPABASE_LIVE_API_URL", "https://api.supabase.green");
143+
vi.stubEnv("SUPABASE_ACCESS_TOKEN", "test-token");
144+
vi.stubEnv("SUPABASE_LIVE_ORG_ID", "org-slug");
145+
vi.stubEnv("SUPABASE_LIVE_REGION", "us-east-1");
146+
vi.stubEnv("SUPABASE_LIVE_KEEP_PROJECT", "0");
147+
148+
const bucketApiKeys: Array<string | undefined> = [];
149+
const ownedProjects = new Set<string>();
150+
const platformApi = Layer.succeed(
151+
HttpClient.HttpClient,
152+
HttpClient.make((request) => {
153+
const url = new URL(request.url);
154+
if (request.method === "GET" && url.pathname === "/v1/organizations") {
155+
return Effect.succeed(
156+
jsonResponse(request, 200, [{ id: "org-id", slug: "org-slug", name: "Test" }]),
157+
);
158+
}
159+
if (request.method === "POST" && url.pathname === "/v1/projects") {
160+
ownedProjects.add(ref);
161+
return Effect.succeed(jsonResponse(request, 201, { ...project, database: undefined }));
162+
}
163+
if (request.method === "GET" && url.pathname === `/v1/projects/${ref}`) {
164+
return Effect.succeed(jsonResponse(request, 200, project));
165+
}
166+
if (request.method === "GET" && url.pathname === `/v1/projects/${ref}/api-keys`) {
167+
return Effect.succeed(
168+
jsonResponse(request, 200, [
169+
{ name: "anon", type: "legacy", api_key: "legacy-anon-jwt" },
170+
{ name: "service_role", type: "legacy", api_key: "legacy-service-role-jwt" },
171+
{ name: "default", type: "publishable", api_key: "sb_publishable_live" },
172+
{
173+
name: "default",
174+
type: "secret",
175+
api_key: "sb_secret_live",
176+
secret_jwt_template: { role: "service_role" },
177+
},
178+
]),
179+
);
180+
}
181+
if (
182+
request.method === "GET" &&
183+
url.pathname === `/v1/projects/${ref}/config/database/pooler`
184+
) {
185+
return Effect.succeed(
186+
jsonResponse(request, 200, [
187+
{
188+
identifier: "primary",
189+
database_type: "PRIMARY",
190+
is_using_scram_auth: false,
191+
db_user: `postgres.${ref}`,
192+
db_host: "aws-0-us-east-1.pooler.supabase.green",
193+
db_port: 6543,
194+
db_name: "postgres",
195+
connection_string: `postgresql://postgres.${ref}:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.green:6543/postgres`,
196+
connectionString: "",
197+
default_pool_size: null,
198+
max_client_conn: null,
199+
pool_mode: "transaction",
200+
},
201+
]),
202+
);
203+
}
204+
if (
205+
request.method === "POST" &&
206+
url.href === `https://${ref}.supabase.green/storage/v1/bucket`
207+
) {
208+
const apiKey = request.headers["apikey"];
209+
bucketApiKeys.push(apiKey);
210+
return Effect.succeed(
211+
apiKey === "sb_secret_live"
212+
? jsonResponse(request, 200, { name: "supabase-cli-live-bucket" })
213+
: jsonResponse(request, 401, { message: "Legacy API keys are disabled" }),
214+
);
215+
}
216+
if (request.method === "DELETE" && url.pathname === `/v1/projects/${ref}`) {
217+
ownedProjects.delete(ref);
218+
return Effect.succeed(jsonResponse(request, 200, { id: 1, ref, name: "live-test" }));
219+
}
220+
return Effect.die(`unexpected request ${request.method} ${url.href}`);
221+
}),
222+
);
223+
224+
return Effect.runPromise(
225+
Effect.gen(function* () {
226+
const client = yield* makeApiClient({
227+
baseUrl: "https://api.supabase.green",
228+
accessToken: "test-token",
229+
});
230+
231+
const environment = yield* provisionLiveEnvironment(client);
232+
yield* cleanupLiveEnvironment(client, environment);
233+
234+
expect(environment.project).toMatchObject({
235+
anonKey: "sb_publishable_live",
236+
serviceRoleKey: "sb_secret_live",
237+
});
238+
expect(bucketApiKeys).toEqual(["sb_secret_live"]);
239+
expect(ownedProjects).toEqual(new Set());
240+
}).pipe(
241+
Effect.provide(
242+
Layer.mergeAll(platformApi, BunFileSystem.layer, BunPath.layer, cliConfigProviderLayer),
243+
),
244+
),
245+
);
246+
});
140247
});

‎apps/cli/tests/helpers/live-project.ts‎

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import * as HttpClientError from "effect/http/HttpClientError";
1919
import * as HttpClientRequest from "effect/http/HttpClientRequest";
2020
import { getDomain } from "tldts";
2121

22+
import { kongAuthHeaders } from "../../src/command-internal/kong-auth.ts";
23+
import { extractServiceKeys } from "../../src/command-internal/tenant-keys.ts";
2224
import {
2325
deriveLiveProjectHost,
2426
keepLiveProject,
@@ -37,7 +39,6 @@ const POLL_TIMEOUT = "5 minutes";
3739

3840
type Project = OperationOutput<"v1GetProject">;
3941
type Organization = OperationOutput<"v1ListAllOrganizations">[number];
40-
type ApiKey = OperationOutput<"v1GetProjectApiKeys">[number];
4142
export type PoolerConfig = OperationOutput<"v1GetPoolerConfig">[number];
4243
type LiveApi = Effect.Success<ReturnType<typeof makeApiClient>>;
4344
type Region =
@@ -386,18 +387,12 @@ function keysReadiness(
386387
return api.v1.getProjectApiKeys({ ref, reveal: true }).pipe(
387388
Effect.mapError((cause) => classifyPollError("project API keys", cause)),
388389
Effect.flatMap((keys) => {
389-
const keyValue = (key: ApiKey): string | undefined => key.api_key ?? undefined;
390-
const anonKey = keys.find((key) => key.name === "anon");
391-
const serviceRoleKey =
392-
keys.find((key) => key.name === "service_role") ??
393-
keys.find((key) => key.api_key?.startsWith("sb_secret_"));
394-
const anon = anonKey === undefined ? undefined : keyValue(anonKey);
395-
const service = serviceRoleKey === undefined ? undefined : keyValue(serviceRoleKey);
396-
return anon === undefined || service === undefined
390+
const { anon, serviceRole } = extractServiceKeys(keys);
391+
return anon === "" || serviceRole === ""
397392
? Effect.fail(
398393
new LiveTransientPoll({ phase: "project API keys", cause: "keys incomplete" }),
399394
)
400-
: Effect.succeed({ anonKey: anon, serviceRoleKey: service });
395+
: Effect.succeed({ anonKey: anon, serviceRoleKey: serviceRole });
401396
}),
402397
);
403398
}
@@ -495,7 +490,7 @@ function createStorageBucket(
495490
`https://${ref}.${host}/storage/v1/bucket`,
496491
).pipe(
497492
HttpClientRequest.setHeaders({
498-
Authorization: `Bearer ${serviceRoleKey}`,
493+
...kongAuthHeaders(serviceRoleKey),
499494
"Content-Type": "application/json",
500495
}),
501496
HttpClientRequest.bodyJson({ id: bucket, name: bucket, public: false }),

0 commit comments

Comments
 (0)