1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as apphub from "@distilled.cloud/gcp/apphub_v1";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8import { GcpEnvironment } from "@/GCP/Environment";
9
10const { test } = Test.make({ providers: GCP.providers() });
11
12const logLevel = Effect.provideService(
13 MinimumLogLevel,
14 process.env.DEBUG ? "Debug" : "Info",
15);
16
17// App Hub is not enabled in the test project; live calls fail with
18// ServiceDisabled:
19// "App Hub API has not been used in project alchemy-gcp-testing-83661
20// before or it is disabled. Enable it by visiting
21// https://console.developers.google.com/apis/api/apphub.googleapis.com/overview?project=alchemy-gcp-testing-83661"
22const runLifecycle = !process.env.FAST && process.env.GCP_TEST_APPHUB === "1";
23const location = "us-central1";
24
25const waitUntilGone = (name: string) =>
26 apphub.getProjectsLocationsApplications({ name }).pipe(
27 Effect.as("found" as const),
28 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
29 Effect.repeat({
30 schedule: Schedule.spaced("1 second"),
31 until: (status) => status === "gone",
32 times: 10,
33 }),
34 );
35
36test.provider.skipIf(runLifecycle)(
37 "createProjectsLocationsApplications fails with ServiceDisabled when App Hub is disabled",
38 (stack) =>
39 Effect.gen(function* () {
40 const { project } = yield* GcpEnvironment.current;
41 yield* stack.destroy();
42
43 const error = yield* Effect.flip(
44 apphub.createProjectsLocationsApplications({
45 parent: `projects/${project}/locations/${location}`,
46 applicationId: "alchemy-apphub-probe",
47 body: {
48 displayName: "probe",
49 scope: { type: "REGIONAL" },
50 },
51 }),
52 );
53 expect(error._tag).toEqual("ServiceDisabled");
54
55 yield* stack.destroy();
56 }).pipe(logLevel),
57 { tags: ["provider:gcp", "provider:gcp:apphub", "live"], timeout: 90_000 },
58);
59
60test.provider.skipIf(runLifecycle)(
61 "getProjectsLocationsApplications on a missing application fails with a typed tag",
62 (stack) =>
63 Effect.gen(function* () {
64 const { project } = yield* GcpEnvironment.current;
65 yield* stack.destroy();
66
67 const error = yield* Effect.flip(
68 apphub.getProjectsLocationsApplications({
69 name: `projects/${project}/locations/${location}/applications/alchemy-missing-app`,
70 }),
71 );
72 expect(error._tag).toEqual("ServiceDisabled");
73
74 yield* stack.destroy();
75 }).pipe(logLevel),
76 { tags: ["provider:gcp", "provider:gcp:apphub", "live"], timeout: 90_000 },
77);
78
79test.provider.skipIf(!runLifecycle)(
80 "create, update, and delete an App Hub application",
81 (stack) =>
82 Effect.gen(function* () {
83 const { project } = yield* GcpEnvironment.current;
84 yield* stack.destroy();
85
86 const created = yield* stack.deploy(
87 Effect.gen(function* () {
88 return yield* GCP.AppHub.Application("Checkout", {
89 location,
90 displayName: "checkout",
91 description: "payments",
92 scope: { type: "REGIONAL" },
93 attributes: {
94 criticality: { type: "MEDIUM" },
95 environment: { type: "DEVELOPMENT" },
96 },
97 });
98 }),
99 );
100
101 expect(created.applicationId).toEqual(expect.any(String));
102 expect(created.name).toEqual(
103 `projects/${project}/locations/${location}/applications/${created.applicationId}`,
104 );
105 expect(created.location).toEqual(location);
106 expect(created.displayName).toEqual("checkout");
107 expect(created.description).toEqual("payments");
108 expect(created.scope?.type).toEqual("REGIONAL");
109 expect(created.attributes?.criticality?.type).toEqual("MEDIUM");
110
111 const fetched = yield* apphub.getProjectsLocationsApplications({
112 name: created.name,
113 });
114 expect(fetched.name).toEqual(created.name);
115 expect(fetched.displayName).toEqual("checkout");
116 expect(fetched.description).toContain("alchemy-id=");
117 expect(fetched.description).toContain("payments");
118 expect(fetched.scope?.type).toEqual("REGIONAL");
119
120 const updated = yield* stack.deploy(
121 Effect.gen(function* () {
122 return yield* GCP.AppHub.Application("Checkout", {
123 applicationId: created.applicationId,
124 location,
125 displayName: "checkout-v2",
126 description: "payments v2",
127 scope: { type: "REGIONAL" },
128 attributes: {
129 criticality: { type: "HIGH" },
130 environment: { type: "TEST" },
131 },
132 });
133 }),
134 );
135
136 expect(updated.name).toEqual(created.name);
137 expect(updated.displayName).toEqual("checkout-v2");
138 expect(updated.description).toEqual("payments v2");
139 expect(updated.attributes?.criticality?.type).toEqual("HIGH");
140 expect(updated.attributes?.environment?.type).toEqual("TEST");
141
142 yield* stack.destroy();
143 const gone = yield* waitUntilGone(created.name);
144 expect(gone).toEqual("gone");
145 }).pipe(logLevel),
146 { tags: ["provider:gcp", "provider:gcp:apphub", "live"], timeout: 90_000 },
147);