@reventlessdev/reventless-aws 3.0.0-alpha.319 → 3.0.0-alpha.320

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/package.json +9 -5
  3. package/rescript.json +4 -0
  4. package/run-provision-identity.mjs +3 -0
  5. package/scripts/ProvisionIdentity.res +303 -0
  6. package/scripts/ProvisionIdentity.res.mjs +376 -0
  7. package/src/Platform.res +54 -30
  8. package/src/Platform.res.mjs +34 -32
  9. package/src/Platform_Stack.res +102 -19
  10. package/src/Platform_Stack.res.mjs +43 -7
  11. package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res +267 -32
  12. package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res.mjs +124 -14
  13. package/src/adapter/Auth/Auth_ActiveRoleStore.res +93 -5
  14. package/src/adapter/Auth/Auth_ActiveRoleStore.res.mjs +53 -4
  15. package/src/adapter/Auth/Auth_ActiveRoleStore_Ops.res +50 -4
  16. package/src/adapter/Auth/Auth_ActiveRoleStore_Ops.res.mjs +24 -3
  17. package/src/adapter/Auth/Auth_ActiveRoleStore_Schema.res +92 -0
  18. package/src/adapter/Auth/Auth_ActiveRoleStore_Schema.res.mjs +49 -0
  19. package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res +21 -7
  20. package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res.mjs +15 -7
  21. package/src/util/Util_AwsError.res +56 -0
  22. package/src/util/Util_AwsError.res.mjs +59 -0
  23. package/src/util/Util_ShellConfig.res +28 -0
  24. package/src/util/Util_ShellConfig.res.mjs +22 -0
  25. package/tests/Auth_ActiveRolePoolAttachmentTest.res +175 -0
  26. package/tests/Auth_ActiveRolePoolAttachmentTest.res.mjs +136 -0
  27. package/tests/Auth_ActiveRoleStoreTest.res +65 -0
  28. package/tests/Auth_ActiveRoleStoreTest.res.mjs +48 -0
  29. package/tests/Auth_ActiveRoleStore_SchemaTest.res +98 -0
  30. package/tests/Auth_ActiveRoleStore_SchemaTest.res.mjs +97 -0
  31. package/tests/AwsErrorFixtures.mjs +16 -0
  32. package/tests/ProvisionIdentityTest.res +109 -0
  33. package/tests/ProvisionIdentityTest.res.mjs +132 -0
  34. package/tests/Util_AwsErrorTest.res +127 -0
  35. package/tests/Util_AwsErrorTest.res.mjs +76 -0
  36. package/tests/Util_ShellConfigTest.res +51 -0
  37. package/tests/Util_ShellConfigTest.res.mjs +42 -0
@@ -362,6 +362,142 @@ globalThis.describe("Auth_ActiveRolePoolAttachment.probeEvent", () => {
362
362
  globalThis.test("presents no groups to narrow", () => {
363
363
  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([]);
364
364
  });
365
+ globalThis.test("carries the caller context the handler keys its read on", () => {
366
+ globalThis.expect(Stdlib_Option.isSome(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(event, o => o["callerContext"]), Stdlib_JSON.Decode.object), c => c["clientId"]), Stdlib_JSON.Decode.string))).toBe(true);
367
+ });
368
+ globalThis.test("the probe's subject and client cannot collide with a real row", () => {
369
+ globalThis.expect(Auth_ActiveRolePoolAttachment$ReventlessAws.probeSubject === Auth_ActiveRolePoolAttachment$ReventlessAws.probeClientId).toBe(false);
370
+ });
371
+ });
372
+
373
+ globalThis.describe("Auth_ActiveRolePoolAttachment.attachedTriggerArn", () => {
374
+ let theirArn = "arn:aws:lambda:eu-west-1:1:function:TheirOwnPreToken";
375
+ let withConfig = version => Object.fromEntries([[
376
+ "LambdaConfig",
377
+ Object.fromEntries([[
378
+ "PreTokenGenerationConfig",
379
+ Object.fromEntries([
380
+ [
381
+ "LambdaArn",
382
+ theirArn
383
+ ],
384
+ [
385
+ "LambdaVersion",
386
+ version
387
+ ]
388
+ ])
389
+ ]])
390
+ ]]);
391
+ globalThis.test("an empty pool holds nothing", () => {
392
+ globalThis.expect(Auth_ActiveRolePoolAttachment$ReventlessAws.attachedTriggerArn(Object.fromEntries([[
393
+ "Name",
394
+ "Bare"
395
+ ]]))).toEqual(undefined);
396
+ });
397
+ globalThis.test("reports the trigger a pool carries", () => {
398
+ globalThis.expect(Auth_ActiveRolePoolAttachment$ReventlessAws.attachedTriggerArn(withConfig("V1_0"))).toEqual(theirArn);
399
+ });
400
+ globalThis.test("a foreign trigger at another version is still occupying the slot", () => {
401
+ globalThis.expect([
402
+ Auth_ActiveRolePoolAttachment$ReventlessAws.attachedTriggerArn(withConfig("V2_0")),
403
+ Auth_ActiveRolePoolAttachment$ReventlessAws.attachedTrigger(withConfig("V2_0"))
404
+ ]).toEqual([
405
+ theirArn,
406
+ undefined
407
+ ]);
408
+ });
409
+ globalThis.test("falls back to the legacy field on a pool carrying only that", () => {
410
+ globalThis.expect(Auth_ActiveRolePoolAttachment$ReventlessAws.attachedTriggerArn(Object.fromEntries([[
411
+ "LambdaConfig",
412
+ Object.fromEntries([[
413
+ "PreTokenGeneration",
414
+ theirArn
415
+ ]])
416
+ ]]))).toEqual(theirArn);
417
+ });
418
+ });
419
+
420
+ globalThis.describe("Auth_ActiveRolePoolAttachment.classifySlot", () => {
421
+ let ours = "arn:aws:lambda:eu-west-1:1:function:OurTrigger";
422
+ let theirs = "arn:aws:lambda:eu-west-1:1:function:OtherStackTrigger";
423
+ let store = "ReventlessActiveRoleStore-eu-west-1_x";
424
+ let classify = (attachedArn, attachedStore) => Auth_ActiveRolePoolAttachment$ReventlessAws.classifySlot(attachedArn, ours, store, attachedStore);
425
+ globalThis.test("an empty slot is free to take", () => {
426
+ globalThis.expect(classify(undefined, undefined)).toEqual("Vacant");
427
+ });
428
+ globalThis.test("an empty ARN is an empty slot, not a foreign trigger", () => {
429
+ globalThis.expect(classify("", undefined)).toEqual("Vacant");
430
+ });
431
+ globalThis.test("our own trigger is ours to re-attach", () => {
432
+ globalThis.expect(classify(ours, undefined)).toEqual("Ours");
433
+ });
434
+ globalThis.test("another deployment's trigger on the same store serves both", () => {
435
+ globalThis.expect(classify(theirs, store)).toEqual({
436
+ TAG: "SharedWith",
437
+ _0: theirs
438
+ });
439
+ });
440
+ globalThis.test("another deployment's trigger on a different store is the defect", () => {
441
+ globalThis.expect(classify(theirs, "ActiveRoleStore-829c96f")).toEqual({
442
+ TAG: "DifferentStore",
443
+ arn: theirs,
444
+ theirStore: "ActiveRoleStore-829c96f"
445
+ });
446
+ });
447
+ globalThis.test("a trigger with no store of ours is foreign", () => {
448
+ globalThis.expect(classify(theirs, undefined)).toEqual({
449
+ TAG: "Foreign",
450
+ _0: theirs
451
+ });
452
+ });
453
+ });
454
+
455
+ globalThis.describe("Auth_ActiveRolePoolAttachment.refusalFor", () => {
456
+ let store = "ReventlessActiveRoleStore-eu-west-1_x";
457
+ globalThis.test("the three attachable slots produce no refusal", () => {
458
+ globalThis.expect([
459
+ Auth_ActiveRolePoolAttachment$ReventlessAws.refusalFor("Vacant", "eu-west-1_x", store),
460
+ Auth_ActiveRolePoolAttachment$ReventlessAws.refusalFor("Ours", "eu-west-1_x", store),
461
+ Auth_ActiveRolePoolAttachment$ReventlessAws.refusalFor({
462
+ TAG: "SharedWith",
463
+ _0: "arn:aws:lambda:eu-west-1:1:function:Other"
464
+ }, "eu-west-1_x", store)
465
+ ]).toEqual([
466
+ undefined,
467
+ undefined,
468
+ undefined
469
+ ]);
470
+ });
471
+ globalThis.test("a disagreeing store is refused, naming both stores", () => {
472
+ let message = Stdlib_Option.getOr(Auth_ActiveRolePoolAttachment$ReventlessAws.refusalFor({
473
+ TAG: "DifferentStore",
474
+ arn: "arn:aws:lambda:eu-west-1:1:function:Other",
475
+ theirStore: "ActiveRoleStore-829c96f"
476
+ }, "eu-west-1_x", store), "");
477
+ globalThis.expect([
478
+ message.includes(store),
479
+ message.includes("ActiveRoleStore-829c96f"),
480
+ message.includes("eu-west-1_x")
481
+ ]).toEqual([
482
+ true,
483
+ true,
484
+ true
485
+ ]);
486
+ });
487
+ globalThis.test("a foreign trigger is refused, naming what is attached", () => {
488
+ let arn = "arn:aws:lambda:eu-west-1:1:function:TheirClaimsEnrichment";
489
+ let message = Stdlib_Option.getOr(Auth_ActiveRolePoolAttachment$ReventlessAws.refusalFor({
490
+ TAG: "Foreign",
491
+ _0: arn
492
+ }, "eu-west-1_x", store), "");
493
+ globalThis.expect([
494
+ message.includes(arn),
495
+ message.includes("lambda:GetFunctionConfiguration")
496
+ ]).toEqual([
497
+ true,
498
+ true
499
+ ]);
500
+ });
365
501
  });
366
502
 
367
503
  let Attachment;
@@ -72,3 +72,68 @@ describe("Auth_ActiveRoleStore_Ops.cognitoLookupName", () => {
72
72
  expect(Ops.cognitoLookupName(~identity={}))->toEqual(None)
73
73
  )
74
74
  })
75
+
76
+ // Which store this deployment reads and writes. Two cases and no third: a stack
77
+ // that creates its own provider owns everything attached to it, and a stack given
78
+ // a provider owns none of it. See
79
+ // [docs/plans/active-role-store-scoped-to-the-pool.md].
80
+ describe("Auth_ActiveRoleStore.chooseStore", () => {
81
+ testSync("a stack that creates its own provider keeps its own table", () =>
82
+ expect(Auth_ActiveRoleStore.chooseStore(~identityProviderId=None))->toEqual(
83
+ Auth_ActiveRoleStore.StackScoped,
84
+ )
85
+ )
86
+
87
+ testSync("a stack given a provider reads that provider's derived store", () =>
88
+ expect(Auth_ActiveRoleStore.chooseStore(~identityProviderId=Some("eu-west-1_CQTwafSeX")))
89
+ ->toEqual(
90
+ Auth_ActiveRoleStore.ProviderScoped("ReventlessActiveRoleStore-eu-west-1_CQTwafSeX"),
91
+ )
92
+ )
93
+
94
+ // 🚨 The defect, expressed as the property that now prevents it. Two platform
95
+ // stacks on one provider previously deployed a table each, so the pool's single
96
+ // trigger read rows the other's resolver wrote — every switch succeeding and
97
+ // doing nothing. There is no configuration either could carry that would make
98
+ // them disagree.
99
+ testSync("two deployments on one provider cannot choose different stores", () =>
100
+ expect(Auth_ActiveRoleStore.chooseStore(~identityProviderId=Some("eu-west-1_Shared")))
101
+ ->toEqual(Auth_ActiveRoleStore.chooseStore(~identityProviderId=Some("eu-west-1_Shared")))
102
+ )
103
+ })
104
+
105
+ // The second half of the row key. Every other absent field in this handler has a
106
+ // defensible default; this one does not, and the test that matters is the one
107
+ // asserting it refuses rather than substituting something.
108
+ describe("Auth_ActiveRoleStore_Ops.appClientId", () => {
109
+ testSync("the app client the authorizer resolved is the one used", () =>
110
+ expect(Ops.appClientId(~identity={sub: "sub-1", clientId: Value("7cl13nt")}))->toEqual(
111
+ Some("7cl13nt"),
112
+ )
113
+ )
114
+
115
+ // 🚨 The trigger keys its read on the client id Cognito hands it, so a write
116
+ // under any substitute — a constant, the subject, the empty string — is a row
117
+ // the trigger never finds. Refusing is the only answer that does not recreate
118
+ // the defect one level down.
119
+ testSync("an absent client id yields nothing to key a row on", () =>
120
+ expect(Ops.appClientId(~identity={sub: "sub-1"}))->toEqual(None)
121
+ )
122
+
123
+ // The resolver sends `appClientId(id)`, which returns null when the claims are
124
+ // not there to read — so null is the shape that actually arrives, not a missing
125
+ // field.
126
+ testSync("an explicitly null client id is not a client id", () =>
127
+ expect(Ops.appClientId(~identity={sub: "sub-1", clientId: Null}))->toEqual(None)
128
+ )
129
+
130
+ testSync("a blank client id is not a client id either", () =>
131
+ expect(Ops.appClientId(~identity={sub: "sub-1", clientId: Value(" ")}))->toEqual(None)
132
+ )
133
+
134
+ // The subject is deliberately not a fallback: it would key every platform's row
135
+ // identically and undo the per-platform narrowing the pair key exists for.
136
+ testSync("the subject is never substituted for the app client", () =>
137
+ expect(Ops.appClientId(~identity={sub: "sub-1", clientId: Null}))->toEqual(None)
138
+ )
139
+ })
@@ -2,6 +2,7 @@
2
2
 
3
3
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
4
  import * as Auth_ActiveRole$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Auth/Auth_ActiveRole.res.mjs";
5
+ import * as Auth_ActiveRoleStore$ReventlessAws from "../src/adapter/Auth/Auth_ActiveRoleStore.res.mjs";
5
6
  import * as Auth_ActiveRoleStore_Ops$ReventlessAws from "../src/adapter/Auth/Auth_ActiveRoleStore_Ops.res.mjs";
6
7
 
7
8
  globalThis.describe("Auth_ActiveRoleStore_Ops.mayActAs — the conformance table", () => {
@@ -59,6 +60,53 @@ globalThis.describe("Auth_ActiveRoleStore_Ops.cognitoLookupName", () => {
59
60
  });
60
61
  });
61
62
 
63
+ globalThis.describe("Auth_ActiveRoleStore.chooseStore", () => {
64
+ globalThis.test("a stack that creates its own provider keeps its own table", () => {
65
+ globalThis.expect(Auth_ActiveRoleStore$ReventlessAws.chooseStore(undefined)).toEqual("StackScoped");
66
+ });
67
+ globalThis.test("a stack given a provider reads that provider's derived store", () => {
68
+ globalThis.expect(Auth_ActiveRoleStore$ReventlessAws.chooseStore("eu-west-1_CQTwafSeX")).toEqual({
69
+ TAG: "ProviderScoped",
70
+ _0: "ReventlessActiveRoleStore-eu-west-1_CQTwafSeX"
71
+ });
72
+ });
73
+ globalThis.test("two deployments on one provider cannot choose different stores", () => {
74
+ globalThis.expect(Auth_ActiveRoleStore$ReventlessAws.chooseStore("eu-west-1_Shared")).toEqual(Auth_ActiveRoleStore$ReventlessAws.chooseStore("eu-west-1_Shared"));
75
+ });
76
+ });
77
+
78
+ globalThis.describe("Auth_ActiveRoleStore_Ops.appClientId", () => {
79
+ globalThis.test("the app client the authorizer resolved is the one used", () => {
80
+ globalThis.expect(Auth_ActiveRoleStore_Ops$ReventlessAws.appClientId({
81
+ sub: "sub-1",
82
+ clientId: "7cl13nt"
83
+ })).toEqual("7cl13nt");
84
+ });
85
+ globalThis.test("an absent client id yields nothing to key a row on", () => {
86
+ globalThis.expect(Auth_ActiveRoleStore_Ops$ReventlessAws.appClientId({
87
+ sub: "sub-1"
88
+ })).toEqual(undefined);
89
+ });
90
+ globalThis.test("an explicitly null client id is not a client id", () => {
91
+ globalThis.expect(Auth_ActiveRoleStore_Ops$ReventlessAws.appClientId({
92
+ sub: "sub-1",
93
+ clientId: null
94
+ })).toEqual(undefined);
95
+ });
96
+ globalThis.test("a blank client id is not a client id either", () => {
97
+ globalThis.expect(Auth_ActiveRoleStore_Ops$ReventlessAws.appClientId({
98
+ sub: "sub-1",
99
+ clientId: " "
100
+ })).toEqual(undefined);
101
+ });
102
+ globalThis.test("the subject is never substituted for the app client", () => {
103
+ globalThis.expect(Auth_ActiveRoleStore_Ops$ReventlessAws.appClientId({
104
+ sub: "sub-1",
105
+ clientId: null
106
+ })).toEqual(undefined);
107
+ });
108
+ });
109
+
62
110
  let Ops;
63
111
 
64
112
  let Contract;
@@ -0,0 +1,98 @@
1
+ open JestGlobals
2
+
3
+ // The store's identity — its name and its key — is shared by four things that
4
+ // cannot see each other: the deploy, the write door, the pre-token trigger, and
5
+ // the provisioning script an operator runs by hand. Any two of them disagreeing
6
+ // produces one symptom, and it is the symptom this whole area exists to end: a
7
+ // row written where nothing looks for it, so a role switch reports success and
8
+ // changes nothing.
9
+ //
10
+ // These assert the contract itself rather than any one consumer's use of it.
11
+
12
+ module Schema = Auth_ActiveRoleStore_Schema
13
+
14
+ describe("Auth_ActiveRoleStore_Schema.derivedStoreName", () => {
15
+ testSync("names the store after the provider whose trigger reads it", () =>
16
+ expect(Schema.derivedStoreName(~identityProviderId="eu-west-1_CQTwafSeX"))->toBe(
17
+ "ReventlessActiveRoleStore-eu-west-1_CQTwafSeX",
18
+ )
19
+ )
20
+
21
+ // The property the whole design turns on: two platforms on one provider cannot
22
+ // name different stores, so the defect stops being something to detect and
23
+ // becomes something that cannot be expressed.
24
+ testSync("two deployments on one provider derive the same store", () =>
25
+ expect(Schema.derivedStoreName(~identityProviderId="eu-west-1_Shared"))->toBe(
26
+ Schema.derivedStoreName(~identityProviderId="eu-west-1_Shared"),
27
+ )
28
+ )
29
+
30
+ testSync("different providers do not collide", () =>
31
+ expect(
32
+ Schema.derivedStoreName(~identityProviderId="eu-west-1_A") ==
33
+ Schema.derivedStoreName(~identityProviderId="eu-west-1_B"),
34
+ )->toBe(false)
35
+ )
36
+
37
+ // A pool id carries an underscore and a hyphen; DynamoDB accepts both, and a
38
+ // name that needed escaping would make the script and the deploy disagree about
39
+ // what "the same name" is.
40
+ testSync("the derived name is a legal DynamoDB table name", () => {
41
+ let name = Schema.derivedStoreName(~identityProviderId="eu-west-1_CQTwafSeX")
42
+ expect((
43
+ RegExp.test(RegExp.fromString("^[A-Za-z0-9_.-]+$"), name),
44
+ name->String.length >= 3 && name->String.length <= 255,
45
+ ))->toEqual((true, true))
46
+ })
47
+ })
48
+
49
+ describe("Auth_ActiveRoleStore_Schema.keySchemaRefusal", () => {
50
+ let store = "ReventlessActiveRoleStore-eu-west-1_x"
51
+
52
+ testSync("the expected pair is adopted without complaint", () =>
53
+ expect(
54
+ Schema.keySchemaRefusal(~tableName=store, ~actual=[("id", "HASH"), ("clientId", "RANGE")]),
55
+ )->toEqual(None)
56
+ )
57
+
58
+ // DescribeTable promises no ordering, and a refusal that depended on one would
59
+ // reject a table that is perfectly correct.
60
+ testSync("order is not part of the comparison", () =>
61
+ expect(
62
+ Schema.keySchemaRefusal(~tableName=store, ~actual=[("clientId", "RANGE"), ("id", "HASH")]),
63
+ )->toEqual(None)
64
+ )
65
+
66
+ // 🚨 The upgrade case. A store keyed on the subject alone is what every
67
+ // deployment had before the active role became per-platform, and adopting it
68
+ // silently would write rows under a two-part key that nothing reads.
69
+ testSync("the pre-clientId store is refused rather than adopted", () =>
70
+ expect(
71
+ Schema.keySchemaRefusal(~tableName=store, ~actual=[("id", "HASH")])->Option.isSome,
72
+ )->toBe(true)
73
+ )
74
+
75
+ testSync("the refusal names the table, what it found, and what is needed", () => {
76
+ let message = Schema.keySchemaRefusal(~tableName=store, ~actual=[("id", "HASH")])->Option.getOr("")
77
+ expect((
78
+ message->String.includes(store),
79
+ message->String.includes("id:HASH"),
80
+ message->String.includes("clientId:RANGE"),
81
+ ))->toEqual((true, true, true))
82
+ })
83
+
84
+ testSync("a table keyed on something else entirely is refused", () =>
85
+ expect(
86
+ Schema.keySchemaRefusal(
87
+ ~tableName=store,
88
+ ~actual=[("pk", "HASH"), ("sk", "RANGE")],
89
+ )->Option.isSome,
90
+ )->toBe(true)
91
+ )
92
+
93
+ // Not a table anyone would build on purpose — but `DescribeTable`'s KeySchema is
94
+ // optional in the SDK's types, and the caller resolves an absent one to this.
95
+ testSync("an empty key schema is refused, not treated as a match", () =>
96
+ expect(Schema.keySchemaRefusal(~tableName=store, ~actual=[])->Option.isSome)->toBe(true)
97
+ )
98
+ })
@@ -0,0 +1,97 @@
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_ActiveRoleStore_Schema$ReventlessAws from "../src/adapter/Auth/Auth_ActiveRoleStore_Schema.res.mjs";
5
+
6
+ globalThis.describe("Auth_ActiveRoleStore_Schema.derivedStoreName", () => {
7
+ globalThis.test("names the store after the provider whose trigger reads it", () => {
8
+ globalThis.expect(Auth_ActiveRoleStore_Schema$ReventlessAws.derivedStoreName("eu-west-1_CQTwafSeX")).toBe("ReventlessActiveRoleStore-eu-west-1_CQTwafSeX");
9
+ });
10
+ globalThis.test("two deployments on one provider derive the same store", () => {
11
+ globalThis.expect(Auth_ActiveRoleStore_Schema$ReventlessAws.derivedStoreName("eu-west-1_Shared")).toBe(Auth_ActiveRoleStore_Schema$ReventlessAws.derivedStoreName("eu-west-1_Shared"));
12
+ });
13
+ globalThis.test("different providers do not collide", () => {
14
+ globalThis.expect(Auth_ActiveRoleStore_Schema$ReventlessAws.derivedStoreName("eu-west-1_A") === Auth_ActiveRoleStore_Schema$ReventlessAws.derivedStoreName("eu-west-1_B")).toBe(false);
15
+ });
16
+ globalThis.test("the derived name is a legal DynamoDB table name", () => {
17
+ let name = Auth_ActiveRoleStore_Schema$ReventlessAws.derivedStoreName("eu-west-1_CQTwafSeX");
18
+ globalThis.expect([
19
+ new RegExp("^[A-Za-z0-9_.-]+$").test(name),
20
+ name.length >= 3 && name.length <= 255
21
+ ]).toEqual([
22
+ true,
23
+ true
24
+ ]);
25
+ });
26
+ });
27
+
28
+ globalThis.describe("Auth_ActiveRoleStore_Schema.keySchemaRefusal", () => {
29
+ let store = "ReventlessActiveRoleStore-eu-west-1_x";
30
+ globalThis.test("the expected pair is adopted without complaint", () => {
31
+ globalThis.expect(Auth_ActiveRoleStore_Schema$ReventlessAws.keySchemaRefusal(store, [
32
+ [
33
+ "id",
34
+ "HASH"
35
+ ],
36
+ [
37
+ "clientId",
38
+ "RANGE"
39
+ ]
40
+ ])).toEqual(undefined);
41
+ });
42
+ globalThis.test("order is not part of the comparison", () => {
43
+ globalThis.expect(Auth_ActiveRoleStore_Schema$ReventlessAws.keySchemaRefusal(store, [
44
+ [
45
+ "clientId",
46
+ "RANGE"
47
+ ],
48
+ [
49
+ "id",
50
+ "HASH"
51
+ ]
52
+ ])).toEqual(undefined);
53
+ });
54
+ globalThis.test("the pre-clientId store is refused rather than adopted", () => {
55
+ globalThis.expect(Stdlib_Option.isSome(Auth_ActiveRoleStore_Schema$ReventlessAws.keySchemaRefusal(store, [[
56
+ "id",
57
+ "HASH"
58
+ ]]))).toBe(true);
59
+ });
60
+ globalThis.test("the refusal names the table, what it found, and what is needed", () => {
61
+ let message = Stdlib_Option.getOr(Auth_ActiveRoleStore_Schema$ReventlessAws.keySchemaRefusal(store, [[
62
+ "id",
63
+ "HASH"
64
+ ]]), "");
65
+ globalThis.expect([
66
+ message.includes(store),
67
+ message.includes("id:HASH"),
68
+ message.includes("clientId:RANGE")
69
+ ]).toEqual([
70
+ true,
71
+ true,
72
+ true
73
+ ]);
74
+ });
75
+ globalThis.test("a table keyed on something else entirely is refused", () => {
76
+ globalThis.expect(Stdlib_Option.isSome(Auth_ActiveRoleStore_Schema$ReventlessAws.keySchemaRefusal(store, [
77
+ [
78
+ "pk",
79
+ "HASH"
80
+ ],
81
+ [
82
+ "sk",
83
+ "RANGE"
84
+ ]
85
+ ]))).toBe(true);
86
+ });
87
+ globalThis.test("an empty key schema is refused, not treated as a match", () => {
88
+ globalThis.expect(Stdlib_Option.isSome(Auth_ActiveRoleStore_Schema$ReventlessAws.keySchemaRefusal(store, []))).toBe(true);
89
+ });
90
+ });
91
+
92
+ let Schema;
93
+
94
+ export {
95
+ Schema,
96
+ }
97
+ /* Not a pure module */
@@ -0,0 +1,16 @@
1
+ // Throws the shape an AWS SDK v3 client actually throws, so a test catches it
2
+ // through the same path the real code does.
3
+ //
4
+ // It has to *throw* rather than return: ReScript's `exn` for a JS error is the
5
+ // wrapper its own catch builds, not the bare `Error` object. A fixture that
6
+ // returned one would be a different value than any real call produces, and a test
7
+ // built on it would prove nothing about the code that catches them.
8
+ //
9
+ // Here rather than in a `%raw` inside the test, per the repo convention that
10
+ // untyped reflection lives in a companion `.mjs` bound through `@module`.
11
+
12
+ export const throwAwsError = (name, message) => {
13
+ const error = new Error(message)
14
+ error.name = name
15
+ throw error
16
+ }
@@ -0,0 +1,109 @@
1
+ open JestGlobals
2
+
3
+ // Argument handling for the operator script. Testable at all only because the
4
+ // module no longer calls `main` at the top level — `run-provision-identity.mjs`
5
+ // does. While it ran itself on import, nothing in here could be reached, and
6
+ // that is how a guard that could never match shipped in the same file.
7
+ //
8
+ // What these cover is the one thing this script does that is hard to undo:
9
+ // deciding whether to *adopt* a user pool or *create* one.
10
+
11
+ module Provision = ProvisionIdentity
12
+
13
+ describe("ProvisionIdentity.parseArgs", () => {
14
+ testSync("no arguments means adopt-or-create the default name", () =>
15
+ expect(Provision.parseArgs([]))->toEqual(
16
+ Ok({
17
+ Provision.poolName: Provision.defaultPoolName,
18
+ providerId: None,
19
+ help: false,
20
+ }),
21
+ )
22
+ )
23
+
24
+ testSync("--provider-id names an existing pool", () =>
25
+ expect(Provision.parseArgs(["--provider-id", "eu-west-1_AbCdEfGhI"]))->toEqual(
26
+ Ok({
27
+ Provision.poolName: Provision.defaultPoolName,
28
+ providerId: Some("eu-west-1_AbCdEfGhI"),
29
+ help: false,
30
+ }),
31
+ )
32
+ )
33
+
34
+ testSync("--name selects which pool to adopt by name", () =>
35
+ expect(Provision.parseArgs(["--name", "examples-dev"]))->toEqual(
36
+ Ok({Provision.poolName: "examples-dev", providerId: None, help: false}),
37
+ )
38
+ )
39
+
40
+ testSync("both together are accepted, and the id wins at resolve time", () =>
41
+ expect(Provision.parseArgs(["--name", "ignored", "--provider-id", "eu-west-1_x"]))->toEqual(
42
+ Ok({Provision.poolName: "ignored", providerId: Some("eu-west-1_x"), help: false}),
43
+ )
44
+ )
45
+
46
+ // 🚨 The reason unknown flags are refused rather than skipped. A misspelled
47
+ // `--provider-id` that parsed as "absent" would silently take the *create* path
48
+ // and stand up a second user pool beside the one the operator meant to extend.
49
+ testSync("a misspelled flag is refused, not ignored", () =>
50
+ expect(Provision.parseArgs(["--provider_id", "eu-west-1_x"]))->toEqual(
51
+ Error(`unknown argument "--provider_id"`),
52
+ )
53
+ )
54
+
55
+ // Same hazard from the other direction: a flag whose value was left off must
56
+ // not read as "no id given".
57
+ testSync("a flag with no value is refused", () =>
58
+ expect(Provision.parseArgs(["--provider-id"]))->toEqual(
59
+ Error("--provider-id needs a value"),
60
+ )
61
+ )
62
+
63
+ testSync("--name with no value is refused too", () =>
64
+ expect(Provision.parseArgs(["--name"]))->toEqual(Error("--name needs a value"))
65
+ )
66
+
67
+ testSync("--help and -h both ask for the usage text", () =>
68
+ expect((
69
+ Provision.parseArgs(["--help"])->Result.map(a => a.help),
70
+ Provision.parseArgs(["-h"])->Result.map(a => a.help),
71
+ ))->toEqual((Ok(true), Ok(true)))
72
+ )
73
+
74
+ // The first error stops the scan rather than the last one winning: an operator
75
+ // fixing what they are told about should not then meet a second surprise.
76
+ testSync("the first refusal is the one reported", () =>
77
+ expect(Provision.parseArgs(["--nope", "--also-nope"]))->toEqual(
78
+ Error(`unknown argument "--nope"`),
79
+ )
80
+ )
81
+ })
82
+
83
+ // The pool this script creates when it creates one. Asserted because a pool is
84
+ // the one thing here that outlives every stack — nothing in a `pulumi destroy`
85
+ // touches it — so its settings are not a detail an operator can revise cheaply
86
+ // once accounts exist in it.
87
+ describe("ProvisionIdentity.poolSettings", () => {
88
+ let settings = Provision.poolSettings(~poolName="MyIdentity")
89
+
90
+ testSync("carries the name it was asked for", () =>
91
+ expect(settings.poolName)->toBe("MyIdentity")
92
+ )
93
+
94
+ testSync("matches what auto mode declares: email sign-in, no MFA, admin-only", () =>
95
+ expect((
96
+ settings.usernameAttributes,
97
+ settings.mfaConfiguration,
98
+ settings.adminCreateUserConfig->Option.flatMap(c => c.allowAdminCreateUserOnly),
99
+ ))->toEqual((Some(["email"]), Some("OFF"), Some(true)))
100
+ )
101
+
102
+ testSync("keeps the 12-character password policy", () =>
103
+ expect(
104
+ settings.policies
105
+ ->Option.flatMap(p => p.passwordPolicy)
106
+ ->Option.flatMap(p => p.minimumLength),
107
+ )->toEqual(Some(12))
108
+ )
109
+ })