1import * as GCP from "@/GCP";
2import type { StackServices } from "@/Stack";
3import * as Test from "@/Test/Alchemy";
4import * as sqladmin from "@distilled.cloud/gcp/sqladmin_v1";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";
8import { MinimumLogLevel } from "effect/References";
9import * as Schedule from "effect/Schedule";
10import { GcpEnvironment } from "@/GCP/Environment";
11
12const { test } = Test.make({
13 providers: GCP.providers() as Layer.Layer<
14 GCP.ProviderRequirements,
15 never,
16 StackServices
17 >,
18});
19
20const logLevel = Effect.provideService(
21 MinimumLogLevel,
22 process.env.DEBUG ? "Debug" : "Info",
23);
24
25// Runs against an existing Cloud SQL instance (creating one takes well over
26// 5 minutes); set GCP_SQL_INSTANCE to its name.
27const sqlInstance =
28 process.env.GCP_SQL_INSTANCE || process.env.GCP_TEST_SQL_INSTANCE;
29const runLifecycle = !!sqlInstance && !process.env.FAST;
30
31const waitUntilGone = (name: string) =>
32 sqladmin.getBackupBackups({ name }).pipe(
33 Effect.as("found" as const),
34 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
35 // Cloud SQL answers a deleted backup uid with this 403.
36 Effect.catchTag("SqlBackupAccessDenied", () =>
37 Effect.succeed("gone" as const),
38 ),
39 Effect.repeat({
40 schedule: Schedule.spaced("2 seconds"),
41 until: (status) => status === "gone",
42 times: 10,
43 }),
44 );
45
46test.provider(
47 "getBackupBackups on a missing backup fails with SqlBackupAccessDenied",
48 (stack) =>
49 Effect.gen(function* () {
50 const { project } = yield* GcpEnvironment.current;
51 yield* stack.destroy();
52
53 const error = yield* Effect.flip(
54 sqladmin.getBackupBackups({
55 name: `projects/${project}/backups/00000000-0000-0000-0000-000000000000`,
56 }),
57 );
58 // Cloud SQL hides unknown backups behind 403 rather than 404.
59 expect(error._tag).toBe("SqlBackupAccessDenied");
60
61 yield* stack.destroy();
62 }).pipe(logLevel),
63 { tags: ["provider:gcp", "provider:gcp:sql", "live"], timeout: 90_000 },
64);
65
66test.provider(
67 "lists sql backups",
68 (stack) =>
69 Effect.gen(function* () {
70 const { project } = yield* GcpEnvironment.current;
71 yield* stack.destroy();
72
73 const page = yield* sqladmin.listBackupsBackups({
74 parent: `projects/${project}`,
75 pageSize: 10,
76 });
77 expect((page.backups ?? []).map((backup) => backup.name)).not.toContain(
78 `projects/${project}/backups/00000000-0000-0000-0000-000000000000`,
79 );
80
81 yield* stack.destroy();
82 }).pipe(logLevel),
83 { tags: ["provider:gcp", "provider:gcp:sql", "live"], timeout: 90_000 },
84);
85
86test.provider.skipIf(!runLifecycle)(
87 "create, update, and delete a sql backup",
88 (stack) =>
89 Effect.gen(function* () {
90 const { project } = yield* GcpEnvironment.current;
91 yield* stack.destroy();
92
93 const instance = sqlInstance!;
94
95 const created = yield* stack.deploy(
96 Effect.gen(function* () {
97 return yield* GCP.SQL.Backup("Nightly", {
98 instance,
99 description: "alchemy-on-demand",
100 });
101 }),
102 );
103
104 expect(created.backupId).toEqual(expect.any(String));
105 expect(created.backupId.length).toBeGreaterThan(0);
106 expect(created.instance).toEqual(instance);
107 expect(created.project).toEqual(project);
108 expect(created.name).toEqual(
109 `projects/${project}/backups/${created.backupId}`,
110 );
111 expect(created.description).toEqual("alchemy-on-demand");
112
113 const fetched = yield* sqladmin.getBackupBackups({
114 name: created.name,
115 });
116 expect(fetched.name).toEqual(created.name);
117 expect(fetched.instance).toEqual(instance);
118 expect(fetched.description).toContain("alchemy-on-demand");
119
120 const updated = yield* stack.deploy(
121 Effect.gen(function* () {
122 return yield* GCP.SQL.Backup("Nightly", {
123 instance,
124 description: "alchemy-on-demand",
125 });
126 }),
127 );
128
129 expect(updated.backupId).toEqual(created.backupId);
130 expect(updated.name).toEqual(created.name);
131
132 const refetched = yield* sqladmin.getBackupBackups({
133 name: updated.name,
134 });
135 expect(refetched.name).toEqual(updated.name);
136
137 yield* stack.destroy();
138
139 const gone = yield* waitUntilGone(created.name);
140 expect(gone).toEqual("gone");
141 }).pipe(logLevel),
142 { tags: ["provider:gcp", "provider:gcp:sql", "live"], timeout: 120_000 },
143);