1import * as AWS from "@/AWS";
2import {
3 IdentityPool,
4 IdentityPoolRoleAttachment,
5 UserPool,
6 UserPoolClient,
7} from "@/AWS/Cognito";
8import { Role } from "@/AWS/IAM";
9import * as Output from "@/Output";
10import * as Test from "@/Test/Alchemy";
11import * as ci from "@distilled.cloud/aws/cognito-identity";
12import { expect } from "alchemy-test";
13import * as Data from "effect/Data";
14import * as Effect from "effect/Effect";
15import * as Schedule from "effect/Schedule";
16
17const { test } = Test.make({ providers: AWS.providers() });
18
19class IdentityPoolStillExists extends Data.TaggedError(
20 "IdentityPoolStillExists",
21)<{ readonly identityPoolId: string }> {}
22
23const assertIdentityPoolDeleted = (identityPoolId: string) =>
24 ci.describeIdentityPool({ IdentityPoolId: identityPoolId }).pipe(
25 Effect.flatMap(() =>
26 Effect.fail(new IdentityPoolStillExists({ identityPoolId })),
27 ),
28 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
29 Effect.retry({
30 while: (e) => e._tag === "IdentityPoolStillExists",
31 schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(8)]),
32 }),
33 );
34
35test.provider(
36 "identity pool lifecycle with user pool federation and tags",
37 (stack) =>
38 Effect.gen(function* () {
39 yield* stack.destroy();
40
41 const outputs = yield* stack.deploy(
42 Effect.gen(function* () {
43 const identities = yield* IdentityPool("Identities", {
44 allowUnauthenticatedIdentities: false,
45 tags: { Environment: "test" },
46 });
47 return { identities };
48 }),
49 );
50
51 expect(outputs.identities.identityPoolId).toContain(":");
52 expect(outputs.identities.identityPoolArn).toContain(":identitypool/");
53
54 // out-of-band verification via distilled
55 const created = yield* ci.describeIdentityPool({
56 IdentityPoolId: outputs.identities.identityPoolId,
57 });
58 expect(created.IdentityPoolName).toBe(
59 outputs.identities.identityPoolName,
60 );
61 expect(created.AllowUnauthenticatedIdentities).toBe(false);
62 const tags = yield* ci.listTagsForResource({
63 ResourceArn: outputs.identities.identityPoolArn,
64 });
65 expect(tags.Tags?.Environment).toBe("test");
66 expect(tags.Tags?.["alchemy::id"]).toBe("Identities");
67
68 // update in place: federate a user pool + allow guests
69 const updated = yield* stack.deploy(
70 Effect.gen(function* () {
71 const pool = yield* UserPool("FederatedPool", {});
72 const client = yield* UserPoolClient("FederatedClient", {
73 userPoolId: pool.userPoolId,
74 });
75 const identities = yield* IdentityPool("Identities", {
76 allowUnauthenticatedIdentities: true,
77 cognitoIdentityProviders: [
78 {
79 providerName: Output.interpolate`cognito-idp.us-west-2.amazonaws.com/${pool.userPoolId}`,
80 clientId: client.clientId,
81 },
82 ],
83 tags: { Environment: "test" },
84 });
85 return { pool, client, identities };
86 }),
87 );
88 expect(updated.identities.identityPoolId).toBe(
89 outputs.identities.identityPoolId,
90 );
91
92 const afterUpdate = yield* ci.describeIdentityPool({
93 IdentityPoolId: outputs.identities.identityPoolId,
94 });
95 expect(afterUpdate.AllowUnauthenticatedIdentities).toBe(true);
96 expect(afterUpdate.CognitoIdentityProviders).toHaveLength(1);
97
98 yield* stack.destroy();
99 yield* assertIdentityPoolDeleted(outputs.identities.identityPoolId);
100 }),
101 { tags: ["provider:aws", "provider:aws:cognito", "live"], timeout: 120_000 },
102);
103
104test.provider(
105 "role attachment lifecycle",
106 (stack) =>
107 Effect.gen(function* () {
108 yield* stack.destroy();
109
110 const outputs = yield* stack.deploy(
111 Effect.gen(function* () {
112 const identities = yield* IdentityPool("RolesPool", {
113 allowUnauthenticatedIdentities: true,
114 });
115 const role = yield* Role("CognitoAuthRole", {
116 assumeRolePolicyDocument: {
117 Version: "2012-10-17",
118 Statement: [
119 {
120 Effect: "Allow",
121 Principal: { Federated: "cognito-identity.amazonaws.com" },
122 Action: ["sts:AssumeRoleWithWebIdentity"],
123 Condition: {
124 StringEquals: {
125 "cognito-identity.amazonaws.com:aud":
126 identities.identityPoolId,
127 },
128 },
129 },
130 ],
131 },
132 });
133 const attachment = yield* IdentityPoolRoleAttachment("Roles", {
134 identityPoolId: identities.identityPoolId,
135 roles: { authenticated: role.roleArn },
136 });
137 return { identities, role, attachment };
138 }),
139 );
140
141 expect(outputs.attachment.roles.authenticated).toBe(outputs.role.roleArn);
142
143 // out-of-band verification via distilled
144 const observed = yield* ci.getIdentityPoolRoles({
145 IdentityPoolId: outputs.identities.identityPoolId,
146 });
147 expect(observed.Roles?.authenticated).toBe(outputs.role.roleArn);
148
149 yield* stack.destroy();
150 yield* assertIdentityPoolDeleted(outputs.identities.identityPoolId);
151 }),
152 {
153 tags: ["provider:aws", "provider:aws:cognito", "provider:aws:iam", "live"],
154 timeout: 120_000,
155 },
156);