1import { GcpEnvironment } from "@/GCP/Environment";
2import * as GCP from "@/GCP";
3import * as Test from "@/Test/Alchemy";
4import * as billingbudgets from "@distilled.cloud/gcp/billingbudgets_v1";
5import * as cloudbilling from "@distilled.cloud/gcp/cloudbilling_v1";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { MinimumLogLevel } from "effect/References";
9import * as Schedule from "effect/Schedule";
10
11const { test } = Test.make({ providers: GCP.providers() });
12
13const logLevel = Effect.provideService(
14 MinimumLogLevel,
15 process.env.DEBUG ? "Debug" : "Info",
16);
17
18const waitUntilGone = (name: string) =>
19 billingbudgets.getBillingAccountsBudgets({ name }).pipe(
20 Effect.as("found" as const),
21 Effect.catchTag(["NotFound", "BudgetNotFound"], () =>
22 Effect.succeed("gone" as const),
23 ),
24 Effect.repeat({
25 schedule: Schedule.spaced("1 second"),
26 until: (status) => status === "gone",
27 times: 10,
28 }),
29 );
30
31const billingAccountId = () => {
32 const fromEnv = process.env.GOOGLE_BILLING_ACCOUNT;
33 if (fromEnv && fromEnv.length > 0) {
34 return Effect.succeed(
35 fromEnv.startsWith("billingAccounts/")
36 ? fromEnv.slice("billingAccounts/".length)
37 : fromEnv,
38 );
39 }
40 return GcpEnvironment.current.pipe(
41 Effect.flatMap(({ project }) =>
42 cloudbilling.getBillingInfoProjects({ name: `projects/${project}` }),
43 ),
44 Effect.map(
45 (info) => (info.billingAccountName ?? "").split("/").pop() ?? "",
46 ),
47 );
48};
49
50test.provider(
51 "getBillingAccountsBudgets on a missing budget fails with a typed tag",
52 (stack) =>
53 Effect.gen(function* () {
54 yield* stack.destroy();
55
56 const account = yield* billingAccountId();
57 const error = yield* Effect.flip(
58 billingbudgets.getBillingAccountsBudgets({
59 name: `billingAccounts/${account}/budgets/00000000-0000-0000-0000-000000000000`,
60 }),
61 );
62 expect(error._tag).toEqual("BudgetNotFound");
63
64 yield* stack.destroy();
65 }).pipe(logLevel),
66 {
67 tags: ["provider:gcp", "provider:gcp:billingbudgets", "live"],
68 timeout: 90_000,
69 },
70);
71
72test.provider(
73 "create, update, and delete a billing budget",
74 (stack) =>
75 Effect.gen(function* () {
76 const { project } = yield* GcpEnvironment.current;
77
78 yield* stack.destroy();
79
80 const account = yield* billingAccountId();
81 expect(account.length).toBeGreaterThan(0);
82
83 const created = yield* stack.deploy(
84 Effect.gen(function* () {
85 return yield* GCP.BillingBudgets.BillingBudget("Spend", {
86 billingAccountId: account,
87 displayName: "cap",
88 amount: {
89 specifiedAmount: { currencyCode: "USD", units: "10" },
90 },
91 budgetFilter: {
92 projects: [`projects/${project}`],
93 calendarPeriod: "MONTH",
94 },
95 thresholdRules: [
96 { thresholdPercent: 0.5, spendBasis: "CURRENT_SPEND" },
97 ],
98 });
99 }),
100 );
101
102 expect(created.name).toContain(`/budgets/`);
103 expect(created.budgetId).toEqual(expect.any(String));
104 expect(created.billingAccountId).toEqual(account);
105 expect(created.displayName).toEqual("cap");
106 expect(created.amount?.specifiedAmount?.units).toEqual("10");
107 expect(created.thresholdRules[0]?.thresholdPercent).toEqual(0.5);
108
109 const fetched = yield* billingbudgets.getBillingAccountsBudgets({
110 name: created.name,
111 });
112 expect(fetched.name).toEqual(created.name);
113 expect(fetched.displayName).toEqual("cap");
114 expect(fetched.amount?.specifiedAmount?.units).toEqual("10");
115
116 const updated = yield* stack.deploy(
117 Effect.gen(function* () {
118 return yield* GCP.BillingBudgets.BillingBudget("Spend", {
119 billingAccountId: account,
120 displayName: "cap",
121 amount: {
122 specifiedAmount: { currencyCode: "USD", units: "25" },
123 },
124 budgetFilter: {
125 projects: [`projects/${project}`],
126 calendarPeriod: "MONTH",
127 },
128 thresholdRules: [
129 { thresholdPercent: 0.5, spendBasis: "CURRENT_SPEND" },
130 { thresholdPercent: 0.9, spendBasis: "CURRENT_SPEND" },
131 ],
132 });
133 }),
134 );
135
136 expect(updated.name).toEqual(created.name);
137 expect(updated.budgetId).toEqual(created.budgetId);
138 expect(updated.amount?.specifiedAmount?.units).toEqual("25");
139 expect(updated.thresholdRules.length).toEqual(2);
140
141 const fetchedUpdate = yield* billingbudgets.getBillingAccountsBudgets({
142 name: created.name,
143 });
144 expect(fetchedUpdate.amount?.specifiedAmount?.units).toEqual("25");
145 expect(fetchedUpdate.thresholdRules?.length).toEqual(2);
146
147 yield* stack.destroy();
148
149 const gone = yield* waitUntilGone(created.name);
150 expect(gone).toEqual("gone");
151 }).pipe(logLevel),
152 {
153 tags: ["provider:gcp", "provider:gcp:billingbudgets", "live"],
154 timeout: 90_000,
155 },
156);