1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as apihub from "@distilled.cloud/gcp/apihub_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 a provisioned API Hub instance in us-central1 (one per project,
18// behind a host project registration); without one writes fail with
19// BadRequest ("Invalid resource state … API Hub instance …"). Set
20// GCP_TEST_APIHUB_INSTANCE=1 when the hub exists.
21const runLifecycle = !!process.env.GCP_TEST_APIHUB_INSTANCE;
22const location = "us-central1";
23
24const openApiV1 = `openapi: 3.0.0
25info:
26 title: pets
27 version: 1.0.0
28paths:
29 /pets:
30 get:
31 operationId: listPets
32 responses:
33 "200":
34 description: ok
35`;
36
37const openApiV2 = `openapi: 3.0.0
38info:
39 title: pets
40 version: 2.0.0
41paths:
42 /pets:
43 get:
44 operationId: listPets
45 responses:
46 "200":
47 description: ok
48 /pets/{id}:
49 get:
50 operationId: getPet
51 parameters:
52 - name: id
53 in: path
54 required: true
55 schema:
56 type: string
57 responses:
58 "200":
59 description: ok
60`;
61
62const waitUntilGone = (name: string) =>
63 apihub.getProjectsLocationsApisVersionsSpecs({ name }).pipe(
64 Effect.as("found" as const),
65 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
66 Effect.repeat({
67 schedule: Schedule.spaced("1 second"),
68 until: (status) => status === "gone",
69 times: 10,
70 }),
71 );
72
73test.provider(
74 "getProjectsLocationsApisVersionsSpecs on a missing spec fails with a typed tag",
75 (stack) =>
76 Effect.gen(function* () {
77 const { project } = yield* GcpEnvironment.current;
78 yield* stack.destroy();
79
80 const error = yield* Effect.flip(
81 apihub.getProjectsLocationsApisVersionsSpecs({
82 name: `projects/${project}/locations/${location}/apis/alchemy-missing/versions/v1/specs/openapi`,
83 }),
84 );
85 expect(error._tag).toEqual("NotFound");
86
87 yield* stack.destroy();
88 }).pipe(logLevel),
89 { tags: ["provider:gcp", "provider:gcp:apihub", "live"], timeout: 90_000 },
90);
91
92test.provider.skipIf(!runLifecycle)(
93 "create, update, and delete an API Hub spec",
94 (stack) =>
95 Effect.gen(function* () {
96 yield* stack.destroy();
97
98 const created = yield* stack.deploy(
99 Effect.gen(function* () {
100 const api = yield* GCP.ApiHub.Api("Pets", {
101 location,
102 displayName: "pets",
103 });
104 const version = yield* GCP.ApiHub.ApisVersion("V1", {
105 api: api.name,
106 location,
107 displayName: "v1",
108 });
109 const spec = yield* GCP.ApiHub.ApisVersionsSpec("OpenApi", {
110 version: version.name,
111 location,
112 displayName: "openapi.yaml",
113 contents: {
114 contents: openApiV1,
115 mimeType: "application/yaml",
116 },
117 });
118 return { api, version, spec };
119 }),
120 );
121
122 expect(created.spec.name).toContain("/specs/");
123 expect(created.spec.version).toEqual(created.version.name);
124 expect(created.spec.displayName).toEqual("openapi.yaml");
125
126 const fetched = yield* apihub.getProjectsLocationsApisVersionsSpecs({
127 name: created.spec.name,
128 });
129 expect(fetched.name).toEqual(created.spec.name);
130 expect(fetched.displayName).toContain("alchemy-");
131
132 const updated = yield* stack.deploy(
133 Effect.gen(function* () {
134 const api = yield* GCP.ApiHub.Api("Pets", {
135 apiId: created.api.apiId,
136 location,
137 displayName: "pets",
138 });
139 const version = yield* GCP.ApiHub.ApisVersion("V1", {
140 api: api.name,
141 versionId: created.version.versionId,
142 location,
143 displayName: "v1",
144 });
145 const spec = yield* GCP.ApiHub.ApisVersionsSpec("OpenApi", {
146 version: version.name,
147 specId: created.spec.specId,
148 location,
149 displayName: "openapi-v2.yaml",
150 contents: {
151 contents: openApiV2,
152 mimeType: "application/yaml",
153 },
154 });
155 return { api, version, spec };
156 }),
157 );
158
159 expect(updated.spec.name).toEqual(created.spec.name);
160 expect(updated.spec.displayName).toEqual("openapi-v2.yaml");
161
162 yield* stack.destroy();
163
164 const gone = yield* waitUntilGone(created.spec.name);
165 expect(gone).toEqual("gone");
166 }).pipe(logLevel),
167 { tags: ["provider:gcp", "provider:gcp:apihub", "live"], timeout: 90_000 },
168);