1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import { GcpEnvironment } from "@/GCP/Environment";
4import * as resourcemanager from "@distilled.cloud/gcp/cloudresourcemanager_v3";
5import * as dataplex from "@distilled.cloud/gcp/dataplex_v1";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { MinimumLogLevel } from "effect/References";
9import * as Schedule from "effect/Schedule";
10import { withDataplexSlot } from "./quota.ts";
11
12const { test } = Test.make({ providers: GCP.providers() });
13
14const logLevel = Effect.provideService(
15 MinimumLogLevel,
16 process.env.DEBUG ? "Debug" : "Info",
17);
18
19const organizationId = process.env.GOOGLE_ORGANIZATION_ID ?? "";
20const runLifecycle = !process.env.FAST && organizationId.length > 0;
21
22const waitUntilGone = (name: string) =>
23 dataplex.getOrganizationsLocationsEncryptionConfigs({ name }).pipe(
24 Effect.as("found" as const),
25 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
26 Effect.repeat({
27 schedule: Schedule.spaced("2 seconds"),
28 until: (status) => status === "gone",
29 times: 10,
30 }),
31 );
32
33test.provider(
34 "getOrganizationsLocationsEncryptionConfigs on a missing config fails with a typed tag",
35 (stack) =>
36 Effect.gen(function* () {
37 yield* stack.destroy();
38
39 const { project } = yield* GcpEnvironment.current;
40 // Probe the test project's own organization so the name is valid.
41 const projectInfo = yield* resourcemanager.getProjects({
42 name: `projects/${project}`,
43 });
44 const org =
45 organizationId.length > 0
46 ? organizationId
47 : (projectInfo.parent ?? "").replace(/^organizations\//, "");
48 const error = yield* Effect.flip(
49 dataplex.getOrganizationsLocationsEncryptionConfigs({
50 name: `organizations/${org}/locations/us-central1/encryptionConfigs/alchemy-missing`,
51 }),
52 );
53 // Without an organization-level Dataplex role the API answers
54 // "Permission 'dataplex.encryptionConfig.get' denied".
55 expect(error._tag).toEqual(runLifecycle ? "NotFound" : "Forbidden");
56
57 yield* stack.destroy();
58 }).pipe(logLevel, withDataplexSlot),
59 { tags: ["provider:gcp", "provider:gcp:dataplex", "live"], timeout: 900_000 },
60);
61
62test.provider.skipIf(!runLifecycle)(
63 "create, update, and delete an encryption config",
64 (stack) =>
65 Effect.gen(function* () {
66 yield* stack.destroy();
67
68 const created = yield* stack.deploy(
69 Effect.gen(function* () {
70 return yield* GCP.Dataplex.EncryptionConfig("Default", {
71 organizationId,
72 location: "us-central1",
73 encryptionConfigId: "default",
74 enableMetastoreEncryption: false,
75 });
76 }),
77 );
78
79 expect(created.name).toContain("/encryptionConfigs/");
80 expect(created.encryptionConfigId).toEqual("default");
81 expect(created.organizationId).toEqual(organizationId);
82 expect(created.location).toEqual("us-central1");
83
84 const fetched =
85 yield* dataplex.getOrganizationsLocationsEncryptionConfigs({
86 name: created.name,
87 });
88 expect(fetched.name).toEqual(created.name);
89
90 const updated = yield* stack.deploy(
91 Effect.gen(function* () {
92 return yield* GCP.Dataplex.EncryptionConfig("Default", {
93 organizationId,
94 location: "us-central1",
95 encryptionConfigId: "default",
96 enableMetastoreEncryption: false,
97 });
98 }),
99 );
100
101 expect(updated.name).toEqual(created.name);
102
103 yield* stack.destroy();
104 const gone = yield* waitUntilGone(created.name);
105 expect(gone).toEqual("gone");
106 }).pipe(logLevel, withDataplexSlot),
107 { tags: ["provider:gcp", "provider:gcp:dataplex", "live"], timeout: 900_000 },
108);