1import * as GCP from "@/GCP";
2import { GcpEnvironment } from "@/GCP/Environment";
3import * as Test from "@/Test/Alchemy";
4import * as Core from "@/Test/Core";
5import * as connectors from "@distilled.cloud/gcp/connectors_v2";
6import * as crm 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 ConnectorsBindingsHost, {
11 Account,
12 ENTITY_TYPE_PARENT,
13} from "./fixtures/bindings-host.ts";
14
15const testOptions = { providers: GCP.providers() };
16const { test, beforeAll, afterAll } = Test.make(testOptions);
17const sharedStack = Core.scratchStack(testOptions, "ConnectorsBindings");
18
19let baseUrl: string;
20let hostAccount: string;
21let entityName: string;
22
23// Entities live in an existing Integration Connectors connection; set
24// GCP_TEST_CONNECTORS_PARENT to its `…/entityTypes/{type}` to run this.
25describe.skipIf(!dockerAvailable || ENTITY_TYPE_PARENT.length === 0)(
26 "Connectors Bindings",
27 {
28 tags: [
29 "provider:gcp",
30 "provider:gcp:connectors",
31 "provider:gcp:run",
32 "live",
33 ],
34 },
35 () => {
36 beforeAll(
37 Effect.gen(function* () {
38 yield* sharedStack.destroy();
39 const out = yield* sharedStack.deploy(
40 Effect.gen(function* () {
41 const host = yield* ConnectorsBindingsHost;
42 const entity = yield* Account;
43 return {
44 uri: host.uri,
45 serviceAccount: host.serviceAccount,
46 entity: entity.name,
47 };
48 }),
49 );
50 baseUrl = out.uri!;
51 hostAccount = out.serviceAccount!;
52 entityName = out.entity;
53 }),
54 { timeout: 900_000 },
55 );
56
57 afterAll(sharedStack.destroy(), { timeout: 600_000 });
58
59 describe("GetEntity", () => {
60 test.provider(
61 "reads the entity as the host's service account, granted on the project",
62 (_stack) =>
63 Effect.gen(function* () {
64 const entity = yield* expectProbe<connectors.Entity>(
65 baseUrl,
66 "getEntity",
67 );
68 const live =
69 yield* connectors.getProjectsLocationsConnectionsEntityTypesEntities(
70 { name: entityName },
71 );
72 expect(entity.name).toEqual(entityName);
73 expect(entity.fields).toMatchObject({ Name: "Alchemy Binding" });
74 expect(entity.fields).toEqual(live.fields);
75
76 // Connectors has no resource-level IAM for entities.
77 const { project } = yield* GcpEnvironment.current;
78 const policy = yield* crm.getIamPolicyProjects({
79 resource: `projects/${project}`,
80 body: { options: { requestedPolicyVersion: 3 } },
81 });
82 const roles = (policy.bindings ?? [])
83 .filter((binding) =>
84 (binding.members ?? []).includes(
85 `serviceAccount:${hostAccount}`,
86 ),
87 )
88 .map((binding) => binding.role);
89 expect(roles).toEqual(["roles/connectors.invoker"]);
90 }),
91 {
92 tags: ["provider:gcp", "provider:gcp:connectors", "live"],
93 timeout: 600_000,
94 },
95 );
96 });
97 },
98);