1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as cloudidentity from "@distilled.cloud/gcp/cloudidentity_v1";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as Schedule from "effect/Schedule";
7import {
8 customer,
9 domain,
10 logLevel,
11 memberEmail,
12 runMembershipLifecycle,
13} from "./common.ts";
14
15const { test } = Test.make({ providers: GCP.providers() });
16
17const waitUntilGone = (name: string) =>
18 cloudidentity.getGroupsMemberships({ name }).pipe(
19 Effect.as("found" as const),
20 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
21 Effect.repeat({
22 schedule: Schedule.spaced("1 second"),
23 until: (status) => status === "gone",
24 times: 10,
25 }),
26 );
27
28test.provider(
29 "getGroupsMemberships on a missing membership fails with a typed tag",
30 (stack) =>
31 Effect.gen(function* () {
32 yield* stack.destroy();
33
34 const error = yield* Effect.flip(
35 cloudidentity.getGroupsMemberships({
36 name: "groups/alchemy-missing-group/memberships/alchemy-missing",
37 }),
38 );
39 expect(error._tag).toEqual("ServiceDisabled");
40
41 yield* stack.destroy();
42 }).pipe(logLevel),
43 {
44 tags: ["provider:gcp", "provider:gcp:cloudidentity", "live"],
45 timeout: 90_000,
46 },
47);
48
49test.provider.skipIf(!!process.env.GCP_TEST_CLOUDIDENTITY)(
50 "createGroupsMemberships without Cloud Identity access fails with a typed entitlement error",
51 (stack) =>
52 Effect.gen(function* () {
53 yield* stack.destroy();
54
55 const error = yield* Effect.flip(
56 cloudidentity.createGroupsMemberships({
57 parent: "groups/alchemy-missing-group",
58 body: {
59 preferredMemberKey: { id: "probe@example.com" },
60 },
61 }),
62 );
63 expect(error._tag).toEqual("ServiceDisabled");
64
65 yield* stack.destroy();
66 }).pipe(logLevel),
67 {
68 tags: ["provider:gcp", "provider:gcp:cloudidentity", "live"],
69 timeout: 90_000,
70 },
71);
72
73test.provider.skipIf(!runMembershipLifecycle)(
74 "create, update, and delete a membership",
75 (stack) =>
76 Effect.gen(function* () {
77 yield* stack.destroy();
78
79 const created = yield* stack.deploy(
80 Effect.gen(function* () {
81 const group = yield* GCP.CloudIdentity.Group("Eng", {
82 parent: customer,
83 domain,
84 displayName: "Engineering",
85 });
86 const membership = yield* GCP.CloudIdentity.GroupsMembership("Ada", {
87 parent: group.name,
88 memberKeyId: memberEmail!,
89 roles: [{ name: "MEMBER" }],
90 });
91 return { group, membership };
92 }),
93 );
94
95 expect(created.membership.name).toContain("/memberships/");
96 expect(created.membership.memberKeyId).toEqual(memberEmail);
97 expect(
98 created.membership.roles.some((role) => role.name === "MEMBER"),
99 ).toEqual(true);
100
101 const fetched = yield* cloudidentity.getGroupsMemberships({
102 name: created.membership.name,
103 });
104 expect(fetched.name).toEqual(created.membership.name);
105
106 const updated = yield* stack.deploy(
107 Effect.gen(function* () {
108 const group = yield* GCP.CloudIdentity.Group("Eng", {
109 parent: customer,
110 groupKeyId: created.group.groupKeyId,
111 displayName: "Engineering",
112 });
113 const membership = yield* GCP.CloudIdentity.GroupsMembership("Ada", {
114 parent: group.name,
115 memberKeyId: memberEmail!,
116 roles: [{ name: "MEMBER" }, { name: "MANAGER" }],
117 });
118 return { group, membership };
119 }),
120 );
121
122 expect(updated.membership.name).toEqual(created.membership.name);
123 expect(
124 updated.membership.roles.some((role) => role.name === "MANAGER"),
125 ).toEqual(true);
126
127 yield* stack.destroy();
128
129 const gone = yield* waitUntilGone(created.membership.name);
130 expect(gone).toEqual("gone");
131 }).pipe(logLevel),
132 {
133 tags: ["provider:gcp", "provider:gcp:cloudidentity", "live"],
134 timeout: 90_000,
135 },
136);