1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as servicemanagement from "@distilled.cloud/gcp/servicemanagement_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
17const missingNameOf = (project: string) =>
18 `alch-missing.endpoints.${project}.cloud.goog`;
19
20const waitUntilGone = (serviceName: string) =>
21 servicemanagement.getServices({ serviceName }).pipe(
22 Effect.as("found" as const),
23 // Service Management answers a missing service with 403 "not found or
24 // permission denied", typed ServiceNotFound.
25 Effect.catchTag(["NotFound", "ServiceNotFound"], () =>
26 Effect.succeed("gone" as const),
27 ),
28 Effect.repeat({
29 schedule: Schedule.spaced("2 seconds"),
30 until: (status) => status === "gone",
31 times: 10,
32 }),
33 );
34
35test.provider(
36 "getServices on a missing service fails with a typed tag",
37 (stack) =>
38 Effect.gen(function* () {
39 const { project } = yield* GcpEnvironment.current;
40 const missingName = missingNameOf(project);
41 yield* stack.destroy();
42
43 const error = yield* Effect.flip(
44 servicemanagement.getServices({ serviceName: missingName }),
45 );
46 expect(error._tag).toEqual("ServiceNotFound");
47
48 const page = yield* servicemanagement.listServices({
49 producerProjectId: project,
50 pageSize: 10,
51 });
52 expect(
53 (page.services ?? []).map((service) => service.serviceName),
54 ).not.toContain(missingName);
55
56 yield* stack.destroy();
57 }).pipe(logLevel),
58 {
59 tags: ["provider:gcp", "provider:gcp:servicemanagement", "live"],
60 timeout: 90_000,
61 },
62);
63
64test.provider.skipIf(!!process.env.FAST)(
65 "create, update, and delete a managed service",
66 (stack) =>
67 Effect.gen(function* () {
68 const { project } = yield* GcpEnvironment.current;
69 yield* stack.destroy();
70
71 const created = yield* stack.deploy(
72 Effect.gen(function* () {
73 return yield* GCP.ServiceManagement.Service("Hello", {
74 title: "Alchemy SM",
75 });
76 }),
77 );
78
79 // A generated name: deleted names stay reserved for 30 days.
80 expect(created.serviceName).toMatch(
81 new RegExp(`^alch-.*\\.endpoints\\.${project}\\.cloud\\.goog$`),
82 );
83 expect(created.producerProjectId).toEqual(project);
84 expect(created.title).toEqual("Alchemy SM");
85
86 const fetched = yield* servicemanagement.getServices({
87 serviceName: created.serviceName,
88 });
89 expect(fetched.serviceName).toEqual(created.serviceName);
90 expect(fetched.producerProjectId).toEqual(project);
91
92 const config = yield* servicemanagement.listServicesConfigs({
93 serviceName: created.serviceName,
94 pageSize: 1,
95 });
96 // The title is shown in the API docs: no ownership marker.
97 expect(config.serviceConfigs?.[0]?.title).toEqual("Alchemy SM");
98
99 const updated = yield* stack.deploy(
100 Effect.gen(function* () {
101 return yield* GCP.ServiceManagement.Service("Hello", {
102 title: "Alchemy SM v2",
103 });
104 }),
105 );
106
107 expect(updated.serviceName).toEqual(created.serviceName);
108 expect(updated.title).toEqual("Alchemy SM v2");
109
110 yield* stack.destroy();
111
112 const gone = yield* waitUntilGone(created.serviceName);
113 expect(gone).toEqual("gone");
114 }).pipe(logLevel),
115 {
116 tags: ["provider:gcp", "provider:gcp:servicemanagement", "live"],
117 timeout: 600_000,
118 },
119);