1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as iap from "@distilled.cloud/gcp/iap_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// IAP OAuth clients hang off an OAuth brand, which only an org-internal project
18// with a Workspace support email can create (createProjectsBrands otherwise
19// fails with BadRequest "invalid argument", see probe). Set GCP_TEST_IAP_BRAND
20// to an existing brand name (projects/{number}/brands/{id}) to run it.
21const brandName = process.env.GCP_TEST_IAP_BRAND?.trim();
22
23const waitUntilGone = (name: string) =>
24 iap.getProjectsBrandsIdentityAwareProxyClients({ name }).pipe(
25 Effect.as("found" as const),
26 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
27 Effect.repeat({
28 schedule: Schedule.spaced("1 second"),
29 until: (status) => status === "gone",
30 times: 10,
31 }),
32 );
33
34test.provider(
35 "getProjectsBrandsIdentityAwareProxyClients on a missing client fails with a typed tag",
36 (stack) =>
37 Effect.gen(function* () {
38 const { project } = yield* GcpEnvironment.current;
39 yield* stack.destroy();
40
41 const error = yield* Effect.flip(
42 iap.getProjectsBrandsIdentityAwareProxyClients({
43 name: `projects/${project}/brands/1/identityAwareProxyClients/missing`,
44 }),
45 );
46 expect(error._tag).toEqual("NotFound");
47
48 yield* stack.destroy();
49 }).pipe(logLevel),
50 { tags: ["provider:gcp", "provider:gcp:iap", "live"], timeout: 90_000 },
51);
52
53test.provider.skipIf(!!brandName)(
54 "createProjectsBrands without a Workspace support email fails with a typed tag",
55 (stack) =>
56 Effect.gen(function* () {
57 const { project } = yield* GcpEnvironment.current;
58 yield* stack.destroy();
59
60 const error = yield* Effect.flip(
61 iap.createProjectsBrands({
62 parent: `projects/${project}`,
63 body: {
64 applicationTitle: "Alchemy IAP probe",
65 supportEmail: "iap@example.com",
66 },
67 }),
68 );
69 expect(error._tag).toEqual("BadRequest");
70 expect(error.message).toContain("invalid argument");
71
72 yield* stack.destroy();
73 }).pipe(logLevel),
74 { tags: ["provider:gcp", "provider:gcp:iap", "live"], timeout: 90_000 },
75);
76
77test.provider.skipIf(!!brandName)(
78 "createProjectsBrandsIdentityAwareProxyClients without a Workspace brand fails with a typed tag",
79 (stack) =>
80 Effect.gen(function* () {
81 const { project } = yield* GcpEnvironment.current;
82 yield* stack.destroy();
83
84 const error = yield* Effect.flip(
85 iap.createProjectsBrandsIdentityAwareProxyClients({
86 parent: `projects/${project}/brands/missing`,
87 body: { displayName: "Alchemy IAP client probe" },
88 }),
89 );
90 expect(error._tag).toEqual("BadRequest");
91 expect(error.message).toContain(
92 "Unable to parse project number and brand",
93 );
94
95 yield* stack.destroy();
96 }).pipe(logLevel),
97 { tags: ["provider:gcp", "provider:gcp:iap", "live"], timeout: 90_000 },
98);
99
100test.provider.skipIf(!brandName)(
101 "create, replace, and delete an IAP OAuth client",
102 (stack) =>
103 Effect.gen(function* () {
104 yield* stack.destroy();
105 const brand = brandName!;
106
107 const created = yield* stack.deploy(
108 Effect.gen(function* () {
109 return yield* GCP.IAP.BrandsIdentityAwareProxyClient("Console", {
110 brand,
111 displayName: "Alchemy console",
112 });
113 }),
114 );
115
116 expect(created.name).toContain("/identityAwareProxyClients/");
117 expect(created.brand).toEqual(brand);
118 expect(created.displayName).toEqual("Alchemy console");
119 expect(created.identityAwareProxyClientId.length).toBeGreaterThan(0);
120
121 const fetched = yield* iap.getProjectsBrandsIdentityAwareProxyClients({
122 name: created.name,
123 });
124 expect(fetched.name).toEqual(created.name);
125 expect(fetched.displayName).toContain("[alchemy ");
126 expect(fetched.displayName).toContain("Alchemy console");
127
128 const replaced = yield* stack.deploy(
129 Effect.gen(function* () {
130 return yield* GCP.IAP.BrandsIdentityAwareProxyClient("Console", {
131 brand,
132 displayName: "Alchemy portal",
133 });
134 }),
135 );
136
137 expect(replaced.displayName).toEqual("Alchemy portal");
138 expect(replaced.brand).toEqual(brand);
139
140 const fetchedReplace =
141 yield* iap.getProjectsBrandsIdentityAwareProxyClients({
142 name: replaced.name,
143 });
144 expect(fetchedReplace.displayName).toContain("Alchemy portal");
145
146 yield* stack.destroy();
147
148 const gone = yield* waitUntilGone(replaced.name);
149 expect(gone).toEqual("gone");
150 }).pipe(logLevel),
151 { tags: ["provider:gcp", "provider:gcp:iap", "live"], timeout: 90_000 },
152);