1import { GcpEnvironment } from "@/GCP/Environment";
2import * as GCP from "@/GCP";
3import * as Test from "@/Test/Alchemy";
4import * as cloudcontrolspartner from "@distilled.cloud/gcp/cloudcontrolspartner_v1";
5import * as resourcemanager from "@distilled.cloud/gcp/cloudresourcemanager_v3";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { MinimumLogLevel } from "effect/References";
9import * as Schedule from "effect/Schedule";
10
11const { test } = Test.make({ providers: GCP.providers() });
12
13const logLevel = Effect.provideService(
14 MinimumLogLevel,
15 process.env.DEBUG ? "Debug" : "Info",
16);
17const location = "us-central1";
18
19const organizationOf = () =>
20 Effect.gen(function* () {
21 const { project } = yield* GcpEnvironment.current;
22
23 const fromEnv =
24 process.env.GOOGLE_CLOUDCONTROLSPARTNER_ORGANIZATION ??
25 process.env.GOOGLE_ORGANIZATION_ID;
26 if (fromEnv && fromEnv.length > 0) {
27 return fromEnv.startsWith("organizations/")
28 ? fromEnv
29 : `organizations/${fromEnv}`;
30 }
31 let current: string | undefined = `projects/${project}`;
32 for (let i = 0; i < 8; i++) {
33 if (current === undefined) return "";
34 if (current.startsWith("organizations/")) return current;
35 current = current.startsWith("projects/")
36 ? yield* resourcemanager.getProjects({ name: current }).pipe(
37 Effect.map((resource) => resource.parent),
38 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
39 )
40 : current.startsWith("folders/")
41 ? yield* resourcemanager.getFolders({ name: current }).pipe(
42 Effect.map((folder) => folder.parent),
43 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
44 )
45 : undefined;
46 }
47 return "";
48 });
49
50const waitUntilGone = (name: string) =>
51 cloudcontrolspartner.getOrganizationsLocationsCustomers({ name }).pipe(
52 Effect.as("found" as const),
53 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
54 Effect.repeat({
55 schedule: Schedule.spaced("1 second"),
56 until: (status) => status === "gone",
57 times: 10,
58 }),
59 );
60
61const customerOrgId = (
62 process.env.GOOGLE_CLOUDCONTROLSPARTNER_CUSTOMER_ID ?? ""
63).trim();
64
65// Cloud Controls Partner is for allow-listed Sovereign Cloud partners: without
66// partner permissions on the organization every call answers 403 ("Permission
67// ... denied ... (or it may not exist)"). Set GCP_TEST_CLOUDCONTROLSPARTNER=1
68// and GOOGLE_CLOUDCONTROLSPARTNER_CUSTOMER_ID on a partner organization.
69const runLifecycle =
70 !process.env.FAST &&
71 process.env.GCP_TEST_CLOUDCONTROLSPARTNER === "1" &&
72 customerOrgId.length > 0;
73
74test.provider.skipIf(!runLifecycle)(
75 "getOrganizationsLocationsCustomers on a missing customer fails with NotFound",
76 (stack) =>
77 Effect.gen(function* () {
78 yield* stack.destroy();
79
80 const organization = (yield* organizationOf()) || "organizations/0";
81 const error = yield* Effect.flip(
82 cloudcontrolspartner.getOrganizationsLocationsCustomers({
83 name: `${organization}/locations/${location}/customers/0`,
84 }),
85 );
86 expect(error._tag).toEqual("NotFound");
87
88 yield* stack.destroy();
89 }).pipe(logLevel),
90 {
91 tags: ["provider:gcp", "provider:gcp:cloudcontrolspartner", "live"],
92 timeout: 90_000,
93 },
94);
95
96test.provider.skipIf(runLifecycle)(
97 "createOrganizationsLocationsCustomers without partner permissions fails with Forbidden",
98 (stack) =>
99 Effect.gen(function* () {
100 yield* stack.destroy();
101
102 const organization = (yield* organizationOf()) || "organizations/0";
103 const error = yield* Effect.flip(
104 cloudcontrolspartner.createOrganizationsLocationsCustomers({
105 parent: `${organization}/locations/${location}`,
106 customerId: "0",
107 body: { displayName: "Alchemy Cloudcontrolspartner Probe" },
108 }),
109 );
110 expect(error._tag).toEqual("Forbidden");
111
112 yield* stack.destroy();
113 }).pipe(logLevel),
114 {
115 tags: ["provider:gcp", "provider:gcp:cloudcontrolspartner", "live"],
116 timeout: 90_000,
117 },
118);
119
120test.provider.skipIf(!runLifecycle)(
121 "create, update, and delete a customer",
122 (stack) =>
123 Effect.gen(function* () {
124 yield* stack.destroy();
125
126 const organization = (yield* organizationOf()) || "organizations/0";
127 const parent = `${organization}/locations/${location}`;
128
129 const created = yield* stack.deploy(
130 Effect.gen(function* () {
131 return yield* GCP.CloudControlsPartner.Customer("Acme", {
132 organization,
133 location,
134 customerId: customerOrgId,
135 displayName: "Acme Corp",
136 });
137 }),
138 );
139
140 expect(created.name).toEqual(`${parent}/customers/${customerOrgId}`);
141 expect(created.customerId).toEqual(customerOrgId);
142 expect(created.displayName).toEqual("Acme Corp");
143 expect(created.location).toEqual(location);
144
145 const fetched =
146 yield* cloudcontrolspartner.getOrganizationsLocationsCustomers({
147 name: created.name,
148 });
149 expect(fetched.name).toEqual(created.name);
150 expect(fetched.displayName).toContain("[alchemy ");
151 expect(fetched.displayName).toContain("Acme Corp");
152
153 const updated = yield* stack.deploy(
154 Effect.gen(function* () {
155 return yield* GCP.CloudControlsPartner.Customer("Acme", {
156 organization,
157 location,
158 customerId: created.customerId,
159 displayName: "Acme Corporation",
160 });
161 }),
162 );
163
164 expect(updated.name).toEqual(created.name);
165 expect(updated.displayName).toEqual("Acme Corporation");
166
167 yield* stack.destroy();
168
169 const gone = yield* waitUntilGone(created.name);
170 expect(gone).toEqual("gone");
171 }).pipe(logLevel),
172 {
173 tags: ["provider:gcp", "provider:gcp:cloudcontrolspartner", "live"],
174 timeout: 90_000,
175 },
176);