1import * as GCP from "@/GCP";
2import { GcpEnvironment } from "@/GCP/Environment";
3import * as Test from "@/Test/Alchemy";
4import * as Core from "@/Test/Core";
5import * as apikeys from "@distilled.cloud/gcp/apikeys_v2";
6import * as resourcemanager from "@distilled.cloud/gcp/cloudresourcemanager_v3";
7import { describe, expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import { dockerAvailable, expectProbe } from "../bindingHost.ts";
10import ApiKeysBindingsHost, { Maps } from "./fixtures/bindings-host.ts";
11
12const testOptions = { providers: GCP.providers() };
13const { test, beforeAll, afterAll } = Test.make(testOptions);
14const sharedStack = Core.scratchStack(testOptions, "ApiKeysBindings");
15
16let baseUrl: string;
17let keyName: string;
18let hostAccount: string;
19
20/** Every project-level role (and its IAM Condition) `account` holds. */
21const projectGrantsOf = (account: string) =>
22 Effect.gen(function* () {
23 const { project } = yield* GcpEnvironment.current;
24 const policy = yield* resourcemanager.getIamPolicyProjects({
25 resource: `projects/${project}`,
26 body: { options: { requestedPolicyVersion: 3 } },
27 });
28 return (policy.bindings ?? [])
29 .filter((binding) =>
30 (binding.members ?? []).includes(`serviceAccount:${account}`),
31 )
32 .map((binding) => ({
33 role: binding.role,
34 condition: binding.condition?.expression,
35 }));
36 });
37
38describe.skipIf(!dockerAvailable)(
39 "ApiKeys Bindings",
40 {
41 tags: ["provider:gcp", "provider:gcp:apikeys", "provider:gcp:run", "live"],
42 },
43 () => {
44 beforeAll(
45 Effect.gen(function* () {
46 yield* sharedStack.destroy();
47 const out = yield* sharedStack.deploy(
48 Effect.gen(function* () {
49 const host = yield* ApiKeysBindingsHost;
50 const key = yield* Maps;
51 return {
52 uri: host.uri,
53 serviceAccount: host.serviceAccount,
54 key: key.name,
55 };
56 }),
57 );
58 baseUrl = out.uri!;
59 keyName = out.key;
60 hostAccount = out.serviceAccount!;
61 }),
62 { timeout: 900_000 },
63 );
64
65 afterAll(sharedStack.destroy(), { timeout: 600_000 });
66
67 describe("GetKeyString", () => {
68 test.provider(
69 "reads the key string as the host's service account",
70 (_stack) =>
71 Effect.gen(function* () {
72 const out = yield* expectProbe<{ keyString?: string }>(
73 baseUrl,
74 "getKeyString",
75 );
76 const expected = yield* apikeys.getKeyStringProjectsLocationsKeys({
77 name: keyName,
78 });
79 expect(out.keyString).toEqual(expected.keyString);
80 expect(out.keyString?.length ?? 0).toBeGreaterThan(8);
81
82 // API keys have no resource-level IAM policy.
83 const grants = yield* projectGrantsOf(hostAccount);
84 expect(grants).toEqual([
85 {
86 role: "roles/serviceusage.apiKeysViewer",
87 condition: undefined,
88 },
89 ]);
90 }),
91 {
92 tags: ["provider:gcp", "provider:gcp:apikeys", "live"],
93 timeout: 600_000,
94 },
95 );
96 });
97 },
98);