1import * as AWS from "@/AWS";
2import { 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 Data from "effect/Data";
7import * as Effect from "effect/Effect";
8import * as Schedule from "effect/Schedule";
9
10const { test } = Test.make({ providers: AWS.providers() });
11
12class UserPoolStillExists extends Data.TaggedError("UserPoolStillExists")<{
13 readonly userPoolId: string;
14}> {}
15
16const assertPoolDeleted = (userPoolId: string) =>
17 cip.describeUserPool({ UserPoolId: userPoolId }).pipe(
18 Effect.flatMap(() => Effect.fail(new UserPoolStillExists({ userPoolId }))),
19 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
20 Effect.retry({
21 while: (e) => e._tag === "UserPoolStillExists",
22 schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(8)]),
23 }),
24 );
25
26/**
27 * RiskConfiguration (threat protection) probe — pins the live semantics of
28 * `setRiskConfiguration` with TYPED outcomes only (no untyped error may
29 * escape; `UserPoolAddOnNotEnabledException` is in distilled's
30 * `SetRiskConfigurationError` union).
31 *
32 * Probed live 2026-07-09: contrary to the catalog's entitlement assumption,
33 * the WRITE is accepted on both `LITE` and `ESSENTIALS` (default) tier
34 * pools and reads back verbatim — the feature plan gates *enforcement* of
35 * threat protection, not configuration. A future RiskConfiguration
36 * resource (catalog: P2, unimplemented) is therefore fully live-testable
37 * without a PLUS plan.
38 */
39test.provider(
40 "setRiskConfiguration is accepted and reads back on LITE and ESSENTIALS pools",
41 (stack) =>
42 Effect.gen(function* () {
43 yield* stack.destroy();
44
45 const outputs = yield* stack.deploy(
46 Effect.gen(function* () {
47 const lite = yield* UserPool("RiskProbeLitePool", { tier: "LITE" });
48 const essentials = yield* UserPool("RiskProbePool", {});
49 return { lite, essentials };
50 }),
51 );
52
53 const probe = (userPoolId: string) =>
54 Effect.gen(function* () {
55 const outcome = yield* cip
56 .setRiskConfiguration({
57 UserPoolId: userPoolId,
58 CompromisedCredentialsRiskConfiguration: {
59 Actions: { EventAction: "BLOCK" },
60 },
61 })
62 .pipe(
63 Effect.map(() => "accepted" as const),
64 Effect.catchTag("UserPoolAddOnNotEnabledException", () =>
65 Effect.succeed("add-on-not-enabled" as const),
66 ),
67 );
68 const readBack = yield* cip.describeRiskConfiguration({
69 UserPoolId: userPoolId,
70 });
71 return {
72 outcome,
73 eventAction:
74 readBack.RiskConfiguration
75 ?.CompromisedCredentialsRiskConfiguration?.Actions?.EventAction,
76 };
77 });
78
79 const lite = yield* probe(outputs.lite.userPoolId);
80 expect(lite.outcome).toBe("accepted");
81 expect(lite.eventAction).toBe("BLOCK");
82
83 const essentials = yield* probe(outputs.essentials.userPoolId);
84 expect(essentials.outcome).toBe("accepted");
85 expect(essentials.eventAction).toBe("BLOCK");
86
87 yield* stack.destroy();
88 yield* assertPoolDeleted(outputs.lite.userPoolId);
89 yield* assertPoolDeleted(outputs.essentials.userPoolId);
90 }),
91 { tags: ["provider:aws", "provider:aws:cognito", "live"], timeout: 120_000 },
92);