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.getBillingAccountsLocationsBucketsViews({ 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.skipIf(!billingAccountId)(
36 "getBillingAccountsLocationsBucketsViews on a missing view fails with NotFound",
37 (stack) =>
38 Effect.gen(function* () {
39 yield* stack.destroy();
40
41 const error = yield* Effect.flip(
42 logging.getBillingAccountsLocationsBucketsViews({
43 name: `billingAccounts/${account}/locations/global/buckets/_Default/views/alchemy-missing`,
44 }),
45 );
46 expect(error._tag).toEqual("NotFound");
47
48 yield* stack.destroy();
49 }).pipe(logLevel),
50 { tags: ["provider:gcp", "provider:gcp:logging", "live"], timeout: 90_000 },
51);
52
53test.provider.skipIf(!billingAccountId)(
54 "create, update, replace, and delete a billing bucket view",
55 (stack) =>
56 Effect.gen(function* () {
57 yield* stack.destroy();
58
59 const created = yield* stack.deploy(
60 Effect.gen(function* () {
61 const bucket = yield* GCP.Logging.BillingBucket("AppLogs", {
62 billingAccountId: account,
63 description: "view parent",
64 });
65 const view = yield* GCP.Logging.BillingBucketsView("Gce", {
66 billingAccountId: account,
67 location: bucket.location,
68 bucketId: bucket.bucketId,
69 filter: 'resource.type = "gce_instance"',
70 description: "compute logs",
71 });
72 return { bucket, view };
73 }),
74 );
75
76 expect(created.view.viewId).toEqual(expect.any(String));
77 expect(created.view.bucketId).toEqual(created.bucket.bucketId);
78 expect(created.view.filter).toEqual('resource.type = "gce_instance"');
79 expect(created.view.description).toEqual("compute logs");
80
81 const fetched = yield* logging.getBillingAccountsLocationsBucketsViews({
82 name: created.view.name,
83 });
84 expect(fetched.filter).toEqual('resource.type = "gce_instance"');
85 expect(fetched.description).toContain("alchemy-id=");
86
87 const updated = yield* stack.deploy(
88 Effect.gen(function* () {
89 const bucket = yield* GCP.Logging.BillingBucket("AppLogs", {
90 billingAccountId: account,
91 bucketId: created.bucket.bucketId,
92 location: created.bucket.location,
93 description: "view parent",
94 });
95 const view = yield* GCP.Logging.BillingBucketsView("Gce", {
96 billingAccountId: account,
97 location: bucket.location,
98 bucketId: bucket.bucketId,
99 viewId: created.view.viewId,
100 filter: 'resource.type = "gce_instance" AND severity>=ERROR',
101 description: "compute errors",
102 });
103 return { bucket, view };
104 }),
105 );
106
107 expect(updated.view.name).toEqual(created.view.name);
108 expect(updated.view.filter).toEqual(
109 'resource.type = "gce_instance" AND severity>=ERROR',
110 );
111
112 const last = created.view.viewId.at(-1) ?? "a";
113 const nextViewId = `${created.view.viewId.slice(0, -1)}${last === "z" ? "0" : "z"}`;
114
115 const replaced = yield* stack.deploy(
116 Effect.gen(function* () {
117 const bucket = yield* GCP.Logging.BillingBucket("AppLogs", {
118 billingAccountId: account,
119 bucketId: created.bucket.bucketId,
120 location: created.bucket.location,
121 description: "view parent",
122 });
123 const view = yield* GCP.Logging.BillingBucketsView("Gce", {
124 billingAccountId: account,
125 location: bucket.location,
126 bucketId: bucket.bucketId,
127 viewId: nextViewId,
128 filter: 'resource.type = "gce_instance"',
129 description: "replaced view",
130 });
131 return { bucket, view };
132 }),
133 );
134
135 expect(replaced.view.viewId).not.toEqual(created.view.viewId);
136
137 const previousGone = yield* waitUntilGone(created.view.name);
138 expect(previousGone).toEqual("gone");
139
140 yield* stack.destroy();
141
142 const gone = yield* waitUntilGone(replaced.view.name);
143 expect(gone).toEqual("gone");
144 }).pipe(logLevel),
145 { tags: ["provider:gcp", "provider:gcp:logging", "live"], timeout: 90_000 },
146);