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 waitUntilGone = (name: string) =>
25 apihub.getProjectsLocationsApisVersionsOperations({ name }).pipe(
26 Effect.as("found" as const),
27 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
28 Effect.repeat({
29 schedule: Schedule.spaced("1 second"),
30 until: (status) => status === "gone",
31 times: 10,
32 }),
33 );
34
35test.provider.skipIf(!runLifecycle)(
36 "getProjectsLocationsApisVersionsOperations on a missing operation fails with a typed tag",
37 (stack) =>
38 Effect.gen(function* () {
39 const { project } = yield* GcpEnvironment.current;
40 yield* stack.destroy();
41
42 const error = yield* Effect.flip(
43 apihub.getProjectsLocationsApisVersionsOperations({
44 name: `projects/${project}/locations/${location}/apis/alchemy-missing/versions/v1/operations/list`,
45 }),
46 );
47 expect(error._tag).toEqual("NotFound");
48
49 yield* stack.destroy();
50 }).pipe(logLevel),
51 { tags: ["provider:gcp", "provider:gcp:apihub", "live"], timeout: 90_000 },
52);
53
54test.provider.skipIf(runLifecycle)(
55 "getProjectsLocationsApisVersionsOperations fails with ApiHubNotProvisioned without an API hub instance",
56 (stack) =>
57 Effect.gen(function* () {
58 const { project } = yield* GcpEnvironment.current;
59 yield* stack.destroy();
60
61 const error = yield* Effect.flip(
62 apihub.getProjectsLocationsApisVersionsOperations({
63 name: `projects/${project}/locations/${location}/apis/alchemy-missing/versions/v1/operations/list`,
64 }),
65 );
66 expect(error._tag).toEqual("ApiHubNotProvisioned");
67
68 yield* stack.destroy();
69 }).pipe(logLevel),
70 { tags: ["provider:gcp", "provider:gcp:apihub", "live"], timeout: 90_000 },
71);
72
73test.provider.skipIf(!runLifecycle)(
74 "create, update, and delete an API Hub operation",
75 (stack) =>
76 Effect.gen(function* () {
77 yield* stack.destroy();
78
79 const created = yield* stack.deploy(
80 Effect.gen(function* () {
81 const api = yield* GCP.ApiHub.Api("Pets", {
82 location,
83 displayName: "pets",
84 });
85 const version = yield* GCP.ApiHub.ApisVersion("V1", {
86 api: api.name,
87 location,
88 displayName: "v1",
89 });
90 const operation = yield* GCP.ApiHub.ApisVersionsOperation(
91 "ListPets",
92 {
93 version: version.name,
94 location,
95 details: {
96 httpOperation: {
97 method: "GET",
98 path: { path: "/pets" },
99 },
100 description: "list pets",
101 },
102 },
103 );
104 return { api, version, operation };
105 }),
106 );
107
108 expect(created.operation.name).toContain("/operations/");
109 expect(created.operation.version).toEqual(created.version.name);
110 expect(created.operation.details?.httpOperation?.method).toEqual("GET");
111 expect(created.operation.details?.httpOperation?.path?.path).toEqual(
112 "/pets",
113 );
114 expect(created.operation.details?.description).toEqual("list pets");
115
116 const fetched = yield* apihub.getProjectsLocationsApisVersionsOperations({
117 name: created.operation.name,
118 });
119 expect(fetched.name).toEqual(created.operation.name);
120 expect(fetched.details?.description).toContain("alchemy-id=");
121
122 const updated = yield* stack.deploy(
123 Effect.gen(function* () {
124 const api = yield* GCP.ApiHub.Api("Pets", {
125 apiId: created.api.apiId,
126 location,
127 displayName: "pets",
128 });
129 const version = yield* GCP.ApiHub.ApisVersion("V1", {
130 api: api.name,
131 versionId: created.version.versionId,
132 location,
133 displayName: "v1",
134 });
135 const operation = yield* GCP.ApiHub.ApisVersionsOperation(
136 "ListPets",
137 {
138 version: version.name,
139 apiOperationId: created.operation.apiOperationId,
140 location,
141 details: {
142 httpOperation: {
143 method: "GET",
144 path: { path: "/animals" },
145 },
146 description: "list animals",
147 deprecated: true,
148 },
149 },
150 );
151 return { api, version, operation };
152 }),
153 );
154
155 expect(updated.operation.name).toEqual(created.operation.name);
156 expect(updated.operation.details?.httpOperation?.path?.path).toEqual(
157 "/animals",
158 );
159 expect(updated.operation.details?.description).toEqual("list animals");
160 expect(updated.operation.details?.deprecated).toEqual(true);
161
162 yield* stack.destroy();
163
164 const gone = yield* waitUntilGone(created.operation.name);
165 expect(gone).toEqual("gone");
166 }).pipe(logLevel),
167 { tags: ["provider:gcp", "provider:gcp:apihub", "live"], timeout: 90_000 },
168);