1import * as AWS from "@/AWS";
2import { User, UserPool } from "@/AWS/Cognito";
3import * as Test from "@/Test/Alchemy";
4import * as cip from "@distilled.cloud/aws/cognito-identity-provider";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Redacted from "effect/Redacted";
8
9const { test } = Test.make({ providers: AWS.providers() });
10
11const PASSWORD = "Alchemy-User-Passw0rd1";
12
13const plain = (
14 value: string | Redacted.Redacted<string> | undefined,
15): string | undefined =>
16 value === undefined
17 ? undefined
18 : typeof value === "string"
19 ? value
20 : Redacted.value(value);
21
22const attributeValue = (
23 attributes: cip.AttributeType[] | undefined,
24 name: string,
25) => plain(attributes?.find((attribute) => attribute.Name === name)?.Value);
26
27test.provider(
28 "create confirmed user, update attributes/enabled, rename (replace), delete",
29 (stack) =>
30 Effect.gen(function* () {
31 yield* stack.destroy();
32
33 const outputs = yield* stack.deploy(
34 Effect.gen(function* () {
35 const pool = yield* UserPool("UserTestPool", {
36 passwordPolicy: { minimumLength: 12, requireSymbols: false },
37 accountRecovery: [{ name: "admin_only", priority: 1 }],
38 });
39 const user = yield* User("Admin", {
40 userPoolId: pool.userPoolId,
41 attributes: {
42 email: "admin@example.com",
43 email_verified: "true",
44 },
45 password: Redacted.make(PASSWORD),
46 });
47 return { pool, user };
48 }),
49 );
50
51 expect(outputs.user.username).toBeDefined();
52 expect(outputs.user.sub).toBeTruthy();
53 // permanent password ⇒ account is CONFIRMED without any invite flow
54 expect(outputs.user.userStatus).toBe("CONFIRMED");
55
56 // out-of-band verification via distilled
57 const created = yield* cip.adminGetUser({
58 UserPoolId: outputs.pool.userPoolId,
59 Username: outputs.user.username,
60 });
61 expect(created.Enabled).toBe(true);
62 expect(created.UserStatus).toBe("CONFIRMED");
63 expect(attributeValue(created.UserAttributes, "email")).toBe(
64 "admin@example.com",
65 );
66 expect(attributeValue(created.UserAttributes, "email_verified")).toBe(
67 "true",
68 );
69
70 // attributes and enabled mutate in place
71 const updated = yield* stack.deploy(
72 Effect.gen(function* () {
73 const pool = yield* UserPool("UserTestPool", {
74 passwordPolicy: { minimumLength: 12, requireSymbols: false },
75 accountRecovery: [{ name: "admin_only", priority: 1 }],
76 });
77 const user = yield* User("Admin", {
78 userPoolId: pool.userPoolId,
79 attributes: {
80 email: "admin2@example.com",
81 email_verified: "true",
82 },
83 password: Redacted.make(PASSWORD),
84 enabled: false,
85 });
86 return { pool, user };
87 }),
88 );
89 expect(updated.user.username).toBe(outputs.user.username);
90 expect(updated.user.sub).toBe(outputs.user.sub);
91 const afterUpdate = yield* cip.adminGetUser({
92 UserPoolId: outputs.pool.userPoolId,
93 Username: outputs.user.username,
94 });
95 expect(afterUpdate.Enabled).toBe(false);
96 expect(attributeValue(afterUpdate.UserAttributes, "email")).toBe(
97 "admin2@example.com",
98 );
99
100 // an explicit username replaces the user
101 const renamed = yield* stack.deploy(
102 Effect.gen(function* () {
103 const pool = yield* UserPool("UserTestPool", {
104 passwordPolicy: { minimumLength: 12, requireSymbols: false },
105 accountRecovery: [{ name: "admin_only", priority: 1 }],
106 });
107 const user = yield* User("Admin", {
108 userPoolId: pool.userPoolId,
109 username: "explicit-admin",
110 attributes: {
111 email: "admin2@example.com",
112 email_verified: "true",
113 },
114 password: Redacted.make(PASSWORD),
115 });
116 return { pool, user };
117 }),
118 );
119 expect(renamed.user.username).toBe("explicit-admin");
120 expect(renamed.user.sub).not.toBe(outputs.user.sub);
121 const oldGone = yield* cip
122 .adminGetUser({
123 UserPoolId: outputs.pool.userPoolId,
124 Username: outputs.user.username,
125 })
126 .pipe(
127 Effect.map(() => false),
128 Effect.catchTag("UserNotFoundException", () => Effect.succeed(true)),
129 );
130 expect(oldGone).toBe(true);
131
132 yield* stack.destroy();
133 const gone = yield* cip
134 .adminGetUser({
135 UserPoolId: outputs.pool.userPoolId,
136 Username: "explicit-admin",
137 })
138 .pipe(
139 Effect.map(() => false),
140 // the pool is destroyed with the stack, so the pool itself being
141 // gone also proves the user is gone
142 Effect.catchTag(
143 ["UserNotFoundException", "ResourceNotFoundException"],
144 () => Effect.succeed(true),
145 ),
146 );
147 expect(gone).toBe(true);
148 }),
149 { tags: ["provider:aws", "provider:aws:cognito", "live"], timeout: 120_000 },
150);