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 domain =
25 process.env.GCP_TEST_APPENGINE_DOMAIN ?? "alchemy-appengine.test";
26
27const waitUntilGone = (appsId: string, domainId: string) =>
28 appengine
29 .getAppsDomainMappings({
30 appsId,
31 domainMappingsId: domainId,
32 })
33 .pipe(
34 Effect.as("found" as const),
35 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
36 Effect.repeat({
37 schedule: Schedule.spaced("1 second"),
38 until: (status) => status === "gone",
39 times: 10,
40 }),
41 );
42
43test.provider(
44 "getAppsDomainMappings on a missing mapping fails with a typed tag",
45 (stack) =>
46 Effect.gen(function* () {
47 const { project } = yield* GcpEnvironment.current;
48 yield* stack.destroy();
49
50 const error = yield* Effect.flip(
51 appengine.getAppsDomainMappings({
52 appsId: project,
53 domainMappingsId: "alchemy-missing.example.com",
54 }),
55 );
56 expect(error._tag).toEqual("ServiceDisabled");
57
58 yield* stack.destroy();
59 }).pipe(logLevel),
60 { tags: ["provider:gcp", "provider:gcp:appengine", "live"], timeout: 90_000 },
61);
62
63test.provider.skipIf(runLifecycle)(
64 "createAppsDomainMappings without an App Engine app fails with a typed tag",
65 (stack) =>
66 Effect.gen(function* () {
67 const { project } = yield* GcpEnvironment.current;
68 yield* stack.destroy();
69
70 const error = yield* Effect.flip(
71 appengine.createAppsDomainMappings({
72 appsId: project,
73 body: {
74 id: "alchemy-missing.example.com",
75 sslSettings: { sslManagementType: "AUTOMATIC" },
76 },
77 }),
78 );
79 expect(error._tag).toEqual("ServiceDisabled");
80
81 yield* stack.destroy();
82 }).pipe(logLevel),
83 { tags: ["provider:gcp", "provider:gcp:appengine", "live"], timeout: 90_000 },
84);
85
86test.provider.skipIf(!runLifecycle)(
87 "create, update, and delete a domain mapping",
88 (stack) =>
89 Effect.gen(function* () {
90 const { project } = yield* GcpEnvironment.current;
91 yield* stack.destroy();
92
93 const created = yield* stack.deploy(
94 Effect.gen(function* () {
95 return yield* GCP.AppEngine.AppsDomainMapping("Www", {
96 domain,
97 sslSettings: { sslManagementType: "AUTOMATIC" },
98 });
99 }),
100 );
101
102 expect(created.domain).toEqual(domain);
103 expect(created.appsId).toEqual(project);
104
105 const fetched = yield* appengine.getAppsDomainMappings({
106 appsId: created.appsId,
107 domainMappingsId: created.domain,
108 });
109 expect(fetched.id).toEqual(created.domain);
110
111 const updated = yield* stack.deploy(
112 Effect.gen(function* () {
113 return yield* GCP.AppEngine.AppsDomainMapping("Www", {
114 domain: created.domain,
115 sslSettings: { sslManagementType: "MANUAL" },
116 });
117 }),
118 );
119
120 expect(updated.domain).toEqual(created.domain);
121 expect(updated.sslManagementType).toEqual("MANUAL");
122
123 yield* stack.destroy();
124
125 const gone = yield* waitUntilGone(created.appsId, created.domain);
126 expect(gone).toEqual("gone");
127 }).pipe(logLevel),
128 { tags: ["provider:gcp", "provider:gcp:appengine", "live"], timeout: 90_000 },
129);