1import * as integrations from "@distilled.cloud/gcp/integrations_v1";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import * as Stream from "effect/Stream";
5import { Unowned } from "../../AdoptPolicy.ts";
6import { isResolved } from "../../Diff.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { GcpEnvironment } from "../Environment.ts";
10import { createInternalLabels } from "../Labels.ts";
11import type { Providers } from "../Providers.ts";
12import {
13 encodeOwnership,
14 hasOwnershipMarker,
15 lastSegment,
16 locationOf,
17 locationParent,
18 normalizeLocation,
19 ownedByAlchemy,
20 parseOwnership,
21 sameText,
22 toResourceId,
23 updateMaskOf,
24} from "./ownership.ts";
25
26export type IntegrationsVersionsTestCasesProps = {
27 /**
28 * Parent integration version resource name
29 * `projects/{project}/locations/{location}/integrations/{integration}/versions/{version}`.
30 * Immutable — changing it replaces the test case.
31 */
32 version: string;
33 /**
34 * Test case id (the `{testCase}` segment). If omitted, a unique id is
35 * generated. Immutable — changing it replaces the test case.
36 */
37 testCaseId?: string;
38 /**
39 * Location used when `version` is a bare id. Immutable — changing it
40 * replaces the test case.
41 * @default the stack's GCP region (`GCP.Region`, else the profile
42 * region, else `us-central1`)
43 */
44 location?: string;
45 /**
46 * Display name.
47 */
48 displayName?: string;
49 /**
50 * Human-readable description. Test cases have no labels field, so
51 * Alchemy ownership is stored in a `[alchemy …]` prefix and stripped
52 * from attributes.
53 */
54 description?: string;
55 /**
56 * Trigger id in the parent version that starts this test case
57 * (`api_trigger/{name}`).
58 */
59 triggerId: string;
60};
61
62export type IntegrationsVersionsTestCases = Resource<
63 "GCP.Integrations.IntegrationsVersionsTestCases",
64 IntegrationsVersionsTestCasesProps,
65 {
66 /** Full resource name. */
67 name: string;
68 /** Test case id (last path segment). */
69 testCaseId: string;
70 /** Parent version resource name. */
71 version: string;
72 /** Location id. */
73 location: string;
74 /** Project id. */
75 project: string;
76 /** Display name. */
77 displayName: string | undefined;
78 /** User description with the Alchemy ownership prefix stripped. */
79 description: string | undefined;
80 /** Trigger id this test case starts from. */
81 triggerId: string | undefined;
82 /** RFC3339 creation timestamp. */
83 createTime: string | undefined;
84 /** RFC3339 last-update timestamp. */
85 updateTime: string | undefined;
86 },
87 never,
88 Providers
89>;
90
91/**
92 * A functional test case attached to an Application Integration version.
93 *
94 * Test cases have no labels field — Alchemy stamps ownership into the
95 * description. Parent version and test case id are immutable. Display
96 * name, description, and trigger id update in place.
97 *
98 * ### Creating a Test Case
99 * **Example:** Exercise an API trigger
100 * ```typescript
101 * const testCase = yield* GCP.Integrations.IntegrationsVersionsTestCases("HappyPath", {
102 * version: version.name,
103 * triggerId: "api_trigger/orders",
104 * displayName: "happy-path",
105 * description: "fires the API trigger",
106 * });
107 * ```
108 *
109 * @resource
110 * @category Integrations
111 */
112export const IntegrationsVersionsTestCases =
113 Resource<IntegrationsVersionsTestCases>(
114 "GCP.Integrations.IntegrationsVersionsTestCases",
115 );
116
117export class IntegrationsVersionsTestCasesNotResolved extends Data.TaggedError(
118 "GCP.Integrations.IntegrationsVersionsTestCasesNotResolved",
119)<{
120 name: string;
121}> {}
122
123const expandVersion = (value: string, project: string, location: string) =>
124 value.includes("/versions/")
125 ? value
126 : `${locationParent(project, location)}/integrations/${value}/versions/${value}`;
127
128const resourceName = (version: string, testCaseId: string) =>
129 `${version}/testCases/${testCaseId}`;
130
131const toAttrs = (
132 testCase: integrations.GoogleCloudIntegrationsV1alphaTestCase,
133 project: string,
134 region: string,
135 version: string,
136) => {
137 const name = testCase.name ?? "";
138 const parsed = parseOwnership(testCase.description);
139 return {
140 name,
141 testCaseId: lastSegment(name),
142 version,
143 location: locationOf(name, region),
144 project,
145 displayName: testCase.displayName,
146 description: parsed.text,
147 triggerId: testCase.triggerId,
148 createTime: testCase.createTime,
149 updateTime: testCase.updateTime,
150 };
151};
152
153const getByName = (name: string) =>
154 name.length === 0
155 ? Effect.succeed(undefined)
156 : integrations
157 .getProjectsLocationsIntegrationsVersionsTestCases({ name })
158 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
159
160const listAt = (parent: string, project: string, region: string) =>
161 integrations.listProjectsLocationsIntegrationsVersionsTestCases
162 .pages({ parent, pageSize: 100 })
163 .pipe(
164 Stream.flatMap((page) => Stream.fromIterable(page.testCases ?? [])),
165 Stream.filter((testCase) => hasOwnershipMarker(testCase.description)),
166 Stream.map((testCase) => toAttrs(testCase, project, region, parent)),
167 Stream.runCollect,
168 Effect.map((chunk) => Array.from(chunk)),
169 Effect.catchTag("NotFound", () => Effect.succeed([])),
170 );
171
172const findOwned = (parent: string, id: string) =>
173 integrations.listProjectsLocationsIntegrationsVersionsTestCases
174 .pages({ parent, pageSize: 100 })
175 .pipe(
176 Stream.flatMap((page) => Stream.fromIterable(page.testCases ?? [])),
177 Stream.filterEffect((testCase) =>
178 ownedByAlchemy(id, testCase.description),
179 ),
180 Stream.runHead,
181 Effect.map((option) =>
182 option._tag === "Some" ? option.value : undefined,
183 ),
184 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
185 );
186
187const listWildcard = (project: string, location: string) =>
188 listAt(
189 `${locationParent(project, location)}/integrations/-/versions/-`,
190 project,
191 location,
192 );
193
194export const IntegrationsVersionsTestCasesProvider = () =>
195 Provider.succeed(IntegrationsVersionsTestCases, {
196 stables: [
197 "name",
198 "testCaseId",
199 "version",
200 "location",
201 "project",
202 "createTime",
203 ],
204
205 diff: Effect.fn(function* ({ news, olds, output }) {
206 if (!isResolved(news)) return undefined;
207 const previousVersion = olds?.version ?? output?.version;
208 if (
209 previousVersion !== undefined &&
210 news.version !== previousVersion &&
211 !news.version.endsWith(`/${lastSegment(previousVersion)}`)
212 ) {
213 return { action: "replace" as const, deleteFirst: false };
214 }
215 const previousId = olds?.testCaseId ?? output?.testCaseId;
216 if (
217 previousId !== undefined &&
218 news.testCaseId !== undefined &&
219 news.testCaseId !== previousId
220 ) {
221 return { action: "replace" as const, deleteFirst: true };
222 }
223 return undefined;
224 }),
225
226 read: Effect.fn(function* ({ id, olds, output }) {
227 const env = yield* GcpEnvironment.current;
228 const location = normalizeLocation(
229 olds?.location ?? output?.location,
230 env.region,
231 );
232 const version = expandVersion(
233 olds?.version ?? output?.version ?? "",
234 env.project,
235 location,
236 );
237 const testCaseId = yield* toResourceId(
238 id,
239 olds?.testCaseId,
240 output?.testCaseId,
241 );
242 const name = output?.name ?? resourceName(version, testCaseId);
243 let existing = yield* getByName(name);
244 if (existing === undefined && version.length > 0) {
245 existing = yield* findOwned(version, id);
246 }
247 if (existing === undefined) return undefined;
248 const attrs = toAttrs(existing, env.project, env.region, version);
249 return (yield* ownedByAlchemy(id, existing.description))
250 ? attrs
251 : Unowned(attrs);
252 }),
253
254 list: () =>
255 Effect.gen(function* () {
256 const env = yield* GcpEnvironment.current;
257 return yield* listWildcard(env.project, env.region);
258 }),
259
260 reconcile: Effect.fn(function* ({ id, news, output }) {
261 const env = yield* GcpEnvironment.current;
262 const location = normalizeLocation(
263 news.location ?? output?.location,
264 env.region,
265 );
266 const version = expandVersion(news.version, env.project, location);
267 const testCaseId = yield* toResourceId(
268 id,
269 news.testCaseId,
270 output?.testCaseId,
271 );
272 const name = output?.name ?? resourceName(version, testCaseId);
273 const ownership = yield* createInternalLabels(id);
274 const description = encodeOwnership(ownership, news.description);
275 const displayName = news.displayName ?? testCaseId;
276
277 let current = yield* getByName(output?.name ?? name);
278 if (current === undefined) {
279 current = yield* findOwned(version, id);
280 }
281
282 if (current === undefined) {
283 const created = yield* integrations
284 .createProjectsLocationsIntegrationsVersionsTestCases({
285 parent: version,
286 testCaseId,
287 body: {
288 displayName,
289 description,
290 triggerId: news.triggerId,
291 },
292 })
293 .pipe(Effect.catchTag("Conflict", () => findOwned(version, id)));
294 current = created ?? (yield* findOwned(version, id));
295 }
296
297 if (current === undefined) {
298 return yield* new IntegrationsVersionsTestCasesNotResolved({ name });
299 }
300
301 const currentName = current.name ?? name;
302 const displayChanged = !sameText(current.displayName, displayName);
303 const descriptionChanged = (current.description ?? "") !== description;
304 const triggerChanged = !sameText(current.triggerId, news.triggerId);
305
306 if (displayChanged || descriptionChanged || triggerChanged) {
307 current =
308 yield* integrations.patchProjectsLocationsIntegrationsVersionsTestCases(
309 {
310 name: currentName,
311 updateMask: updateMaskOf(
312 displayChanged ? "display_name" : undefined,
313 descriptionChanged ? "description" : undefined,
314 triggerChanged ? "trigger_id" : undefined,
315 ),
316 body: {
317 name: currentName,
318 displayName,
319 description,
320 triggerId: news.triggerId,
321 },
322 },
323 );
324 }
325
326 return toAttrs(current, env.project, env.region, version);
327 }),
328
329 delete: Effect.fn(function* ({ output }) {
330 yield* integrations
331 .deleteProjectsLocationsIntegrationsVersionsTestCases({
332 name: output.name,
333 })
334 .pipe(Effect.catchTag("NotFound", () => Effect.void));
335 }),
336 });