1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as Core from "@/Test/Core";
4import * as crm from "@distilled.cloud/gcp/cloudresourcemanager_v3";
5import type * as developerconnect from "@distilled.cloud/gcp/developerconnect_v1";
6import { describe, expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { dockerAvailable, expectProbe } from "../bindingHost.ts";
9import DeveloperConnectBindingsHost, {
10 linkEnabled,
11 Source,
12} from "./fixtures/bindings-host.ts";
13
14const testOptions = { providers: GCP.providers() };
15const { test, beforeAll, afterAll } = Test.make(testOptions);
16const sharedStack = Core.scratchStack(testOptions, "DeveloperConnectBindings");
17
18let baseUrl: string;
19let hostAccount: string;
20let project: string;
21
22/** The token/ref bindings grant their roles on the project. */
23const expectProjectGrants = Effect.gen(function* () {
24 const policy = yield* crm.getIamPolicyProjects({
25 resource: `projects/${project}`,
26 body: { options: { requestedPolicyVersion: 3 } },
27 });
28 const roles = (policy.bindings ?? [])
29 .filter((binding) =>
30 (binding.members ?? []).includes(`serviceAccount:${hostAccount}`),
31 )
32 .map((binding) => ({ role: binding.role, condition: binding.condition }))
33 .sort((a, b) => (a.role ?? "").localeCompare(b.role ?? ""));
34 expect(roles).toEqual([
35 { role: "roles/developerconnect.readTokenAccessor", condition: undefined },
36 { role: "roles/developerconnect.tokenAccessor", condition: undefined },
37 { role: "roles/developerconnect.user", condition: undefined },
38 ]);
39});
40
41describe.skipIf(!dockerAvailable || !linkEnabled)(
42 "DeveloperConnect Bindings",
43 {
44 tags: [
45 "provider:gcp",
46 "provider:gcp:developerconnect",
47 "provider:gcp:run",
48 "live",
49 ],
50 },
51 () => {
52 beforeAll(
53 Effect.gen(function* () {
54 yield* sharedStack.destroy();
55 const out = yield* sharedStack.deploy(
56 Effect.gen(function* () {
57 const host = yield* DeveloperConnectBindingsHost;
58 const link = yield* Source;
59 return {
60 uri: host.uri,
61 serviceAccount: host.serviceAccount,
62 project: link.project,
63 };
64 }),
65 );
66 baseUrl = out.uri!;
67 hostAccount = out.serviceAccount!;
68 project = out.project;
69 }),
70 { timeout: 900_000 },
71 );
72
73 afterAll(sharedStack.destroy(), { timeout: 600_000 });
74
75 describe("FetchReadToken", () => {
76 test.provider(
77 "mints a read token as the host's service account",
78 (_stack) =>
79 Effect.gen(function* () {
80 expect(yield* expectProbe(baseUrl, "fetchReadToken")).toEqual({
81 hasToken: true,
82 });
83 yield* expectProjectGrants;
84 }),
85 {
86 tags: ["provider:gcp", "provider:gcp:developerconnect", "live"],
87 timeout: 600_000,
88 },
89 );
90 });
91
92 describe("FetchReadWriteToken", () => {
93 test.provider(
94 "mints a read-write token as the host's service account",
95 (_stack) =>
96 Effect.gen(function* () {
97 expect(yield* expectProbe(baseUrl, "fetchReadWriteToken")).toEqual({
98 hasToken: true,
99 });
100 yield* expectProjectGrants;
101 }),
102 {
103 tags: ["provider:gcp", "provider:gcp:developerconnect", "live"],
104 timeout: 600_000,
105 },
106 );
107 });
108
109 describe("FetchGitRefs", () => {
110 test.provider(
111 "lists branches as the host's service account",
112 (_stack) =>
113 Effect.gen(function* () {
114 const refs =
115 yield* expectProbe<developerconnect.FetchGitRefsResponse>(
116 baseUrl,
117 "fetchGitRefs",
118 );
119 // Every cloneable repository has at least its default branch.
120 expect(refs.refNames?.length).toBeGreaterThan(0);
121 yield* expectProjectGrants;
122 }),
123 {
124 tags: ["provider:gcp", "provider:gcp:developerconnect", "live"],
125 timeout: 600_000,
126 },
127 );
128 });
129 },
130);