@reventlessdev/reventless-aws 3.0.0-alpha.292 โ 3.0.0-alpha.294
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/package.json +5 -5
- package/src/Platform.res +43 -4
- package/src/Platform.res.mjs +11 -4
- package/src/Platform_Stack.res +82 -0
- package/src/Platform_Stack.res.mjs +18 -2
- package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res +517 -0
- package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res.mjs +341 -0
- package/src/adapter/Auth/Auth_ActiveRoleStore.res +280 -0
- package/src/adapter/Auth/Auth_ActiveRoleStore.res.mjs +163 -0
- package/src/adapter/Auth/Auth_ActiveRoleStore_Ops.res +148 -0
- package/src/adapter/Auth/Auth_ActiveRoleStore_Ops.res.mjs +128 -0
- package/src/adapter/Auth/Auth_ActiveRoleTrigger.res +185 -0
- package/src/adapter/Auth/Auth_ActiveRoleTrigger.res.mjs +107 -0
- package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res +201 -0
- package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res.mjs +126 -0
- package/tests/Auth_ActiveRolePoolAttachmentTest.res +285 -0
- package/tests/Auth_ActiveRolePoolAttachmentTest.res.mjs +263 -0
- package/tests/Auth_ActiveRoleStoreTest.res +42 -0
- package/tests/Auth_ActiveRoleStoreTest.res.mjs +41 -0
- package/tests/Auth_ActiveRoleTrigger_OpsTest.res +169 -0
- package/tests/Auth_ActiveRoleTrigger_OpsTest.res.mjs +189 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// ๐จ The test ยง6 of the plan asks for, and the one that matters most in this
|
|
4
|
+
// feature: `UpdateUserPool` requires "a value for all parameters that you don't
|
|
5
|
+
// want set to a default value". An attach that sends only `LambdaConfig`
|
|
6
|
+
// silently returns every other setting on the pool to its default โ on a pool
|
|
7
|
+
// the framework did not create and whose configuration it never described.
|
|
8
|
+
//
|
|
9
|
+
// So the assertions below are all the same assertion: a pool carrying
|
|
10
|
+
// non-default settings still carries them afterwards. The merge is a pure
|
|
11
|
+
// function precisely so this is checkable against a pool the test did not
|
|
12
|
+
// create, which is the case the merge exists for.
|
|
13
|
+
|
|
14
|
+
module Attachment = Auth_ActiveRolePoolAttachment
|
|
15
|
+
|
|
16
|
+
let str = JSON.Encode.string
|
|
17
|
+
|
|
18
|
+
// A pool that is nothing like a fresh one: MFA on, a deletion guard, a custom
|
|
19
|
+
// verification message, two triggers already attached, tags, and a paid tier.
|
|
20
|
+
//
|
|
21
|
+
// The key set mirrors what `DescribeUserPool` actually returns for a live pool,
|
|
22
|
+
// including `IssuerConfiguration` / `KeyConfiguration` โ informational fields
|
|
23
|
+
// that are *not* members of `UpdateUserPool`. The merge carries them and the SDK
|
|
24
|
+
// drops them during serialisation, which is why they are harmless; they are here
|
|
25
|
+
// so a future edit that starts filtering on this list has a real shape to filter.
|
|
26
|
+
let describedPool = () =>
|
|
27
|
+
Dict.fromArray([
|
|
28
|
+
("Id", str("eu-west-1_abc123")),
|
|
29
|
+
("Name", str("CustomerPool")),
|
|
30
|
+
("Arn", str("arn:aws:cognito-idp:eu-west-1:1:userpool/eu-west-1_abc123")),
|
|
31
|
+
("Status", str("Enabled")),
|
|
32
|
+
("CreationDate", str("2020-01-01T00:00:00Z")),
|
|
33
|
+
("LastModifiedDate", str("2024-01-01T00:00:00Z")),
|
|
34
|
+
("SchemaAttributes", JSON.Encode.array([str("email")])),
|
|
35
|
+
("UsernameAttributes", JSON.Encode.array([str("email")])),
|
|
36
|
+
("EstimatedNumberOfUsers", JSON.Encode.int(4200)),
|
|
37
|
+
("IssuerConfiguration", Dict.fromArray([("IssuerUri", str("https://x"))])->JSON.Encode.object),
|
|
38
|
+
("KeyConfiguration", Dict.fromArray([("KmsKeyId", str("k-1"))])->JSON.Encode.object),
|
|
39
|
+
// A paid tier silently reverting to Lite would be a billing- and
|
|
40
|
+
// capability-level regression, and one nothing in the deploy would report.
|
|
41
|
+
("UserPoolTier", str("PLUS")),
|
|
42
|
+
("MfaConfiguration", str("ON")),
|
|
43
|
+
("DeletionProtection", str("ACTIVE")),
|
|
44
|
+
("EmailVerificationMessage", str("Your code is {####}, do not share it")),
|
|
45
|
+
(
|
|
46
|
+
"Policies",
|
|
47
|
+
Dict.fromArray([
|
|
48
|
+
(
|
|
49
|
+
"PasswordPolicy",
|
|
50
|
+
Dict.fromArray([("MinimumLength", JSON.Encode.int(24))])->JSON.Encode.object,
|
|
51
|
+
),
|
|
52
|
+
])->JSON.Encode.object,
|
|
53
|
+
),
|
|
54
|
+
(
|
|
55
|
+
"UserPoolTags",
|
|
56
|
+
Dict.fromArray([("CostCentre", str("identity"))])->JSON.Encode.object,
|
|
57
|
+
),
|
|
58
|
+
(
|
|
59
|
+
"LambdaConfig",
|
|
60
|
+
Dict.fromArray([
|
|
61
|
+
("PreSignUp", str("arn:aws:lambda:eu-west-1:1:function:TheirPreSignUp")),
|
|
62
|
+
("CustomMessage", str("arn:aws:lambda:eu-west-1:1:function:TheirCustomMessage")),
|
|
63
|
+
])->JSON.Encode.object,
|
|
64
|
+
),
|
|
65
|
+
])
|
|
66
|
+
|
|
67
|
+
let merged = (~preTokenGenerationArn) =>
|
|
68
|
+
Attachment.mergedUpdateInput(
|
|
69
|
+
~described=describedPool(),
|
|
70
|
+
~userPoolId="eu-west-1_abc123",
|
|
71
|
+
~preTokenGenerationArn,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
let triggerArn = "arn:aws:lambda:eu-west-1:1:function:ActiveRoleTrigger"
|
|
75
|
+
|
|
76
|
+
let lambdaConfigOf = input =>
|
|
77
|
+
input->Dict.get("LambdaConfig")->Option.flatMap(JSON.Decode.object)
|
|
78
|
+
|
|
79
|
+
describe("Auth_ActiveRolePoolAttachment.mergedUpdateInput โ settings survive the attach", () => {
|
|
80
|
+
let input = merged(~preTokenGenerationArn=Some(triggerArn))
|
|
81
|
+
|
|
82
|
+
testSync("MFA stays on rather than defaulting off", () =>
|
|
83
|
+
expect(input->Dict.get("MfaConfiguration"))->toEqual(Some(str("ON")))
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
testSync("deletion protection stays active", () =>
|
|
87
|
+
expect(input->Dict.get("DeletionProtection"))->toEqual(Some(str("ACTIVE")))
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
testSync("a customised verification message is not reset", () =>
|
|
91
|
+
expect(input->Dict.get("EmailVerificationMessage"))->toEqual(
|
|
92
|
+
Some(str("Your code is {####}, do not share it")),
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
testSync("the password policy travels whole", () =>
|
|
97
|
+
expect(
|
|
98
|
+
input
|
|
99
|
+
->Dict.get("Policies")
|
|
100
|
+
->Option.flatMap(JSON.Decode.object)
|
|
101
|
+
->Option.flatMap(p => p->Dict.get("PasswordPolicy"))
|
|
102
|
+
->Option.flatMap(JSON.Decode.object)
|
|
103
|
+
->Option.flatMap(p => p->Dict.get("MinimumLength")),
|
|
104
|
+
)->toEqual(Some(JSON.Encode.int(24)))
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
testSync("tags are not dropped", () =>
|
|
108
|
+
expect(
|
|
109
|
+
input
|
|
110
|
+
->Dict.get("UserPoolTags")
|
|
111
|
+
->Option.flatMap(JSON.Decode.object)
|
|
112
|
+
->Option.flatMap(t => t->Dict.get("CostCentre")),
|
|
113
|
+
)->toEqual(Some(str("identity")))
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
testSync("a paid tier is not reverted to Lite", () =>
|
|
117
|
+
expect(input->Dict.get("UserPoolTier"))->toEqual(Some(str("PLUS")))
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
// The reset hazard one level down: replacing the whole `LambdaConfig` with a
|
|
121
|
+
// single-key object would silently detach every trigger the customer had.
|
|
122
|
+
testSync("the pool's existing triggers stay attached", () =>
|
|
123
|
+
expect((
|
|
124
|
+
lambdaConfigOf(input)->Option.flatMap(c => c->Dict.get("PreSignUp")),
|
|
125
|
+
lambdaConfigOf(input)->Option.flatMap(c => c->Dict.get("CustomMessage")),
|
|
126
|
+
))->toEqual((
|
|
127
|
+
Some(str("arn:aws:lambda:eu-west-1:1:function:TheirPreSignUp")),
|
|
128
|
+
Some(str("arn:aws:lambda:eu-west-1:1:function:TheirCustomMessage")),
|
|
129
|
+
))
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
testSync("and ours is added beside them", () =>
|
|
133
|
+
expect(lambdaConfigOf(input)->Option.flatMap(c => c->Dict.get("PreTokenGeneration")))->toEqual(
|
|
134
|
+
Some(str(triggerArn)),
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe("Auth_ActiveRolePoolAttachment.mergedUpdateInput โ shaping for the API", () => {
|
|
140
|
+
let input = merged(~preTokenGenerationArn=Some(triggerArn))
|
|
141
|
+
|
|
142
|
+
testSync("UserPoolId names the pool being updated", () =>
|
|
143
|
+
expect(input->Dict.get("UserPoolId"))->toEqual(Some(str("eu-west-1_abc123")))
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
// DescribeUserPool returns `Name`; UpdateUserPool takes `PoolName`. Sending
|
|
147
|
+
// `Name` is rejected, and omitting the rename would rename the pool by default.
|
|
148
|
+
testSync("Name is renamed to PoolName rather than dropped", () =>
|
|
149
|
+
expect((input->Dict.get("PoolName"), input->Dict.get("Name")))->toEqual((
|
|
150
|
+
Some(str("CustomerPool")),
|
|
151
|
+
None,
|
|
152
|
+
))
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
testSync("read-only fields UpdateUserPool rejects are not sent", () =>
|
|
156
|
+
expect(
|
|
157
|
+
["Id", "Arn", "Status", "CreationDate", "LastModifiedDate", "SchemaAttributes",
|
|
158
|
+
"UsernameAttributes", "EstimatedNumberOfUsers"]->Array.filter(k =>
|
|
159
|
+
input->Dict.get(k)->Option.isSome
|
|
160
|
+
),
|
|
161
|
+
)->toEqual([])
|
|
162
|
+
)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe("Auth_ActiveRolePoolAttachment.mergedUpdateInput โ detaching", () => {
|
|
166
|
+
let input = merged(~preTokenGenerationArn=None)
|
|
167
|
+
|
|
168
|
+
// Destroy has to detach, or the pool is left pointing at a deleted function and
|
|
169
|
+
// every sign-in fails โ on a pool nothing in this deployment would ever fix.
|
|
170
|
+
testSync("clearing removes our trigger", () =>
|
|
171
|
+
expect(lambdaConfigOf(input)->Option.flatMap(c => c->Dict.get("PreTokenGeneration")))->toEqual(
|
|
172
|
+
None,
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
testSync("clearing leaves the customer's own triggers alone", () =>
|
|
177
|
+
expect(lambdaConfigOf(input)->Option.flatMap(c => c->Dict.get("PreSignUp")))->toEqual(
|
|
178
|
+
Some(str("arn:aws:lambda:eu-west-1:1:function:TheirPreSignUp")),
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
testSync("clearing still sends the rest of the pool back whole", () =>
|
|
183
|
+
expect(input->Dict.get("MfaConfiguration"))->toEqual(Some(str("ON")))
|
|
184
|
+
)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
describe("Auth_ActiveRolePoolAttachment.mergedUpdateInput โ a pool with nothing set", () => {
|
|
188
|
+
testSync("a pool with no LambdaConfig gains one holding only our trigger", () => {
|
|
189
|
+
let input = Attachment.mergedUpdateInput(
|
|
190
|
+
~described=Dict.fromArray([("Name", str("Bare"))]),
|
|
191
|
+
~userPoolId="eu-west-1_bare",
|
|
192
|
+
~preTokenGenerationArn=Some(triggerArn),
|
|
193
|
+
)
|
|
194
|
+
expect(lambdaConfigOf(input))->toEqual(
|
|
195
|
+
Some(Dict.fromArray([("PreTokenGeneration", str(triggerArn))])),
|
|
196
|
+
)
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
describe("Auth_ActiveRolePoolAttachment.attachedTrigger", () => {
|
|
201
|
+
testSync("reports the trigger a described pool carries", () =>
|
|
202
|
+
expect(Attachment.attachedTrigger(~described=merged(~preTokenGenerationArn=Some(triggerArn))))
|
|
203
|
+
->toEqual(Some(triggerArn))
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
testSync("reports none when the pool carries no trigger", () =>
|
|
207
|
+
expect(Attachment.attachedTrigger(~described=Dict.fromArray([("Name", str("Bare"))])))->toEqual(
|
|
208
|
+
None,
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// The pre-attach check. Its whole job is to keep a trigger that cannot run off a
|
|
214
|
+
// live pool, because a pre-token-generation trigger that throws does not degrade
|
|
215
|
+
// the feature โ it fails every sign-in for every user of that pool.
|
|
216
|
+
describe("Auth_ActiveRolePoolAttachment.probeVerdict", () => {
|
|
217
|
+
// The real payload from the outage this check exists to prevent: the function
|
|
218
|
+
// could not resolve a package at module load, so it died during init and never
|
|
219
|
+
// reached its handler. Nothing inside the handler could have caught this.
|
|
220
|
+
let initFailurePayload = `{"errorType":"Error","errorMessage":"Cannot find package '@reventlessdev/reventless-core' imported from /var/task/node_modules/@reventlessdev/reventless-aws/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res.mjs","code":"ERR_MODULE_NOT_FOUND"}`
|
|
221
|
+
|
|
222
|
+
testSync("refuses a function that died before reaching its handler", () =>
|
|
223
|
+
expect(
|
|
224
|
+
Attachment.probeVerdict(~functionError=Some("Unhandled"), ~payload=initFailurePayload),
|
|
225
|
+
)->toEqual(Attachment.Crashed("Unhandled"))
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
testSync("accepts a function that hands the event back", () =>
|
|
229
|
+
expect(
|
|
230
|
+
Attachment.probeVerdict(
|
|
231
|
+
~functionError=None,
|
|
232
|
+
~payload=`{"request":{"groupConfiguration":{"groupsToOverride":[]}},"response":{}}`,
|
|
233
|
+
),
|
|
234
|
+
)->toEqual(Attachment.Healthy)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
// A 200 carrying the wrong shape is as fatal to sign-in as a throw, and far
|
|
238
|
+
// easier to mistake for success.
|
|
239
|
+
testSync("refuses a successful call that returns something else", () =>
|
|
240
|
+
expect(Attachment.probeVerdict(~functionError=None, ~payload=`{"ok":true}`))->toEqual(
|
|
241
|
+
Attachment.NotAnEvent,
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
// Produces a verdict rather than escaping: a parse error here would fail the
|
|
246
|
+
// deploy with a message about JSON instead of about the trigger.
|
|
247
|
+
testSync("treats an unparseable payload as not an event", () =>
|
|
248
|
+
expect(Attachment.probeVerdict(~functionError=None, ~payload="<html>502</html>"))->toEqual(
|
|
249
|
+
Attachment.NotAnEvent,
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
testSync("treats an empty payload as not an event", () =>
|
|
254
|
+
expect(Attachment.probeVerdict(~functionError=None, ~payload=""))->toEqual(
|
|
255
|
+
Attachment.NotAnEvent,
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
describe("Auth_ActiveRolePoolAttachment.probeEvent", () => {
|
|
261
|
+
let event = Attachment.probeEvent(~userPoolId="eu-west-1_Example")->JSON.Decode.object
|
|
262
|
+
|
|
263
|
+
testSync("is shaped like the V1_0 event Cognito sends", () =>
|
|
264
|
+
expect(
|
|
265
|
+
event
|
|
266
|
+
->Option.flatMap(o => o->Dict.get("request"))
|
|
267
|
+
->Option.flatMap(JSON.Decode.object)
|
|
268
|
+
->Option.isSome,
|
|
269
|
+
)->toBe(true)
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
// Empty membership and a subject that cannot exist: the probe checks that the
|
|
273
|
+
// function runs, not what it decides, and must not collide with a real row.
|
|
274
|
+
testSync("presents no groups to narrow", () =>
|
|
275
|
+
expect(
|
|
276
|
+
event
|
|
277
|
+
->Option.flatMap(o => o->Dict.get("request"))
|
|
278
|
+
->Option.flatMap(JSON.Decode.object)
|
|
279
|
+
->Option.flatMap(r => r->Dict.get("groupConfiguration"))
|
|
280
|
+
->Option.flatMap(JSON.Decode.object)
|
|
281
|
+
->Option.flatMap(g => g->Dict.get("groupsToOverride"))
|
|
282
|
+
->Option.flatMap(JSON.Decode.array),
|
|
283
|
+
)->toEqual(Some([]))
|
|
284
|
+
)
|
|
285
|
+
})
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Auth_ActiveRolePoolAttachment$ReventlessAws from "../src/adapter/Auth/Auth_ActiveRolePoolAttachment.res.mjs";
|
|
6
|
+
|
|
7
|
+
function str(prim) {
|
|
8
|
+
return prim;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function describedPool() {
|
|
12
|
+
return Object.fromEntries([
|
|
13
|
+
[
|
|
14
|
+
"Id",
|
|
15
|
+
"eu-west-1_abc123"
|
|
16
|
+
],
|
|
17
|
+
[
|
|
18
|
+
"Name",
|
|
19
|
+
"CustomerPool"
|
|
20
|
+
],
|
|
21
|
+
[
|
|
22
|
+
"Arn",
|
|
23
|
+
"arn:aws:cognito-idp:eu-west-1:1:userpool/eu-west-1_abc123"
|
|
24
|
+
],
|
|
25
|
+
[
|
|
26
|
+
"Status",
|
|
27
|
+
"Enabled"
|
|
28
|
+
],
|
|
29
|
+
[
|
|
30
|
+
"CreationDate",
|
|
31
|
+
"2020-01-01T00:00:00Z"
|
|
32
|
+
],
|
|
33
|
+
[
|
|
34
|
+
"LastModifiedDate",
|
|
35
|
+
"2024-01-01T00:00:00Z"
|
|
36
|
+
],
|
|
37
|
+
[
|
|
38
|
+
"SchemaAttributes",
|
|
39
|
+
["email"]
|
|
40
|
+
],
|
|
41
|
+
[
|
|
42
|
+
"UsernameAttributes",
|
|
43
|
+
["email"]
|
|
44
|
+
],
|
|
45
|
+
[
|
|
46
|
+
"EstimatedNumberOfUsers",
|
|
47
|
+
4200
|
|
48
|
+
],
|
|
49
|
+
[
|
|
50
|
+
"IssuerConfiguration",
|
|
51
|
+
Object.fromEntries([[
|
|
52
|
+
"IssuerUri",
|
|
53
|
+
"https://x"
|
|
54
|
+
]])
|
|
55
|
+
],
|
|
56
|
+
[
|
|
57
|
+
"KeyConfiguration",
|
|
58
|
+
Object.fromEntries([[
|
|
59
|
+
"KmsKeyId",
|
|
60
|
+
"k-1"
|
|
61
|
+
]])
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
"UserPoolTier",
|
|
65
|
+
"PLUS"
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
"MfaConfiguration",
|
|
69
|
+
"ON"
|
|
70
|
+
],
|
|
71
|
+
[
|
|
72
|
+
"DeletionProtection",
|
|
73
|
+
"ACTIVE"
|
|
74
|
+
],
|
|
75
|
+
[
|
|
76
|
+
"EmailVerificationMessage",
|
|
77
|
+
"Your code is {####}, do not share it"
|
|
78
|
+
],
|
|
79
|
+
[
|
|
80
|
+
"Policies",
|
|
81
|
+
Object.fromEntries([[
|
|
82
|
+
"PasswordPolicy",
|
|
83
|
+
Object.fromEntries([[
|
|
84
|
+
"MinimumLength",
|
|
85
|
+
24
|
|
86
|
+
]])
|
|
87
|
+
]])
|
|
88
|
+
],
|
|
89
|
+
[
|
|
90
|
+
"UserPoolTags",
|
|
91
|
+
Object.fromEntries([[
|
|
92
|
+
"CostCentre",
|
|
93
|
+
"identity"
|
|
94
|
+
]])
|
|
95
|
+
],
|
|
96
|
+
[
|
|
97
|
+
"LambdaConfig",
|
|
98
|
+
Object.fromEntries([
|
|
99
|
+
[
|
|
100
|
+
"PreSignUp",
|
|
101
|
+
"arn:aws:lambda:eu-west-1:1:function:TheirPreSignUp"
|
|
102
|
+
],
|
|
103
|
+
[
|
|
104
|
+
"CustomMessage",
|
|
105
|
+
"arn:aws:lambda:eu-west-1:1:function:TheirCustomMessage"
|
|
106
|
+
]
|
|
107
|
+
])
|
|
108
|
+
]
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function merged(preTokenGenerationArn) {
|
|
113
|
+
return Auth_ActiveRolePoolAttachment$ReventlessAws.mergedUpdateInput(describedPool(), "eu-west-1_abc123", preTokenGenerationArn);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let triggerArn = "arn:aws:lambda:eu-west-1:1:function:ActiveRoleTrigger";
|
|
117
|
+
|
|
118
|
+
function lambdaConfigOf(input) {
|
|
119
|
+
return Stdlib_Option.flatMap(input["LambdaConfig"], Stdlib_JSON.Decode.object);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
globalThis.describe("Auth_ActiveRolePoolAttachment.mergedUpdateInput โ settings survive the attach", () => {
|
|
123
|
+
let input = merged(triggerArn);
|
|
124
|
+
globalThis.test("MFA stays on rather than defaulting off", () => {
|
|
125
|
+
globalThis.expect(input["MfaConfiguration"]).toEqual("ON");
|
|
126
|
+
});
|
|
127
|
+
globalThis.test("deletion protection stays active", () => {
|
|
128
|
+
globalThis.expect(input["DeletionProtection"]).toEqual("ACTIVE");
|
|
129
|
+
});
|
|
130
|
+
globalThis.test("a customised verification message is not reset", () => {
|
|
131
|
+
globalThis.expect(input["EmailVerificationMessage"]).toEqual("Your code is {####}, do not share it");
|
|
132
|
+
});
|
|
133
|
+
globalThis.test("the password policy travels whole", () => {
|
|
134
|
+
globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(input["Policies"], Stdlib_JSON.Decode.object), p => p["PasswordPolicy"]), Stdlib_JSON.Decode.object), p => p["MinimumLength"])).toEqual(24);
|
|
135
|
+
});
|
|
136
|
+
globalThis.test("tags are not dropped", () => {
|
|
137
|
+
globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(input["UserPoolTags"], Stdlib_JSON.Decode.object), t => t["CostCentre"])).toEqual("identity");
|
|
138
|
+
});
|
|
139
|
+
globalThis.test("a paid tier is not reverted to Lite", () => {
|
|
140
|
+
globalThis.expect(input["UserPoolTier"]).toEqual("PLUS");
|
|
141
|
+
});
|
|
142
|
+
globalThis.test("the pool's existing triggers stay attached", () => {
|
|
143
|
+
globalThis.expect([
|
|
144
|
+
Stdlib_Option.flatMap(lambdaConfigOf(input), c => c["PreSignUp"]),
|
|
145
|
+
Stdlib_Option.flatMap(lambdaConfigOf(input), c => c["CustomMessage"])
|
|
146
|
+
]).toEqual([
|
|
147
|
+
"arn:aws:lambda:eu-west-1:1:function:TheirPreSignUp",
|
|
148
|
+
"arn:aws:lambda:eu-west-1:1:function:TheirCustomMessage"
|
|
149
|
+
]);
|
|
150
|
+
});
|
|
151
|
+
globalThis.test("and ours is added beside them", () => {
|
|
152
|
+
globalThis.expect(Stdlib_Option.flatMap(lambdaConfigOf(input), c => c["PreTokenGeneration"])).toEqual(triggerArn);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
globalThis.describe("Auth_ActiveRolePoolAttachment.mergedUpdateInput โ shaping for the API", () => {
|
|
157
|
+
let input = merged(triggerArn);
|
|
158
|
+
globalThis.test("UserPoolId names the pool being updated", () => {
|
|
159
|
+
globalThis.expect(input["UserPoolId"]).toEqual("eu-west-1_abc123");
|
|
160
|
+
});
|
|
161
|
+
globalThis.test("Name is renamed to PoolName rather than dropped", () => {
|
|
162
|
+
globalThis.expect([
|
|
163
|
+
input["PoolName"],
|
|
164
|
+
input["Name"]
|
|
165
|
+
]).toEqual([
|
|
166
|
+
"CustomerPool",
|
|
167
|
+
undefined
|
|
168
|
+
]);
|
|
169
|
+
});
|
|
170
|
+
globalThis.test("read-only fields UpdateUserPool rejects are not sent", () => {
|
|
171
|
+
globalThis.expect([
|
|
172
|
+
"Id",
|
|
173
|
+
"Arn",
|
|
174
|
+
"Status",
|
|
175
|
+
"CreationDate",
|
|
176
|
+
"LastModifiedDate",
|
|
177
|
+
"SchemaAttributes",
|
|
178
|
+
"UsernameAttributes",
|
|
179
|
+
"EstimatedNumberOfUsers"
|
|
180
|
+
].filter(k => Stdlib_Option.isSome(input[k]))).toEqual([]);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
globalThis.describe("Auth_ActiveRolePoolAttachment.mergedUpdateInput โ detaching", () => {
|
|
185
|
+
let input = merged(undefined);
|
|
186
|
+
globalThis.test("clearing removes our trigger", () => {
|
|
187
|
+
globalThis.expect(Stdlib_Option.flatMap(lambdaConfigOf(input), c => c["PreTokenGeneration"])).toEqual(undefined);
|
|
188
|
+
});
|
|
189
|
+
globalThis.test("clearing leaves the customer's own triggers alone", () => {
|
|
190
|
+
globalThis.expect(Stdlib_Option.flatMap(lambdaConfigOf(input), c => c["PreSignUp"])).toEqual("arn:aws:lambda:eu-west-1:1:function:TheirPreSignUp");
|
|
191
|
+
});
|
|
192
|
+
globalThis.test("clearing still sends the rest of the pool back whole", () => {
|
|
193
|
+
globalThis.expect(input["MfaConfiguration"]).toEqual("ON");
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
globalThis.describe("Auth_ActiveRolePoolAttachment.mergedUpdateInput โ a pool with nothing set", () => {
|
|
198
|
+
globalThis.test("a pool with no LambdaConfig gains one holding only our trigger", () => {
|
|
199
|
+
let input = Auth_ActiveRolePoolAttachment$ReventlessAws.mergedUpdateInput(Object.fromEntries([[
|
|
200
|
+
"Name",
|
|
201
|
+
"Bare"
|
|
202
|
+
]]), "eu-west-1_bare", triggerArn);
|
|
203
|
+
globalThis.expect(lambdaConfigOf(input)).toEqual(Object.fromEntries([[
|
|
204
|
+
"PreTokenGeneration",
|
|
205
|
+
triggerArn
|
|
206
|
+
]]));
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
globalThis.describe("Auth_ActiveRolePoolAttachment.attachedTrigger", () => {
|
|
211
|
+
globalThis.test("reports the trigger a described pool carries", () => {
|
|
212
|
+
globalThis.expect(Auth_ActiveRolePoolAttachment$ReventlessAws.attachedTrigger(merged(triggerArn))).toEqual(triggerArn);
|
|
213
|
+
});
|
|
214
|
+
globalThis.test("reports none when the pool carries no trigger", () => {
|
|
215
|
+
globalThis.expect(Auth_ActiveRolePoolAttachment$ReventlessAws.attachedTrigger(Object.fromEntries([[
|
|
216
|
+
"Name",
|
|
217
|
+
"Bare"
|
|
218
|
+
]]))).toEqual(undefined);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
globalThis.describe("Auth_ActiveRolePoolAttachment.probeVerdict", () => {
|
|
223
|
+
globalThis.test("refuses a function that died before reaching its handler", () => {
|
|
224
|
+
globalThis.expect(Auth_ActiveRolePoolAttachment$ReventlessAws.probeVerdict("Unhandled", `{"errorType":"Error","errorMessage":"Cannot find package '@reventlessdev/reventless-core' imported from /var/task/node_modules/@reventlessdev/reventless-aws/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res.mjs","code":"ERR_MODULE_NOT_FOUND"}`)).toEqual({
|
|
225
|
+
TAG: "Crashed",
|
|
226
|
+
_0: "Unhandled"
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
globalThis.test("accepts a function that hands the event back", () => {
|
|
230
|
+
globalThis.expect(Auth_ActiveRolePoolAttachment$ReventlessAws.probeVerdict(undefined, `{"request":{"groupConfiguration":{"groupsToOverride":[]}},"response":{}}`)).toEqual("Healthy");
|
|
231
|
+
});
|
|
232
|
+
globalThis.test("refuses a successful call that returns something else", () => {
|
|
233
|
+
globalThis.expect(Auth_ActiveRolePoolAttachment$ReventlessAws.probeVerdict(undefined, `{"ok":true}`)).toEqual("NotAnEvent");
|
|
234
|
+
});
|
|
235
|
+
globalThis.test("treats an unparseable payload as not an event", () => {
|
|
236
|
+
globalThis.expect(Auth_ActiveRolePoolAttachment$ReventlessAws.probeVerdict(undefined, "<html>502</html>")).toEqual("NotAnEvent");
|
|
237
|
+
});
|
|
238
|
+
globalThis.test("treats an empty payload as not an event", () => {
|
|
239
|
+
globalThis.expect(Auth_ActiveRolePoolAttachment$ReventlessAws.probeVerdict(undefined, "")).toEqual("NotAnEvent");
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
globalThis.describe("Auth_ActiveRolePoolAttachment.probeEvent", () => {
|
|
244
|
+
let event = Stdlib_JSON.Decode.object(Auth_ActiveRolePoolAttachment$ReventlessAws.probeEvent("eu-west-1_Example"));
|
|
245
|
+
globalThis.test("is shaped like the V1_0 event Cognito sends", () => {
|
|
246
|
+
globalThis.expect(Stdlib_Option.isSome(Stdlib_Option.flatMap(Stdlib_Option.flatMap(event, o => o["request"]), Stdlib_JSON.Decode.object))).toBe(true);
|
|
247
|
+
});
|
|
248
|
+
globalThis.test("presents no groups to narrow", () => {
|
|
249
|
+
globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(event, o => o["request"]), Stdlib_JSON.Decode.object), r => r["groupConfiguration"]), Stdlib_JSON.Decode.object), g => g["groupsToOverride"]), Stdlib_JSON.Decode.array)).toEqual([]);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
let Attachment;
|
|
254
|
+
|
|
255
|
+
export {
|
|
256
|
+
Attachment,
|
|
257
|
+
str,
|
|
258
|
+
describedPool,
|
|
259
|
+
merged,
|
|
260
|
+
triggerArn,
|
|
261
|
+
lambdaConfigOf,
|
|
262
|
+
}
|
|
263
|
+
/* Not a pure module */
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// The subset rule on the Cognito path's write door, driven by the conformance
|
|
4
|
+
// table in `ReventlessCore.Auth_ActiveRole`.
|
|
5
|
+
//
|
|
6
|
+
// That table is the shared artifact ยง6 of the plan calls for: the check itself is
|
|
7
|
+
// written twice (this one runs in a resolver Lambda against an AppSync event, the
|
|
8
|
+
// trigger's runs in Cognito's runtime against Cognito's event shape), so the cases
|
|
9
|
+
// are what keeps the two from drifting on the question that carries the security.
|
|
10
|
+
|
|
11
|
+
module Ops = Auth_ActiveRoleStore_Ops
|
|
12
|
+
module Contract = ReventlessCore.Auth_ActiveRole
|
|
13
|
+
|
|
14
|
+
describe("Auth_ActiveRoleStore_Ops.mayActAs โ the conformance table", () => {
|
|
15
|
+
Contract.conformanceCases->Array.forEach(({label, membership, requested, expected}) =>
|
|
16
|
+
switch requested {
|
|
17
|
+
// "no role requested" has no subset decision to make โ it is the clearing
|
|
18
|
+
// path, exercised against the handler rather than the predicate.
|
|
19
|
+
| None => ()
|
|
20
|
+
| Some(role) =>
|
|
21
|
+
testSync(label, () =>
|
|
22
|
+
expect(Ops.mayActAs(~membership, ~requested=role))->toBe(expected->Option.isSome)
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// The table above is the contract; these are the properties it encodes, asserted
|
|
29
|
+
// directly so a future edit that weakens a case is visible as a failure here too.
|
|
30
|
+
describe("Auth_ActiveRoleStore_Ops.mayActAs โ narrowing only", () => {
|
|
31
|
+
testSync("a role outside membership is never permitted", () =>
|
|
32
|
+
expect(Ops.mayActAs(~membership=["Shopper"], ~requested="Admin"))->toBe(false)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
testSync("membership is matched exactly, so no case folding widens it", () =>
|
|
36
|
+
expect(Ops.mayActAs(~membership=["Admin"], ~requested="ADMIN"))->toBe(false)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
testSync("an empty membership permits nothing at all", () =>
|
|
40
|
+
expect(Ops.mayActAs(~membership=[], ~requested="Shopper"))->toBe(false)
|
|
41
|
+
)
|
|
42
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
import * as Auth_ActiveRole$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Auth/Auth_ActiveRole.res.mjs";
|
|
5
|
+
import * as Auth_ActiveRoleStore_Ops$ReventlessAws from "../src/adapter/Auth/Auth_ActiveRoleStore_Ops.res.mjs";
|
|
6
|
+
|
|
7
|
+
globalThis.describe("Auth_ActiveRoleStore_Ops.mayActAs โ the conformance table", () => {
|
|
8
|
+
Auth_ActiveRole$ReventlessCore.conformanceCases.forEach(param => {
|
|
9
|
+
let requested = param.requested;
|
|
10
|
+
if (requested === undefined) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
let expected = param.expected;
|
|
14
|
+
let membership = param.membership;
|
|
15
|
+
globalThis.test(param.label, () => {
|
|
16
|
+
globalThis.expect(Auth_ActiveRoleStore_Ops$ReventlessAws.mayActAs(membership, requested)).toBe(Stdlib_Option.isSome(expected));
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
globalThis.describe("Auth_ActiveRoleStore_Ops.mayActAs โ narrowing only", () => {
|
|
22
|
+
globalThis.test("a role outside membership is never permitted", () => {
|
|
23
|
+
globalThis.expect(Auth_ActiveRoleStore_Ops$ReventlessAws.mayActAs(["Shopper"], "Admin")).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
globalThis.test("membership is matched exactly, so no case folding widens it", () => {
|
|
26
|
+
globalThis.expect(Auth_ActiveRoleStore_Ops$ReventlessAws.mayActAs(["Admin"], "ADMIN")).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
globalThis.test("an empty membership permits nothing at all", () => {
|
|
29
|
+
globalThis.expect(Auth_ActiveRoleStore_Ops$ReventlessAws.mayActAs([], "Shopper")).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
let Ops;
|
|
34
|
+
|
|
35
|
+
let Contract;
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
Ops,
|
|
39
|
+
Contract,
|
|
40
|
+
}
|
|
41
|
+
/* Not a pure module */
|