1import * as GCP from "@/GCP";
2import { GcpEnvironment } from "@/GCP/Environment";
3import * as Test from "@/Test/Alchemy";
4import * as dataplex from "@distilled.cloud/gcp/dataplex_v1";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import { MinimumLogLevel } from "effect/References";
8import * as Schedule from "effect/Schedule";
9import { withDataplexSlot } from "./quota.ts";
10
11const { test } = Test.make({ providers: GCP.providers() });
12
13const logLevel = Effect.provideService(
14 MinimumLogLevel,
15 process.env.DEBUG ? "Debug" : "Info",
16);
17
18// Attribute bindings hang off sunset data taxonomies, whose creates fail with
19// InternalServerError "An internal error has occurred". Set
20// GCP_TEST_DATAPLEX_DATA_ATTRIBUTE_BINDING=1 on a project that still has them.
21const runLifecycle =
22 process.env.GCP_TEST_DATAPLEX_DATA_ATTRIBUTE_BINDING === "1";
23
24const waitUntilGone = (name: string) =>
25 dataplex.getProjectsLocationsDataAttributeBindings({ name }).pipe(
26 Effect.as("found" as const),
27 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
28 Effect.repeat({
29 schedule: Schedule.spaced("1 second"),
30 until: (status) => status === "gone",
31 times: 10,
32 }),
33 );
34
35test.provider(
36 "getProjectsLocationsDataAttributeBindings on a missing binding fails with a typed tag",
37 (stack) =>
38 Effect.gen(function* () {
39 const { project } = yield* GcpEnvironment.current;
40 yield* stack.destroy();
41
42 const error = yield* Effect.flip(
43 dataplex.getProjectsLocationsDataAttributeBindings({
44 name: `projects/${project}/locations/us-central1/dataAttributeBindings/alchemy-missing-binding`,
45 }),
46 );
47 expect(error._tag).toEqual("NotFound");
48
49 yield* stack.destroy();
50 }).pipe(logLevel, withDataplexSlot),
51 { tags: ["provider:gcp", "provider:gcp:dataplex", "live"], timeout: 900_000 },
52);
53
54test.provider.skipIf(!runLifecycle)(
55 "create, update, and delete a data attribute binding",
56 (stack) =>
57 Effect.gen(function* () {
58 yield* stack.destroy();
59
60 const created = yield* stack.deploy(
61 Effect.gen(function* () {
62 return yield* GCP.Dataplex.DataAttributeBinding("Pii", {
63 location: "us-central1",
64 displayName: "pii bind",
65 description: "binding a",
66 labels: { env: "test" },
67 });
68 }),
69 );
70
71 expect(created.name).toContain("/dataAttributeBindings/");
72 expect(created.dataAttributeBindingId).toEqual(expect.any(String));
73 expect(created.labels).toMatchObject({ env: "test" });
74
75 const fetched = yield* dataplex.getProjectsLocationsDataAttributeBindings(
76 { name: created.name },
77 );
78 expect(fetched.name).toEqual(created.name);
79 expect(
80 Object.keys(fetched.labels ?? {}).some((key) =>
81 key.startsWith("alchemy-"),
82 ),
83 ).toEqual(true);
84
85 const updated = yield* stack.deploy(
86 Effect.gen(function* () {
87 return yield* GCP.Dataplex.DataAttributeBinding("Pii", {
88 dataAttributeBindingId: created.dataAttributeBindingId,
89 location: "us-central1",
90 displayName: "pii bind prod",
91 description: "binding b",
92 labels: { env: "prod", team: "data" },
93 });
94 }),
95 );
96
97 expect(updated.name).toEqual(created.name);
98 expect(updated.displayName).toEqual("pii bind prod");
99 expect(updated.labels).toMatchObject({ env: "prod", team: "data" });
100
101 yield* stack.destroy();
102 const gone = yield* waitUntilGone(created.name);
103 expect(gone).toEqual("gone");
104 }).pipe(logLevel, withDataplexSlot),
105 { tags: ["provider:gcp", "provider:gcp:dataplex", "live"], timeout: 900_000 },
106);