1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as appengine from "@distilled.cloud/gcp/appengine_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// Needs an App Engine application in the project, which is permanent once
18// created (it can never be deleted). The testing project has none and the
19// App Engine Admin API is off (ServiceDisabled: "App Engine Admin API has not been
20// used in project ... or it is disabled."). Set GCP_TEST_APPENGINE_APP=1 on a
21// project with an app to run the lifecycle.
22const runLifecycle = !!process.env.GCP_TEST_APPENGINE_APP;
23
24const waitUntilGone = (appsId: string, priority: number) =>
25 appengine
26 .getAppsFirewallIngressRules({
27 appsId,
28 ingressRulesId: String(priority),
29 })
30 .pipe(
31 Effect.as("found" as const),
32 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
33 Effect.repeat({
34 schedule: Schedule.spaced("1 second"),
35 until: (status) => status === "gone",
36 times: 10,
37 }),
38 );
39
40test.provider(
41 "getAppsFirewallIngressRules on a missing rule fails with a typed tag",
42 (stack) =>
43 Effect.gen(function* () {
44 const { project } = yield* GcpEnvironment.current;
45 yield* stack.destroy();
46
47 const error = yield* Effect.flip(
48 appengine.getAppsFirewallIngressRules({
49 appsId: project,
50 ingressRulesId: "999999998",
51 }),
52 );
53 expect(error._tag).toEqual("ServiceDisabled");
54
55 yield* stack.destroy();
56 }).pipe(logLevel),
57 { tags: ["provider:gcp", "provider:gcp:appengine", "live"], timeout: 90_000 },
58);
59
60test.provider.skipIf(runLifecycle)(
61 "createAppsFirewallIngressRules without an App Engine app 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 appengine.createAppsFirewallIngressRules({
69 appsId: project,
70 body: {
71 action: "DENY",
72 sourceRange: "203.0.113.0/24",
73 description: "Alchemy Appengine Probe",
74 priority: 12345,
75 },
76 }),
77 );
78 expect(error._tag).toEqual("ServiceDisabled");
79
80 yield* stack.destroy();
81 }).pipe(logLevel),
82 { tags: ["provider:gcp", "provider:gcp:appengine", "live"], timeout: 90_000 },
83);
84
85test.provider.skipIf(!runLifecycle)(
86 "create, update, and delete a firewall ingress rule",
87 (stack) =>
88 Effect.gen(function* () {
89 yield* stack.destroy();
90
91 const created = yield* stack.deploy(
92 Effect.gen(function* () {
93 return yield* GCP.AppEngine.AppsFirewallIngressRule("BlockOffice", {
94 action: "DENY",
95 sourceRange: "203.0.113.0/24",
96 description: "office network",
97 });
98 }),
99 );
100
101 expect(created.priority).toBeGreaterThan(0);
102 expect(created.action).toEqual("DENY");
103 expect(created.sourceRange).toEqual("203.0.113.0/24");
104 expect(created.description).toEqual("office network");
105
106 const fetched = yield* appengine.getAppsFirewallIngressRules({
107 appsId: created.appsId,
108 ingressRulesId: String(created.priority),
109 });
110 expect(fetched.priority).toEqual(created.priority);
111 expect(fetched.description).toContain("[alchemy ");
112
113 const updated = yield* stack.deploy(
114 Effect.gen(function* () {
115 return yield* GCP.AppEngine.AppsFirewallIngressRule("BlockOffice", {
116 priority: created.priority,
117 action: "ALLOW",
118 sourceRange: "203.0.113.0/24",
119 description: "office network allow",
120 });
121 }),
122 );
123
124 expect(updated.priority).toEqual(created.priority);
125 expect(updated.action).toEqual("ALLOW");
126 expect(updated.description).toEqual("office network allow");
127
128 yield* stack.destroy();
129
130 const gone = yield* waitUntilGone(created.appsId, created.priority);
131 expect(gone).toEqual("gone");
132 }).pipe(logLevel),
133 { tags: ["provider:gcp", "provider:gcp:appengine", "live"], timeout: 90_000 },
134);