1import * as AWS from "@/AWS";
2import {
3 ManagedLoginBranding,
4 UserPool,
5 UserPoolClient,
6 UserPoolDomain,
7} from "@/AWS/Cognito";
8import * as Test from "@/Test/Alchemy";
9import * as cip from "@distilled.cloud/aws/cognito-identity-provider";
10import { expect } from "alchemy-test";
11import * as Data from "effect/Data";
12import * as Effect from "effect/Effect";
13import * as Schedule from "effect/Schedule";
14
15const { test } = Test.make({ providers: AWS.providers() });
16
17class BrandingStillExists extends Data.TaggedError("BrandingStillExists")<{
18 readonly managedLoginBrandingId: string;
19}> {}
20
21const assertBrandingDeleted = (
22 userPoolId: string,
23 managedLoginBrandingId: string,
24) =>
25 cip
26 .describeManagedLoginBranding({
27 UserPoolId: userPoolId,
28 ManagedLoginBrandingId: managedLoginBrandingId,
29 })
30 .pipe(
31 Effect.flatMap(() =>
32 Effect.fail(new BrandingStillExists({ managedLoginBrandingId })),
33 ),
34 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
35 Effect.retry({
36 while: (e) => e._tag === "BrandingStillExists",
37 schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(8)]),
38 }),
39 );
40
41test.provider(
42 "default branding activates managed login; update to custom settings; replace on client change",
43 (stack) =>
44 Effect.gen(function* () {
45 yield* stack.destroy();
46
47 const infra = (
48 useSecondClient: boolean,
49 settings?: Record<string, unknown>,
50 ) =>
51 Effect.gen(function* () {
52 const pool = yield* UserPool("BrandingPool", {});
53 const clientA = yield* UserPoolClient("WebA", {
54 userPoolId: pool.userPoolId,
55 });
56 const clientB = yield* UserPoolClient("WebB", {
57 userPoolId: pool.userPoolId,
58 });
59 const domain = yield* UserPoolDomain("AuthDomain", {
60 userPoolId: pool.userPoolId,
61 managedLoginVersion: 2,
62 });
63 const branding = yield* ManagedLoginBranding("Branding", {
64 userPoolId: pool.userPoolId,
65 clientId: useSecondClient ? clientB.clientId : clientA.clientId,
66 ...(settings === undefined ? {} : { settings }),
67 });
68 return { pool, clientA, clientB, domain, branding };
69 });
70
71 const created = yield* stack.deploy(infra(false));
72
73 expect(created.branding.managedLoginBrandingId).toBeDefined();
74 expect(created.branding.userPoolId).toBe(created.pool.userPoolId);
75 expect(created.branding.clientId).toBe(created.clientA.clientId);
76 expect(created.domain.domain).toBeDefined();
77
78 // out-of-band: the client has the style assigned with Cognito's
79 // provided default values (no console step required)
80 const byClient = yield* cip.describeManagedLoginBrandingByClient({
81 UserPoolId: created.pool.userPoolId,
82 ClientId: created.clientA.clientId,
83 });
84 expect(byClient.ManagedLoginBranding?.ManagedLoginBrandingId).toBe(
85 created.branding.managedLoginBrandingId,
86 );
87 expect(byClient.ManagedLoginBranding?.UseCognitoProvidedValues).toBe(
88 true,
89 );
90
91 // update in place: switch from provided values to custom settings
92 // (use the merged provided-values document, the exact designer shape)
93 const merged = yield* cip.describeManagedLoginBranding({
94 UserPoolId: created.pool.userPoolId,
95 ManagedLoginBrandingId: created.branding.managedLoginBrandingId,
96 ReturnMergedResources: true,
97 });
98 const settings = merged.ManagedLoginBranding?.Settings as Record<
99 string,
100 unknown
101 >;
102 expect(settings).toBeDefined();
103
104 const updated = yield* stack.deploy(infra(false, settings));
105 expect(updated.branding.managedLoginBrandingId).toBe(
106 created.branding.managedLoginBrandingId,
107 );
108 const afterUpdate = yield* cip.describeManagedLoginBranding({
109 UserPoolId: created.pool.userPoolId,
110 ManagedLoginBrandingId: created.branding.managedLoginBrandingId,
111 });
112 expect(afterUpdate.ManagedLoginBranding?.UseCognitoProvidedValues).toBe(
113 false,
114 );
115
116 // changing the app client replaces the branding style
117 const replaced = yield* stack.deploy(infra(true, settings));
118 expect(replaced.branding.managedLoginBrandingId).not.toBe(
119 created.branding.managedLoginBrandingId,
120 );
121 expect(replaced.branding.clientId).toBe(created.clientB.clientId);
122 yield* assertBrandingDeleted(
123 created.pool.userPoolId,
124 created.branding.managedLoginBrandingId,
125 );
126 const replacedByClient = yield* cip.describeManagedLoginBrandingByClient({
127 UserPoolId: created.pool.userPoolId,
128 ClientId: created.clientB.clientId,
129 });
130 expect(
131 replacedByClient.ManagedLoginBranding?.ManagedLoginBrandingId,
132 ).toBe(replaced.branding.managedLoginBrandingId);
133
134 yield* stack.destroy();
135 yield* cip.describeUserPool({ UserPoolId: created.pool.userPoolId }).pipe(
136 Effect.flatMap(() =>
137 Effect.fail(
138 new BrandingStillExists({
139 managedLoginBrandingId: created.pool.userPoolId,
140 }),
141 ),
142 ),
143 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
144 Effect.retry({
145 while: (e) => e._tag === "BrandingStillExists",
146 schedule: Schedule.max([
147 Schedule.exponential(500),
148 Schedule.recurs(8),
149 ]),
150 }),
151 );
152 }),
153 { tags: ["provider:aws", "provider:aws:cognito", "live"], timeout: 180_000 },
154);