1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as resourcemanager from "@distilled.cloud/gcp/cloudresourcemanager_v3";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8import { GcpEnvironment } from "@/GCP/Environment";
9
10const { test } = Test.make({ providers: GCP.providers() });
11
12const logLevel = Effect.provideService(
13 MinimumLogLevel,
14 process.env.DEBUG ? "Debug" : "Info",
15);
16// Live create returns Forbidden: Permission 'resourcemanager.folders.create'
17// denied on resource '//cloudresourcemanager.googleapis.com/organizations/531963060189'.
18// Set GOOGLE_ORGANIZATION_ID when the credentials can create folders there.
19const runLifecycle = !!process.env.GOOGLE_ORGANIZATION_ID;
20
21const waitUntilGone = (name: string) =>
22 resourcemanager.getFolders({ name }).pipe(
23 Effect.map((folder) =>
24 folder.state === "DELETE_REQUESTED"
25 ? ("gone" as const)
26 : ("found" as const),
27 ),
28 Effect.catchTag(["NotFound", "FolderNotFound"], () =>
29 Effect.succeed("gone" as const),
30 ),
31 Effect.repeat({
32 schedule: Schedule.spaced("1 second"),
33 until: (status) => status === "gone",
34 times: 10,
35 }),
36 );
37
38const resolveParent = Effect.gen(function* () {
39 const { project } = yield* GcpEnvironment.current;
40 const resource = yield* resourcemanager.getProjects({
41 name: `projects/${project}`,
42 });
43 return resource.parent;
44});
45
46test.provider(
47 "getFolders on a missing folder fails with a typed tag",
48 (stack) =>
49 Effect.gen(function* () {
50 yield* stack.destroy();
51
52 const error = yield* Effect.flip(
53 resourcemanager.getFolders({
54 name: "folders/999999999999",
55 }),
56 );
57 expect(error._tag).toEqual("FolderNotFound");
58
59 yield* stack.destroy();
60 }).pipe(logLevel),
61 {
62 tags: ["provider:gcp", "provider:gcp:resourcemanager", "live"],
63 timeout: 90_000,
64 },
65);
66
67test.provider.skipIf(runLifecycle)(
68 "createFolders without folder IAM fails with Forbidden",
69 (stack) =>
70 Effect.gen(function* () {
71 yield* stack.destroy();
72
73 const parent = yield* resolveParent.pipe(
74 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
75 );
76 const error = yield* Effect.flip(
77 resourcemanager.createFolders({
78 body: {
79 parent: parent ?? "organizations/0",
80 displayName: "probe-folder",
81 },
82 }),
83 );
84 expect(error._tag).toEqual("Forbidden");
85
86 yield* stack.destroy();
87 }).pipe(logLevel),
88 {
89 tags: ["provider:gcp", "provider:gcp:resourcemanager", "live"],
90 timeout: 90_000,
91 },
92);
93
94test.provider.skipIf(!runLifecycle)(
95 "create, update, and delete a folder",
96 (stack) =>
97 Effect.gen(function* () {
98 yield* stack.destroy();
99
100 const created = yield* stack.deploy(
101 Effect.gen(function* () {
102 return yield* GCP.ResourceManager.Folder("Team", {
103 displayName: "platform",
104 });
105 }),
106 );
107
108 expect(created.name).toMatch(/^folders\//);
109 expect(created.displayName).toEqual("platform");
110 expect(created.parent).toMatch(/^(organizations|folders)\//);
111 expect(created.state).toEqual("ACTIVE");
112
113 const fetched = yield* resourcemanager.getFolders({
114 name: created.name,
115 });
116 expect(fetched.name).toEqual(created.name);
117 expect(fetched.displayName).toEqual(created.displayName);
118
119 const updated = yield* stack.deploy(
120 Effect.gen(function* () {
121 return yield* GCP.ResourceManager.Folder("Team", {
122 displayName: "platform-prod",
123 parent: created.parent,
124 });
125 }),
126 );
127
128 expect(updated.name).toEqual(created.name);
129 expect(updated.displayName).toEqual("platform-prod");
130 expect(updated.createTime).toEqual(created.createTime);
131
132 const fetchedUpdate = yield* resourcemanager.getFolders({
133 name: updated.name,
134 });
135 expect(fetchedUpdate.displayName).toEqual("platform-prod");
136
137 yield* stack.destroy();
138
139 const gone = yield* waitUntilGone(updated.name);
140 expect(gone).toEqual("gone");
141 }).pipe(logLevel),
142 {
143 tags: ["provider:gcp", "provider:gcp:resourcemanager", "live"],
144 timeout: 90_000,
145 },
146);