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 endpointOf = (project: string) => ({
25 applicationIntegrationEndpointDetails: {
26 uri: `https://integrations.googleapis.com/v1/projects/${project}/locations/${location}/integrations/alchemy-curate:execute`,
27 triggerId: "api_trigger/alchemy-curate",
28 },
29});
30
31const waitUntilGone = (name: string) =>
32 apihub.getProjectsLocationsCurations({ name }).pipe(
33 Effect.as("found" as const),
34 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
35 Effect.repeat({
36 schedule: Schedule.spaced("1 second"),
37 until: (status) => status === "gone",
38 times: 10,
39 }),
40 );
41
42test.provider(
43 "getProjectsLocationsCurations on a missing curation fails with a typed tag",
44 (stack) =>
45 Effect.gen(function* () {
46 const { project } = yield* GcpEnvironment.current;
47 yield* stack.destroy();
48
49 const error = yield* Effect.flip(
50 apihub.getProjectsLocationsCurations({
51 name: `projects/${project}/locations/${location}/curations/alchemy-missing-curation`,
52 }),
53 );
54 expect(error._tag).toEqual("NotFound");
55
56 yield* stack.destroy();
57 }).pipe(logLevel),
58 { tags: ["provider:gcp", "provider:gcp:apihub", "live"], timeout: 90_000 },
59);
60
61test.provider.skipIf(!runLifecycle)(
62 "create, update, and delete an API Hub curation",
63 (stack) =>
64 Effect.gen(function* () {
65 const { project } = yield* GcpEnvironment.current;
66 const endpoint = endpointOf(project);
67 yield* stack.destroy();
68
69 const created = yield* stack.deploy(
70 Effect.gen(function* () {
71 return yield* GCP.ApiHub.Curation("Curate", {
72 location,
73 displayName: "curate-apis",
74 description: "curate metadata",
75 endpoint,
76 });
77 }),
78 );
79
80 expect(created.name).toContain("/curations/");
81 expect(created.curationId).toEqual(expect.any(String));
82 expect(created.displayName).toEqual("curate-apis");
83 expect(created.description).toEqual("curate metadata");
84 expect(
85 created.endpoint?.applicationIntegrationEndpointDetails.triggerId,
86 ).toEqual("api_trigger/alchemy-curate");
87
88 const fetched = yield* apihub.getProjectsLocationsCurations({
89 name: created.name,
90 });
91 expect(fetched.name).toEqual(created.name);
92 expect(fetched.description).toContain("alchemy-id=");
93
94 const updated = yield* stack.deploy(
95 Effect.gen(function* () {
96 return yield* GCP.ApiHub.Curation("Curate", {
97 curationId: created.curationId,
98 location,
99 displayName: "curate-apis-v2",
100 description: "curate metadata v2",
101 endpoint,
102 });
103 }),
104 );
105
106 expect(updated.name).toEqual(created.name);
107 expect(updated.displayName).toEqual("curate-apis-v2");
108 expect(updated.description).toEqual("curate metadata v2");
109
110 yield* stack.destroy();
111
112 const gone = yield* waitUntilGone(created.name);
113 expect(gone).toEqual("gone");
114 }).pipe(logLevel),
115 { tags: ["provider:gcp", "provider:gcp:apihub", "live"], timeout: 90_000 },
116);