1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as cloudsupport from "@distilled.cloud/gcp/cloudsupport_v2";
4import * as resourcemanager from "@distilled.cloud/gcp/cloudresourcemanager_v3";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import { MinimumLogLevel } from "effect/References";
8import * as Schedule from "effect/Schedule";
9import { GcpEnvironment } from "@/GCP/Environment";
10
11const { test } = Test.make({ providers: GCP.providers() });
12
13const logLevel = Effect.provideService(
14 MinimumLogLevel,
15 process.env.DEBUG ? "Debug" : "Info",
16);
17
18// Support event subscriptions need an organization-level Cloud Support role
19// (Cloud Customer Care). The testing credentials hold none, so every call is
20// rejected with Forbidden ("Permission 'cloudsupport.supportSubscriptions.get'
21// denied ..."). Set GCP_TEST_CLOUDSUPPORT=1 on an entitled org to run the
22// lifecycle.
23const entitled = process.env.GCP_TEST_CLOUDSUPPORT === "1";
24const runLifecycle = entitled && !process.env.FAST;
25
26const waitUntilGone = (name: string) =>
27 cloudsupport.getOrganizationsSupportEventSubscriptions({ name }).pipe(
28 Effect.map((subscription) =>
29 subscription.state === "DELETED" ? ("gone" as const) : ("found" as const),
30 ),
31 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
32 Effect.repeat({
33 schedule: Schedule.spaced("1 second"),
34 until: (status) => status === "gone",
35 times: 10,
36 }),
37 );
38
39/** The organization that owns the testing project. */
40const organizationOf = () =>
41 Effect.gen(function* () {
42 const { project } = yield* GcpEnvironment.current;
43 const resource = yield* resourcemanager.getProjects({
44 name: `projects/${project}`,
45 });
46 return resource.parent ?? "";
47 });
48
49test.provider(
50 "getSupportEventSubscriptions on a missing subscription fails with a typed tag",
51 (stack) =>
52 Effect.gen(function* () {
53 yield* stack.destroy();
54
55 const organization = yield* organizationOf();
56 const error = yield* Effect.flip(
57 cloudsupport.getOrganizationsSupportEventSubscriptions({
58 name: `${organization}/supportEventSubscriptions/alchemy-missing`,
59 }),
60 );
61 expect(error._tag).toEqual(entitled ? "NotFound" : "Forbidden");
62
63 yield* stack.destroy();
64 }).pipe(logLevel),
65 {
66 tags: ["provider:gcp", "provider:gcp:cloudsupport", "live"],
67 timeout: 90_000,
68 },
69);
70
71test.provider.skipIf(entitled)(
72 "createSupportEventSubscriptions without Cloud Support access fails with Forbidden",
73 (stack) =>
74 Effect.gen(function* () {
75 const { project } = yield* GcpEnvironment.current;
76 yield* stack.destroy();
77
78 const organization = yield* organizationOf();
79 const error = yield* Effect.flip(
80 cloudsupport.createOrganizationsSupportEventSubscriptions({
81 parent: organization,
82 body: {
83 pubSubTopic: `projects/${project}/topics/alchemy-missing-topic`,
84 },
85 }),
86 );
87 expect(error._tag).toEqual("Forbidden");
88
89 yield* stack.destroy();
90 }).pipe(logLevel),
91 {
92 tags: ["provider:gcp", "provider:gcp:cloudsupport", "live"],
93 timeout: 90_000,
94 },
95);
96
97test.provider.skipIf(!runLifecycle)(
98 "create, update, and delete a support event subscription",
99 (stack) =>
100 Effect.gen(function* () {
101 yield* stack.destroy();
102
103 const organization = yield* organizationOf();
104 expect(organization.length).toBeGreaterThan(0);
105
106 const created = yield* stack.deploy(
107 Effect.gen(function* () {
108 const topic = yield* GCP.PubSub.Topic("SupportEventsA", {});
109 const subscription = yield* GCP.CloudSupport.SupportEventSubscription(
110 "Events",
111 {
112 organization,
113 pubSubTopic: topic.name,
114 },
115 );
116 return { topic, subscription };
117 }),
118 );
119
120 expect(created.subscription.name).toContain(
121 "/supportEventSubscriptions/",
122 );
123 expect(created.subscription.subscriptionId.length).toBeGreaterThan(0);
124 expect(created.subscription.organization).toEqual(organization);
125 expect(created.subscription.pubSubTopic).toEqual(created.topic.name);
126
127 const fetched =
128 yield* cloudsupport.getOrganizationsSupportEventSubscriptions({
129 name: created.subscription.name,
130 });
131 expect(fetched.name).toEqual(created.subscription.name);
132 expect(fetched.pubSubTopic).toEqual(created.topic.name);
133
134 const updated = yield* stack.deploy(
135 Effect.gen(function* () {
136 const topicA = yield* GCP.PubSub.Topic("SupportEventsA", {});
137 const topicB = yield* GCP.PubSub.Topic("SupportEventsB", {});
138 const subscription = yield* GCP.CloudSupport.SupportEventSubscription(
139 "Events",
140 {
141 organization,
142 subscriptionId: created.subscription.subscriptionId,
143 pubSubTopic: topicB.name,
144 },
145 );
146 return { topicA, topicB, subscription };
147 }),
148 );
149
150 expect(updated.subscription.name).toEqual(created.subscription.name);
151 expect(updated.subscription.pubSubTopic).toEqual(updated.topicB.name);
152
153 yield* stack.destroy();
154
155 const gone = yield* waitUntilGone(created.subscription.name);
156 expect(gone).toEqual("gone");
157 }).pipe(logLevel),
158 {
159 tags: ["provider:gcp", "provider:gcp:cloudsupport", "live"],
160 timeout: 90_000,
161 },
162);