1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as ssm from "@distilled.cloud/gcp/securesourcemanager_v1";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as Schedule from "effect/Schedule";
7import {
8 currentProject,
9 logLevel,
10 missingRepoOf,
11 runLifecycle,
12} from "./common.ts";
13
14const { test } = Test.make({ providers: GCP.providers() });
15
16const waitUntilGone = (name: string) =>
17 ssm.getProjectsLocationsRepositoriesHooks({ name }).pipe(
18 Effect.as("found" as const),
19 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
20 Effect.repeat({
21 schedule: Schedule.spaced("2 seconds"),
22 until: (status) => status === "gone",
23 times: 10,
24 }),
25 );
26
27test.provider(
28 "getProjectsLocationsRepositoriesHooks on a missing hook fails with a typed tag",
29 (stack) =>
30 Effect.gen(function* () {
31 const project = yield* currentProject;
32 const missingRepo = missingRepoOf(project);
33 yield* stack.destroy();
34
35 const error = yield* Effect.flip(
36 ssm.getProjectsLocationsRepositoriesHooks({
37 name: `${missingRepo}/hooks/alchemy-missing-hook`,
38 }),
39 );
40 expect(error._tag).toEqual("NotFound");
41
42 yield* stack.destroy();
43 }).pipe(logLevel),
44 {
45 tags: ["provider:gcp", "provider:gcp:securesourcemanager", "live"],
46 timeout: 90_000,
47 },
48);
49
50test.provider(
51 "create against a missing repository is rejected with a typed tag",
52 (stack) =>
53 Effect.gen(function* () {
54 const project = yield* currentProject;
55 const missingRepo = missingRepoOf(project);
56 yield* stack.destroy();
57
58 const error = yield* Effect.flip(
59 stack.deploy(
60 Effect.gen(function* () {
61 return yield* GCP.SecureSourceManager.RepositoriesHook("Notify", {
62 repository: missingRepo,
63 targetUri: "https://example.com/hooks/ssm",
64 events: ["PUSH"],
65 });
66 }),
67 ),
68 );
69 expect(error._tag).toEqual("NotFound");
70
71 yield* stack.destroy();
72 }).pipe(logLevel),
73 {
74 tags: ["provider:gcp", "provider:gcp:securesourcemanager", "live"],
75 timeout: 90_000,
76 },
77);
78
79test.provider.skipIf(!runLifecycle)(
80 "create, update, and delete a hook",
81 (stack) =>
82 Effect.gen(function* () {
83 const project = yield* currentProject;
84 const missingRepo = missingRepoOf(project);
85 yield* stack.destroy();
86
87 const created = yield* stack.deploy(
88 Effect.gen(function* () {
89 return yield* GCP.SecureSourceManager.RepositoriesHook("Notify", {
90 repository:
91 process.env.GCP_TEST_SECURE_SOURCE_MANAGER_REPO ?? missingRepo,
92 targetUri: "https://example.com/hooks/ssm",
93 events: ["PUSH"],
94 });
95 }),
96 );
97
98 expect(created.name).toContain("/hooks/");
99 expect(created.targetUri).toEqual("https://example.com/hooks/ssm");
100 expect(created.events).toContain("PUSH");
101 expect(created.disabled).toEqual(false);
102
103 const fetched = yield* ssm.getProjectsLocationsRepositoriesHooks({
104 name: created.name,
105 });
106 expect(fetched.name).toEqual(created.name);
107 expect(fetched.targetUri).toEqual("https://example.com/hooks/ssm");
108
109 const updated = yield* stack.deploy(
110 Effect.gen(function* () {
111 return yield* GCP.SecureSourceManager.RepositoriesHook("Notify", {
112 repository: created.repository,
113 hookId: created.hookId,
114 targetUri: "https://example.com/hooks/ssm-v2",
115 events: ["PUSH", "PULL_REQUEST"],
116 disabled: true,
117 });
118 }),
119 );
120
121 expect(updated.hookId).toEqual(created.hookId);
122 expect(updated.targetUri).toEqual("https://example.com/hooks/ssm-v2");
123 expect(updated.disabled).toEqual(true);
124 expect(updated.events).toEqual(["PULL_REQUEST", "PUSH"]);
125
126 yield* stack.destroy();
127
128 const gone = yield* waitUntilGone(created.name);
129 expect(gone).toEqual("gone");
130 }).pipe(logLevel),
131 {
132 tags: ["provider:gcp", "provider:gcp:securesourcemanager", "live"],
133 timeout: 120_000,
134 },
135);