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// Billing-account-scoped: set GOOGLE_BILLING_ACCOUNT when the credentials
12// administer the billing account (the testing service account does not).
13const billingAccountId = process.env.GOOGLE_BILLING_ACCOUNT?.trim().replace(
14 /^billingAccounts\//,
15 "",
16);
17const account = billingAccountId ?? "";
18
19const logLevel = Effect.provideService(
20 MinimumLogLevel,
21 process.env.DEBUG ? "Debug" : "Info",
22);
23
24const waitUntilGone = (name: string) =>
25 logging.getBillingAccountsLocationsBucketsLinks({ 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(!billingAccountId)(
40 "getBillingAccountsLocationsBucketsLinks 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.getBillingAccountsLocationsBucketsLinks({
47 name: `billingAccounts/${account}/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(!billingAccountId)(
58 "create, replace, and delete a billing 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.BillingBucket("AppLogs", {
66 billingAccountId: account,
67 analyticsEnabled: true,
68 description: "analytics parent",
69 });
70 const link = yield* GCP.Logging.BillingBucketsLink("Analytics", {
71 billingAccountId: account,
72 location: bucket.location,
73 bucketId: bucket.bucketId,
74 description: "log analytics",
75 });
76 return { bucket, link };
77 }),
78 );
79
80 expect(created.link.linkId).toEqual(expect.any(String));
81 expect(created.link.bucketId).toEqual(created.bucket.bucketId);
82 expect(created.link.description).toEqual("log analytics");
83
84 const fetched = yield* logging.getBillingAccountsLocationsBucketsLinks({
85 name: created.link.name,
86 });
87 expect(fetched.description).toContain("alchemy-id=");
88
89 const nextLinkId = `${created.link.linkId.replace(/[^a-z0-9_]/g, "_")}_z`;
90
91 const replaced = yield* stack.deploy(
92 Effect.gen(function* () {
93 const bucket = yield* GCP.Logging.BillingBucket("AppLogs", {
94 billingAccountId: account,
95 bucketId: created.bucket.bucketId,
96 location: created.bucket.location,
97 analyticsEnabled: true,
98 description: "analytics parent",
99 });
100 const link = yield* GCP.Logging.BillingBucketsLink("Analytics", {
101 billingAccountId: account,
102 location: bucket.location,
103 bucketId: bucket.bucketId,
104 linkId: nextLinkId,
105 description: "replaced link",
106 });
107 return { bucket, link };
108 }),
109 );
110
111 expect(replaced.link.linkId).not.toEqual(created.link.linkId);
112
113 const previousGone = yield* waitUntilGone(created.link.name);
114 expect(previousGone).toEqual("gone");
115
116 yield* stack.destroy();
117
118 const gone = yield* waitUntilGone(replaced.link.name);
119 expect(gone).toEqual("gone");
120 }).pipe(logLevel),
121 { tags: ["provider:gcp", "provider:gcp:logging", "live"], timeout: 120_000 },
122);