1import * as GCP from "@/GCP";
2import { GcpEnvironment } from "@/GCP/Environment";
3import * as Test from "@/Test/Alchemy";
4import * as looker from "@distilled.cloud/gcp/looker_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 location = "us-central1";
18const namesOf = (project: string) => {
19 const instanceParent = `projects/${project}/locations/${location}`;
20 const missingInstance = `${instanceParent}/instances/alchemy-missing-looker`;
21 const missingName = `${missingInstance}/backups/alchemy-missing-backup`;
22 return { instanceParent, missingInstance, missingName };
23};
24
25// Backups need an ACTIVE Looker instance, which bills hourly. Set
26// GCP_TEST_LOOKER_INSTANCE to its full resource name to run the lifecycle.
27const lookerInstance = process.env.GCP_TEST_LOOKER_INSTANCE?.trim();
28
29const waitUntilGone = (name: string) =>
30 looker.getProjectsLocationsInstancesBackups({ name }).pipe(
31 Effect.as("found" as const),
32 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
33 Effect.repeat({
34 schedule: Schedule.spaced("2 seconds"),
35 until: (status) => status === "gone",
36 times: 10,
37 }),
38 );
39
40test.provider(
41 "getProjectsLocationsInstancesBackups on a missing backup fails with a typed tag",
42 (stack) =>
43 Effect.gen(function* () {
44 const { project } = yield* GcpEnvironment.current;
45 const { missingInstance, missingName } = namesOf(project);
46 yield* stack.destroy();
47
48 const error = yield* Effect.flip(
49 looker.getProjectsLocationsInstancesBackups({ name: missingName }),
50 );
51 expect(error._tag).toEqual("NotFound");
52
53 const listError = yield* Effect.flip(
54 looker.listProjectsLocationsInstancesBackups({
55 parent: missingInstance,
56 pageSize: 10,
57 }),
58 );
59 expect(listError._tag).toEqual("NotFound");
60
61 yield* stack.destroy();
62 }).pipe(logLevel),
63 { tags: ["provider:gcp", "provider:gcp:looker", "live"], timeout: 90_000 },
64);
65
66test.provider(
67 "create against a missing Looker instance is rejected with a typed tag",
68 (stack) =>
69 Effect.gen(function* () {
70 const { project } = yield* GcpEnvironment.current;
71 const { missingInstance } = namesOf(project);
72 yield* stack.destroy();
73
74 const error = yield* Effect.flip(
75 stack.deploy(
76 Effect.gen(function* () {
77 return yield* GCP.Looker.InstancesBackup("Nightly", {
78 instance: missingInstance,
79 });
80 }),
81 ),
82 );
83 // "parent resource not found for .../instances/alchemy-missing-looker"
84 expect(error._tag).toEqual("NotFound");
85
86 yield* stack.destroy();
87 }).pipe(logLevel),
88 { tags: ["provider:gcp", "provider:gcp:looker", "live"], timeout: 90_000 },
89);
90
91test.provider.skipIf(lookerInstance === undefined || !!process.env.FAST)(
92 "create, refresh, and delete a Looker instance backup",
93 (stack) =>
94 Effect.gen(function* () {
95 const parent = lookerInstance!;
96 yield* stack.destroy();
97
98 const created = yield* stack.deploy(
99 Effect.gen(function* () {
100 return yield* GCP.Looker.InstancesBackup("Nightly", {
101 instance: parent,
102 });
103 }),
104 );
105
106 expect(created.name).toContain("/backups/");
107 expect(created.instance).toEqual(parent);
108 expect(created.location).toEqual(location);
109 expect(created.backupId).toEqual(expect.any(String));
110 expect(created.state).toEqual("ACTIVE");
111
112 const fetched = yield* looker.getProjectsLocationsInstancesBackups({
113 name: created.name,
114 });
115 expect(fetched.name).toEqual(created.name);
116 expect(fetched.state).toEqual("ACTIVE");
117
118 const refreshed = yield* stack.deploy(
119 Effect.gen(function* () {
120 return yield* GCP.Looker.InstancesBackup("Nightly", {
121 instance: parent,
122 backupId: created.backupId,
123 });
124 }),
125 );
126
127 expect(refreshed.name).toEqual(created.name);
128 expect(refreshed.backupId).toEqual(created.backupId);
129 expect(refreshed.state).toEqual("ACTIVE");
130
131 yield* stack.destroy();
132 const gone = yield* waitUntilGone(created.name);
133 expect(gone).toEqual("gone");
134 }).pipe(logLevel),
135 { tags: ["provider:gcp", "provider:gcp:looker", "live"], timeout: 120_000 },
136);