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.getBillingAccountsLocationsBuckets({ name }).pipe(
26 Effect.map((bucket) =>
27 bucket.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("1 second"),
34 until: (status) => status === "gone",
35 times: 10,
36 }),
37 );
38
39test.provider.skipIf(!billingAccountId)(
40 "getBillingAccountsLocationsBuckets on a missing bucket fails with NotFound",
41 (stack) =>
42 Effect.gen(function* () {
43 yield* stack.destroy();
44
45 const error = yield* Effect.flip(
46 logging.getBillingAccountsLocationsBuckets({
47 name: `billingAccounts/${account}/locations/global/buckets/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, update, replace, and delete a billing log bucket",
59 (stack) =>
60 Effect.gen(function* () {
61 yield* stack.destroy();
62
63 const created = yield* stack.deploy(
64 Effect.gen(function* () {
65 return yield* GCP.Logging.BillingBucket("AppLogs", {
66 billingAccountId: account,
67 description: "application logs",
68 retentionDays: 31,
69 });
70 }),
71 );
72
73 expect(created.bucketId).toEqual(expect.any(String));
74 expect(created.location).toEqual("global");
75 expect(created.billingAccountId).toEqual(account);
76 expect(created.name).toEqual(
77 `billingAccounts/${account}/locations/global/buckets/${created.bucketId}`,
78 );
79 expect(created.description).toEqual("application logs");
80 expect(created.retentionDays).toEqual(31);
81
82 const fetched = yield* logging.getBillingAccountsLocationsBuckets({
83 name: created.name,
84 });
85 expect(fetched.retentionDays).toEqual(31);
86 expect(fetched.description).toContain("alchemy-id=");
87
88 const updated = yield* stack.deploy(
89 Effect.gen(function* () {
90 return yield* GCP.Logging.BillingBucket("AppLogs", {
91 billingAccountId: account,
92 bucketId: created.bucketId,
93 location: created.location,
94 description: "retained application logs",
95 retentionDays: 60,
96 });
97 }),
98 );
99
100 expect(updated.name).toEqual(created.name);
101 expect(updated.retentionDays).toEqual(60);
102
103 const last = created.bucketId.at(-1) ?? "a";
104 const nextBucketId = `${created.bucketId.slice(0, -1)}${last === "z" ? "0" : "z"}`;
105
106 const replaced = yield* stack.deploy(
107 Effect.gen(function* () {
108 return yield* GCP.Logging.BillingBucket("AppLogs", {
109 billingAccountId: account,
110 bucketId: nextBucketId,
111 location: "global",
112 description: "replaced bucket",
113 retentionDays: 31,
114 });
115 }),
116 );
117
118 expect(replaced.bucketId).not.toEqual(created.bucketId);
119
120 const previousGone = yield* waitUntilGone(created.name);
121 expect(previousGone).toEqual("gone");
122
123 yield* stack.destroy();
124
125 const gone = yield* waitUntilGone(replaced.name);
126 expect(gone).toEqual("gone");
127 }).pipe(logLevel),
128 { tags: ["provider:gcp", "provider:gcp:logging", "live"], timeout: 90_000 },
129);