1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as logging from "@distilled.cloud/gcp/logging_v2";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8
9const { test } = Test.make({ providers: GCP.providers() });
10
11// Organization-scoped: set GOOGLE_ORGANIZATION_ID when the credentials
12// administer the organization (the testing service account does not).
13const organizationId = process.env.GOOGLE_ORGANIZATION_ID?.trim().replace(
14 /^organizations\//,
15 "",
16);
17const organization = `organizations/${organizationId}`;
18
19const logLevel = Effect.provideService(
20 MinimumLogLevel,
21 process.env.DEBUG ? "Debug" : "Info",
22);
23
24const waitUntilGone = (name: string) =>
25 logging.getOrganizationsLocationsBucketsLinks({ name }).pipe(
26 Effect.map((link) =>
27 link.lifecycleState === "DELETE_REQUESTED"
28 ? ("gone" as const)
29 : ("found" as const),
30 ),
31 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
32 Effect.repeat({
33 schedule: Schedule.spaced("2 seconds"),
34 until: (status) => status === "gone",
35 times: 10,
36 }),
37 );
38
39test.provider.skipIf(!organizationId)(
40 "getOrganizationsLocationsBucketsLinks on a missing link fails with NotFound",
41 (stack) =>
42 Effect.gen(function* () {
43 yield* stack.destroy();
44
45 const error = yield* Effect.flip(
46 logging.getOrganizationsLocationsBucketsLinks({
47 name: `${organization}/locations/global/buckets/_Default/links/alchemy_missing`,
48 }),
49 );
50 expect(error._tag).toEqual("NotFound");
51
52 yield* stack.destroy();
53 }).pipe(logLevel),
54 { tags: ["provider:gcp", "provider:gcp:logging", "live"], timeout: 90_000 },
55);
56
57test.provider.skipIf(!organizationId || !!process.env.FAST)(
58 "create, replace, and delete an organization logging bucket link",
59 (stack) =>
60 Effect.gen(function* () {
61 yield* stack.destroy();
62
63 const created = yield* stack.deploy(
64 Effect.gen(function* () {
65 const bucket = yield* GCP.Logging.OrganizationLogBucket("Analytics", {
66 organization,
67 analyticsEnabled: true,
68 description: "analytics parent",
69 });
70 const link = yield* GCP.Logging.OrganizationBucketsLink("Bq", {
71 organization,
72 bucket: bucket.name,
73 description: "log analytics dataset",
74 });
75 return { bucket, link };
76 }),
77 );
78
79 expect(created.link.linkId).toEqual(expect.any(String));
80 expect(created.link.bucket).toEqual(created.bucket.name);
81 expect(created.link.name).toEqual(
82 `${created.bucket.name}/links/${created.link.linkId}`,
83 );
84 expect(created.link.description).toEqual("log analytics dataset");
85
86 const fetched = yield* logging.getOrganizationsLocationsBucketsLinks({
87 name: created.link.name,
88 });
89 expect(fetched.description).toContain("alchemy-id=");
90
91 const last = created.link.linkId.at(-1) ?? "a";
92 const nextLinkId = `${created.link.linkId.slice(0, -1)}${last === "z" ? "0" : "a"}`;
93
94 const replaced = yield* stack.deploy(
95 Effect.gen(function* () {
96 const bucket = yield* GCP.Logging.OrganizationLogBucket("Analytics", {
97 organization,
98 bucketId: created.bucket.bucketId,
99 location: created.bucket.location,
100 analyticsEnabled: true,
101 description: "analytics parent",
102 });
103 const link = yield* GCP.Logging.OrganizationBucketsLink("Bq", {
104 organization,
105 bucket: bucket.name,
106 linkId: nextLinkId,
107 description: "replaced link",
108 });
109 return { bucket, link };
110 }),
111 );
112
113 expect(replaced.link.linkId).not.toEqual(created.link.linkId);
114
115 const previousGone = yield* waitUntilGone(created.link.name);
116 expect(previousGone).toEqual("gone");
117
118 yield* stack.destroy();
119
120 const gone = yield* waitUntilGone(replaced.link.name);
121 expect(gone).toEqual("gone");
122 }).pipe(logLevel),
123 { tags: ["provider:gcp", "provider:gcp:logging", "live"], timeout: 90_000 },
124);