1import { GcpEnvironment } from "@/GCP/Environment";
2import * as GCP from "@/GCP";
3import * as Test from "@/Test/Alchemy";
4import * as backupdr from "@distilled.cloud/gcp/backupdr_v1";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import { MinimumLogLevel } from "effect/References";
8import * as Schedule from "effect/Schedule";
9
10const { test } = Test.make({ providers: GCP.providers() });
11
12const logLevel = Effect.provideService(
13 MinimumLogLevel,
14 process.env.DEBUG ? "Debug" : "Info",
15);
16
17const dailyRule = {
18 ruleId: "daily",
19 backupRetentionDays: 1,
20 standardSchedule: {
21 recurrenceType: "DAILY" as const,
22 timeZone: "UTC",
23 backupWindow: { startHourOfDay: 1, endHourOfDay: 5 },
24 },
25};
26
27const waitUntilGone = (name: string) =>
28 backupdr.getProjectsLocationsBackupPlans({ name }).pipe(
29 Effect.as("found" as const),
30 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
31 Effect.repeat({
32 schedule: Schedule.spaced("2 seconds"),
33 until: (status) => status === "gone",
34 times: 10,
35 }),
36 );
37
38test.provider(
39 "getProjectsLocationsBackupPlans on a missing plan fails with a typed tag",
40 (stack) =>
41 Effect.gen(function* () {
42 const { project } = yield* GcpEnvironment.current;
43
44 yield* stack.destroy();
45
46 const error = yield* Effect.flip(
47 backupdr.getProjectsLocationsBackupPlans({
48 name: `projects/${project}/locations/us-central1/backupPlans/alchemy-backupdr-missing`,
49 }),
50 );
51 expect(error._tag).toEqual("NotFound");
52
53 const page = yield* backupdr.listProjectsLocationsBackupPlans({
54 parent: `projects/${project}/locations/-`,
55 pageSize: 10,
56 });
57 expect((page.backupPlans ?? []).map((item) => item.name)).not.toContain(
58 `projects/${project}/locations/us-central1/backupPlans/alchemy-backupdr-missing`,
59 );
60
61 yield* stack.destroy();
62 }).pipe(logLevel),
63 { tags: ["provider:gcp", "provider:gcp:backupdr", "live"], timeout: 90_000 },
64);
65
66test.provider(
67 "create against a missing vault is rejected with a typed tag",
68 (stack) =>
69 Effect.gen(function* () {
70 const { project } = yield* GcpEnvironment.current;
71
72 yield* stack.destroy();
73
74 const error = yield* Effect.flip(
75 stack.deploy(
76 Effect.gen(function* () {
77 return yield* GCP.BackupDR.BackupPlan("Nightly", {
78 backupVault: `projects/${project}/locations/us-central1/backupVaults/alchemy-backupdr-missing`,
79 resourceType: "compute.googleapis.com/Instance",
80 backupRules: [dailyRule],
81 description: "alchemy-test-plan",
82 labels: { env: "test" },
83 });
84 }),
85 ),
86 );
87 expect(error._tag).toEqual("BackupVaultReferenceNotFound");
88
89 yield* stack.destroy();
90 }).pipe(logLevel),
91 { tags: ["provider:gcp", "provider:gcp:backupdr", "live"], timeout: 90_000 },
92);
93
94test.provider(
95 "create, update, and delete a backup plan",
96 (stack) =>
97 Effect.gen(function* () {
98 yield* stack.destroy();
99
100 const created = yield* stack.deploy(
101 Effect.gen(function* () {
102 const vault = yield* GCP.BackupDR.BackupVault("Vault", {
103 backupMinimumEnforcedRetentionDuration: "86400s",
104 description: "alchemy-test-plan-vault",
105 labels: { env: "test" },
106 });
107 return yield* GCP.BackupDR.BackupPlan("Nightly", {
108 backupVault: vault.name,
109 resourceType: "compute.googleapis.com/Instance",
110 backupRules: [dailyRule],
111 description: "alchemy-test-plan",
112 labels: { env: "test" },
113 });
114 }),
115 );
116
117 expect(created.name).toContain("/backupPlans/");
118 expect(created.backupPlanId).toEqual(expect.any(String));
119 expect(created.location).toEqual("us-central1");
120 expect(created.resourceType).toEqual("compute.googleapis.com/Instance");
121 expect(created.description).toEqual("alchemy-test-plan");
122 expect(created.labels).toMatchObject({ env: "test" });
123
124 const fetched = yield* backupdr.getProjectsLocationsBackupPlans({
125 name: created.name,
126 });
127 expect(fetched.name).toEqual(created.name);
128 expect(fetched.description).toEqual("alchemy-test-plan");
129 expect(fetched.labels?.env).toEqual("test");
130
131 const updated = yield* stack.deploy(
132 Effect.gen(function* () {
133 const vault = yield* GCP.BackupDR.BackupVault("Vault", {
134 backupVaultId: created.backupVault
135 ? created.backupVault.split("/").pop()
136 : undefined,
137 backupMinimumEnforcedRetentionDuration: "86400s",
138 description: "alchemy-test-plan-vault",
139 labels: { env: "test" },
140 });
141 return yield* GCP.BackupDR.BackupPlan("Nightly", {
142 backupPlanId: created.backupPlanId,
143 backupVault: vault.name,
144 resourceType: "compute.googleapis.com/Instance",
145 backupRules: [dailyRule],
146 description: "alchemy-prod-plan",
147 labels: { env: "test" },
148 });
149 }),
150 );
151
152 expect(updated.name).toEqual(created.name);
153 expect(updated.description).toEqual("alchemy-prod-plan");
154
155 const refetched = yield* backupdr.getProjectsLocationsBackupPlans({
156 name: created.name,
157 });
158 expect(refetched.description).toEqual("alchemy-prod-plan");
159
160 yield* stack.destroy();
161
162 const gone = yield* waitUntilGone(created.name);
163 expect(gone).toEqual("gone");
164 }).pipe(logLevel),
165 { tags: ["provider:gcp", "provider:gcp:backupdr", "live"], timeout: 300_000 },
166);