@reventlessdev/reventless-aws 3.0.0-alpha.346 → 3.0.0-alpha.348

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 (80) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/package.json +17 -13
  3. package/rescript.json +2 -1
  4. package/run-bake-manifest.mjs +3 -0
  5. package/run-deploy-app.mjs +3 -0
  6. package/scripts/BakeManifest.res +395 -0
  7. package/scripts/BakeManifest.res.mjs +487 -0
  8. package/scripts/DeployApp.res +708 -0
  9. package/scripts/DeployApp.res.mjs +1015 -0
  10. package/scripts/DeployManifest.res +84 -0
  11. package/scripts/DeployManifest.res.mjs +112 -0
  12. package/scripts/ProvisionAccounts.res +60 -46
  13. package/scripts/ProvisionAccounts.res.mjs +51 -37
  14. package/scripts/PulumiCli.res +43 -0
  15. package/scripts/PulumiCli.res.mjs +106 -0
  16. package/src/Platform.res +19 -2
  17. package/src/Platform.res.mjs +12 -4
  18. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +1 -5
  19. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +1 -1
  20. package/src/adapter/Api/Platform_UIFragments_Lambda.res +1 -5
  21. package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +1 -2
  22. package/src/adapter/Auth/Auth_ActiveRoleStore.res +1 -5
  23. package/src/adapter/Auth/Auth_ActiveRoleStore.res.mjs +1 -2
  24. package/src/adapter/Auth/Auth_ActiveRoleTrigger.res +1 -5
  25. package/src/adapter/Auth/Auth_ActiveRoleTrigger.res.mjs +1 -2
  26. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res +52 -31
  27. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs +75 -44
  28. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +1 -5
  29. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +1 -2
  30. package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res +1 -5
  31. package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res.mjs +1 -2
  32. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +5 -4
  33. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +1 -1
  34. package/src/adapter/Postgres/PgMigration_Builder.res +1 -1
  35. package/src/adapter/Postgres/PgMigration_Builder.res.mjs +1 -2
  36. package/src/adapter/Runtime/CommandGeneratorEntryPoint_Ops.res +2 -0
  37. package/src/adapter/Runtime/CommandGeneratorEntryPoint_Ops.res.mjs +2 -2
  38. package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +15 -6
  39. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +51 -30
  40. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +31 -14
  41. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +1 -5
  42. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +1 -1
  43. package/src/adapter/StateTopic/StateTopic_AppSync.res +1 -5
  44. package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +1 -1
  45. package/src/adapter/Task/TaskBucket_S3.res +1 -3
  46. package/src/adapter/Task/TaskBucket_S3.res.mjs +1 -1
  47. package/src/adapter/Upload/Upload_Claim_S3.res +1 -5
  48. package/src/adapter/Upload/Upload_Claim_S3.res.mjs +1 -1
  49. package/src/adapter/Upload/Upload_Presign_S3.res +1 -5
  50. package/src/adapter/Upload/Upload_Presign_S3.res.mjs +1 -2
  51. package/src/capability/Capability_Geocoding_AwsLocation.res +5 -1
  52. package/src/capability/Capability_Geocoding_AwsLocation.res.mjs +7 -1
  53. package/src/components/Api/AppSync_SdlDecorate.res +4 -2
  54. package/src/components/Api/AppSync_SdlDecorate.res.mjs +1 -1
  55. package/src/plugin/cloner/ClonerRunner_Fargate.res +1 -5
  56. package/src/plugin/cloner/ClonerRunner_Fargate.res.mjs +1 -1
  57. package/src/plugin/stack/Plugin_Stack.res +4 -0
  58. package/src/plugin/stack/Plugin_Stack.res.mjs +2 -0
  59. package/src/util/Util_DeadLetterQueue.res +1 -5
  60. package/src/util/Util_DeadLetterQueue.res.mjs +1 -2
  61. package/src/util/Util_StoreLayout.res +15 -2
  62. package/src/util/Util_StoreLayout.res.mjs +11 -3
  63. package/tests/BakeManifestTest.res +204 -0
  64. package/tests/BakeManifestTest.res.mjs +259 -0
  65. package/tests/DcbColdStartPartitionTest.res +32 -0
  66. package/tests/DcbColdStartPartitionTest.res.mjs +33 -0
  67. package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res +88 -0
  68. package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res.mjs +112 -0
  69. package/tests/DcbUnresolvedPartitionSlice.res +28 -0
  70. package/tests/DcbUnresolvedPartitionSlice.res.mjs +38 -0
  71. package/tests/DeployAppTest.res +90 -0
  72. package/tests/DeployAppTest.res.mjs +133 -0
  73. package/tests/LambdaLayerLookupTest.res +104 -0
  74. package/tests/LambdaLayerLookupTest.res.mjs +117 -0
  75. package/tests/ProvisionAccountsTest.res +26 -0
  76. package/tests/ProvisionAccountsTest.res.mjs +31 -0
  77. package/tests/Util_StoreLayoutTest.res +6 -0
  78. package/tests/Util_StoreLayoutTest.res.mjs +7 -4
  79. package/tests/dcbColdStartPartition.mjs +66 -0
  80. package/tests/setup/layerArn.cjs +5 -0
@@ -2,8 +2,10 @@
2
2
 
3
3
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
4
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
5
6
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
7
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
8
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
7
9
  import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
8
10
  import * as DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws from "../src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs";
9
11
 
@@ -314,6 +316,116 @@ globalThis.describe("Runtime.buildScanFilter — excludes fence sentinels (Issue
314
316
  });
315
317
  });
316
318
 
319
+ globalThis.describe("Runtime.derivePartitionKey — each event type under its slice's key", () => {
320
+ let partitionTag = {
321
+ TAG: "ByEventType",
322
+ _0: Object.fromEntries([
323
+ [
324
+ "OrderPlaced",
325
+ "orderId"
326
+ ],
327
+ [
328
+ "AddressLinked",
329
+ "customerId"
330
+ ]
331
+ ])
332
+ };
333
+ let thrownMessage = f => {
334
+ try {
335
+ f();
336
+ return "";
337
+ } catch (raw_err) {
338
+ let err = Primitive_exceptions.internalToException(raw_err);
339
+ if (err.RE_EXN_ID === "JsExn") {
340
+ return Stdlib_Option.getOr(Stdlib_JsExn.message(err._1), "");
341
+ }
342
+ throw err;
343
+ }
344
+ };
345
+ globalThis.test("files an event under its own type's key, not its first tag", () => {
346
+ globalThis.expect(DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.derivePartitionKey(partitionTag, "AddressLinked", [
347
+ {
348
+ key: "addressId",
349
+ value: "a1"
350
+ },
351
+ {
352
+ key: "customerId",
353
+ value: "c1"
354
+ }
355
+ ])).toBe("customerId:c1");
356
+ });
357
+ globalThis.test("another type in the same log keeps its own key", () => {
358
+ globalThis.expect(DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.derivePartitionKey(partitionTag, "OrderPlaced", [
359
+ {
360
+ key: "customerId",
361
+ value: "c1"
362
+ },
363
+ {
364
+ key: "orderId",
365
+ value: "o1"
366
+ }
367
+ ])).toBe("orderId:o1");
368
+ });
369
+ globalThis.test("an event type no slice writes throws instead of guessing", () => {
370
+ globalThis.expect(thrownMessage(() => DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.derivePartitionKey(partitionTag, "Unknown", [{
371
+ key: "customerId",
372
+ value: "c1"
373
+ }])).includes("Unknown has no partition key")).toBe(true);
374
+ });
375
+ globalThis.test("an event missing its partition tag throws instead of guessing", () => {
376
+ globalThis.expect(thrownMessage(() => DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.derivePartitionKey(partitionTag, "AddressLinked", [{
377
+ key: "addressId",
378
+ value: "a1"
379
+ }])).includes("carries no customerId tag")).toBe(true);
380
+ });
381
+ globalThis.test("a conditional append puts and fences the event under that key", () => {
382
+ let linked_data = {};
383
+ let linked_tags = [
384
+ {
385
+ key: "addressId",
386
+ value: "a1"
387
+ },
388
+ {
389
+ key: "customerId",
390
+ value: "c1"
391
+ }
392
+ ];
393
+ let linked_meta = testMeta();
394
+ let linked = {
395
+ eventType: "AddressLinked",
396
+ data: linked_data,
397
+ tags: linked_tags,
398
+ meta: linked_meta
399
+ };
400
+ let cond_query = [{
401
+ eventTypes: ["AddressLinked"],
402
+ tags: [{
403
+ key: "customerId",
404
+ value: "c1"
405
+ }]
406
+ }];
407
+ let cond_after = "50";
408
+ let cond = {
409
+ query: cond_query,
410
+ after: cond_after
411
+ };
412
+ let ids = Stdlib_Array.filterMap(Stdlib_Array.filterMap(DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.buildConditionalTransactItems(table, [linked], cond, "100", partitionTag, undefined), it => {
413
+ let match = it.Put;
414
+ let match$1 = it.Update;
415
+ if (match !== undefined) {
416
+ return Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(match.Item), i => i["id"]);
417
+ } else if (match$1 !== undefined) {
418
+ return match$1.Key["id"];
419
+ } else {
420
+ return;
421
+ }
422
+ }), Stdlib_JSON.Decode.string);
423
+ globalThis.expect(ids.includes("customerId:c1")).toBe(true);
424
+ globalThis.expect(ids.includes("fence#customerId:c1")).toBe(true);
425
+ globalThis.expect(ids.some(id => id.includes("addressId"))).toBe(false);
426
+ });
427
+ });
428
+
317
429
  globalThis.describe("Runtime.buildEventPuts", () => {
318
430
  let event = (eventType, tags) => ({
319
431
  eventType: eventType,
@@ -0,0 +1,28 @@
1
+ // A slice whose partition cannot be inferred, for `DcbColdStartPartitionTest`.
2
+ // It writes two ids and reads both off an event nothing in its boundary writes,
3
+ // so neither can be told apart from a reference.
4
+ //
5
+ // Explicit `@s.matches(...)` form: reventless-ppx is not wired into this package.
6
+
7
+ @schema
8
+ type consumedEvent = GadgetImported({gadgetId: string, supplierId: string})
9
+
10
+ @schema
11
+ type command =
12
+ | LinkGadget({
13
+ gadgetId: @s.matches(Reventless.DcbTag.string) string,
14
+ supplierId: @s.matches(Reventless.DcbTag.string) string,
15
+ })
16
+
17
+ @schema
18
+ type error = AlreadyLinked
19
+
20
+ @schema
21
+ type event =
22
+ | GadgetLinked({
23
+ gadgetId: @s.matches(Reventless.DcbTag.string) string,
24
+ supplierId: @s.matches(Reventless.DcbTag.string) string,
25
+ })
26
+
27
+ let name = "LinkGadget"
28
+ let moduleUrl = "cold-start-test://LinkGadget"
@@ -0,0 +1,38 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Sury from "sury";
4
+ import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
5
+
6
+ let consumedEventSchema = Sury.$schema(s => ({
7
+ TAG: "GadgetImported",
8
+ gadgetId: s.m(Sury.string),
9
+ supplierId: s.m(Sury.string)
10
+ }));
11
+
12
+ let commandSchema = Sury.$schema(s => ({
13
+ TAG: "LinkGadget",
14
+ gadgetId: s.m(DcbTag$Reventless.string),
15
+ supplierId: s.m(DcbTag$Reventless.string)
16
+ }));
17
+
18
+ let errorSchema = Sury.literal("AlreadyLinked");
19
+
20
+ let eventSchema = Sury.$schema(s => ({
21
+ TAG: "GadgetLinked",
22
+ gadgetId: s.m(DcbTag$Reventless.string),
23
+ supplierId: s.m(DcbTag$Reventless.string)
24
+ }));
25
+
26
+ let name = "LinkGadget";
27
+
28
+ let moduleUrl = "cold-start-test://LinkGadget";
29
+
30
+ export {
31
+ consumedEventSchema,
32
+ commandSchema,
33
+ errorSchema,
34
+ eventSchema,
35
+ name,
36
+ moduleUrl,
37
+ }
38
+ /* consumedEventSchema Not a pure module */
@@ -0,0 +1,90 @@
1
+ open JestGlobals
2
+
3
+ // What `deploy-app` decides without AWS or Pulumi: its arguments, the checks
4
+ // that stop it before anything is created, and what a new stack starts with.
5
+
6
+ describe("DeployApp.parseArgs", () => {
7
+ testSync("up with the defaults", () =>
8
+ expect(DeployApp.parseArgs(["up"]))->toEqual(
9
+ Ok({DeployApp.command: Some(Up), manifest: None, stack: "dev", help: false}),
10
+ )
11
+ )
12
+
13
+ testSync("down with a manifest and a stack", () =>
14
+ expect(DeployApp.parseArgs(["down", "--manifest", "../m.yaml", "--stack", "try"]))->toEqual(
15
+ Ok({DeployApp.command: Some(Down), manifest: Some("../m.yaml"), stack: "try", help: false}),
16
+ )
17
+ )
18
+
19
+ testSync("a second command is refused", () =>
20
+ expect(DeployApp.parseArgs(["up", "down"]))->toEqual(Error(`unknown argument "down"`))
21
+ )
22
+ })
23
+
24
+ describe("DeployApp checks", () => {
25
+ testSync("Node 22 and later pass", () => {
26
+ expect(DeployApp.checkNode("22.17.1"))->toEqual(None)
27
+ expect(DeployApp.checkNode("24.0.0"))->toEqual(None)
28
+ })
29
+
30
+ testSync("an older Node says which version is needed", () =>
31
+ expect(DeployApp.checkNode("20.11.0")->Option.getOr(""))->toContain("Node 22 or later")
32
+ )
33
+
34
+ testSync("Pulumi Cloud needs no passphrase", () =>
35
+ expect(
36
+ DeployApp.backendNeedsPassphrase(~url=Some("https://app.pulumi.com/me"), ~env=Dict.make()),
37
+ )->toBe(false)
38
+ )
39
+
40
+ testSync("a local backend needs one, unless it is set", () => {
41
+ expect(DeployApp.backendNeedsPassphrase(~url=Some("file://~"), ~env=Dict.make()))->toBe(true)
42
+ expect(
43
+ DeployApp.backendNeedsPassphrase(
44
+ ~url=Some("s3://bucket"),
45
+ ~env=Dict.fromArray([("PULUMI_CONFIG_PASSPHRASE", "")]),
46
+ ),
47
+ )->toBe(false)
48
+ })
49
+ })
50
+
51
+ describe("DeployApp stacks and layer", () => {
52
+ let project = {
53
+ DeployApp.label: "platform",
54
+ dir: "/app/platform-aws",
55
+ projectName: "shop-platform-aws",
56
+ program: "/app/platform-aws/src/Main.res.mjs",
57
+ stackDefaults: Dict.fromArray([("aws:region", "us-east-1"), ("platform:x", "y")]),
58
+ }
59
+
60
+ // The region and the try-out flag come last, so an app default cannot turn a
61
+ // try-out into a stack `down` refuses to remove.
62
+ testSync("a new stack gets the app's defaults, then the region and the try-out flag", () =>
63
+ expect(DeployApp.newStackConfig(~project, ~region="eu-west-1"))->toEqual([
64
+ ("aws:region", "us-east-1"),
65
+ ("platform:x", "y"),
66
+ ("aws:region", "eu-west-1"),
67
+ ("aws-native:region", "eu-west-1"),
68
+ ("reventless:disposable", "true"),
69
+ ])
70
+ )
71
+
72
+ testSync("the release download is named by the package and version", () =>
73
+ expect(DeployApp.releaseUrl("3.0.0-alpha.346"))->toBe(
74
+ "https://github.com/ReventlessDev/reventless-core/releases/download/%40reventlessdev%2Freventless-aws%403.0.0-alpha.346/reventless-layer.zip",
75
+ )
76
+ )
77
+
78
+ testSync("a layer is marked with the release it was published for", () =>
79
+ expect(DeployApp.layerDescription("3.0.0"))->toBe("reventless-aws@3.0.0")
80
+ )
81
+
82
+ testSync("sign-ins line up under each other", () =>
83
+ expect(
84
+ DeployApp.signInLines([
85
+ {username: "admin@example.com", password: "", groups: ["Admin"]},
86
+ {username: "a@example.com", password: "", groups: []},
87
+ ]),
88
+ )->toEqual([" admin@example.com [Admin]", " a@example.com []"])
89
+ )
90
+ })
@@ -0,0 +1,133 @@
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 DeployApp$ReventlessAws from "../scripts/DeployApp.res.mjs";
5
+
6
+ globalThis.describe("DeployApp.parseArgs", () => {
7
+ globalThis.test("up with the defaults", () => {
8
+ globalThis.expect(DeployApp$ReventlessAws.parseArgs(["up"])).toEqual({
9
+ TAG: "Ok",
10
+ _0: {
11
+ command: "Up",
12
+ manifest: undefined,
13
+ stack: "dev",
14
+ help: false
15
+ }
16
+ });
17
+ });
18
+ globalThis.test("down with a manifest and a stack", () => {
19
+ globalThis.expect(DeployApp$ReventlessAws.parseArgs([
20
+ "down",
21
+ "--manifest",
22
+ "../m.yaml",
23
+ "--stack",
24
+ "try"
25
+ ])).toEqual({
26
+ TAG: "Ok",
27
+ _0: {
28
+ command: "Down",
29
+ manifest: "../m.yaml",
30
+ stack: "try",
31
+ help: false
32
+ }
33
+ });
34
+ });
35
+ globalThis.test("a second command is refused", () => {
36
+ globalThis.expect(DeployApp$ReventlessAws.parseArgs([
37
+ "up",
38
+ "down"
39
+ ])).toEqual({
40
+ TAG: "Error",
41
+ _0: `unknown argument "down"`
42
+ });
43
+ });
44
+ });
45
+
46
+ globalThis.describe("DeployApp checks", () => {
47
+ globalThis.test("Node 22 and later pass", () => {
48
+ globalThis.expect(DeployApp$ReventlessAws.checkNode("22.17.1")).toEqual(undefined);
49
+ globalThis.expect(DeployApp$ReventlessAws.checkNode("24.0.0")).toEqual(undefined);
50
+ });
51
+ globalThis.test("an older Node says which version is needed", () => {
52
+ globalThis.expect(Stdlib_Option.getOr(DeployApp$ReventlessAws.checkNode("20.11.0"), "")).toContain("Node 22 or later");
53
+ });
54
+ globalThis.test("Pulumi Cloud needs no passphrase", () => {
55
+ globalThis.expect(DeployApp$ReventlessAws.backendNeedsPassphrase("https://app.pulumi.com/me", {})).toBe(false);
56
+ });
57
+ globalThis.test("a local backend needs one, unless it is set", () => {
58
+ globalThis.expect(DeployApp$ReventlessAws.backendNeedsPassphrase("file://~", {})).toBe(true);
59
+ globalThis.expect(DeployApp$ReventlessAws.backendNeedsPassphrase("s3://bucket", Object.fromEntries([[
60
+ "PULUMI_CONFIG_PASSPHRASE",
61
+ ""
62
+ ]]))).toBe(false);
63
+ });
64
+ });
65
+
66
+ globalThis.describe("DeployApp stacks and layer", () => {
67
+ let project_stackDefaults = Object.fromEntries([
68
+ [
69
+ "aws:region",
70
+ "us-east-1"
71
+ ],
72
+ [
73
+ "platform:x",
74
+ "y"
75
+ ]
76
+ ]);
77
+ let project = {
78
+ label: "platform",
79
+ dir: "/app/platform-aws",
80
+ projectName: "shop-platform-aws",
81
+ program: "/app/platform-aws/src/Main.res.mjs",
82
+ stackDefaults: project_stackDefaults
83
+ };
84
+ globalThis.test("a new stack gets the app's defaults, then the region and the try-out flag", () => {
85
+ globalThis.expect(DeployApp$ReventlessAws.newStackConfig(project, "eu-west-1")).toEqual([
86
+ [
87
+ "aws:region",
88
+ "us-east-1"
89
+ ],
90
+ [
91
+ "platform:x",
92
+ "y"
93
+ ],
94
+ [
95
+ "aws:region",
96
+ "eu-west-1"
97
+ ],
98
+ [
99
+ "aws-native:region",
100
+ "eu-west-1"
101
+ ],
102
+ [
103
+ "reventless:disposable",
104
+ "true"
105
+ ]
106
+ ]);
107
+ });
108
+ globalThis.test("the release download is named by the package and version", () => {
109
+ globalThis.expect(DeployApp$ReventlessAws.releaseUrl("3.0.0-alpha.346")).toBe("https://github.com/ReventlessDev/reventless-core/releases/download/%40reventlessdev%2Freventless-aws%403.0.0-alpha.346/reventless-layer.zip");
110
+ });
111
+ globalThis.test("a layer is marked with the release it was published for", () => {
112
+ globalThis.expect(DeployApp$ReventlessAws.layerDescription("3.0.0")).toBe("reventless-aws@3.0.0");
113
+ });
114
+ globalThis.test("sign-ins line up under each other", () => {
115
+ globalThis.expect(DeployApp$ReventlessAws.signInLines([
116
+ {
117
+ username: "admin@example.com",
118
+ password: "",
119
+ groups: ["Admin"]
120
+ },
121
+ {
122
+ username: "a@example.com",
123
+ password: "",
124
+ groups: []
125
+ }
126
+ ])).toEqual([
127
+ " admin@example.com [Admin]",
128
+ " a@example.com []"
129
+ ]);
130
+ });
131
+ });
132
+
133
+ /* Not a pure module */
@@ -0,0 +1,104 @@
1
+ open JestGlobals
2
+
3
+ module Lambda = PulumiAws.Lambda
4
+
5
+ // A deploy with no layer used to succeed and leave every function failing with
6
+ // "Cannot find package". These pin what the lookup tells apart and what the
7
+ // stopped deploy says.
8
+ let parameter = "/reventless/layer-arn/dev"
9
+
10
+ let cliResult = (~status=?, ~stdout="", ~stderr="", ~error=?) => {
11
+ NodeChildProcess.status: status->Nullable.fromOption,
12
+ stdout: Nullable.make(stdout),
13
+ stderr: Nullable.make(stderr),
14
+ error: error->Nullable.fromOption,
15
+ }
16
+
17
+ describe("Lambda.classifySsmLookup", () => {
18
+ let classify = (~region=Some("eu-west-1"), result) =>
19
+ Lambda.classifySsmLookup(~parameter, ~region, result)
20
+
21
+ testSync("a stored value is the layer", () =>
22
+ expect(
23
+ classify(cliResult(~status=0, ~stdout="arn:aws:lambda:eu-west-1:1:layer:reventless:7\n")),
24
+ )->toEqual(Lambda.Found("arn:aws:lambda:eu-west-1:1:layer:reventless:7"))
25
+ )
26
+
27
+ testSync("ParameterNotFound means there is no layer", () =>
28
+ expect(
29
+ classify(
30
+ cliResult(
31
+ ~status=254,
32
+ ~stderr="\nAn error occurred (ParameterNotFound) when calling the GetParameter operation: \n",
33
+ ),
34
+ ),
35
+ )->toEqual(Lambda.NotFound({parameter, region: Some("eu-west-1")}))
36
+ )
37
+
38
+ testSync("a refused call is not mistaken for a missing layer", () =>
39
+ expect(
40
+ classify(
41
+ cliResult(
42
+ ~status=254,
43
+ ~stderr="\nAn error occurred (AccessDeniedException) when calling the GetParameter operation: no\n",
44
+ ),
45
+ ),
46
+ )->toEqual(
47
+ Lambda.CouldNotLook({
48
+ parameter,
49
+ region: Some("eu-west-1"),
50
+ reason: "An error occurred (AccessDeniedException) when calling the GetParameter operation: no",
51
+ }),
52
+ )
53
+ )
54
+
55
+ testSync("a CLI that cannot be started is not mistaken for a missing layer", () => {
56
+ let error = JsError.make("spawnSync aws ENOENT")->JsError.toJsExn
57
+ let reason = switch classify(
58
+ ~region=None,
59
+ {...cliResult(~error), stdout: Nullable.null, stderr: Nullable.null},
60
+ ) {
61
+ | CouldNotLook({reason}) => Some(reason)
62
+ | _ => None
63
+ }
64
+ expect(reason)->toEqual(Some("the AWS CLI could not be run (spawnSync aws ENOENT)"))
65
+ })
66
+ })
67
+
68
+ describe("Lambda.missingLayerMessage", () => {
69
+ testSync("names the parameter and the region it looked in", () => {
70
+ let message =
71
+ Lambda.NotFound({parameter, region: Some("eu-west-1")})
72
+ ->Lambda.missingLayerMessage
73
+ ->Option.getOr("")
74
+ expect(message)->toContain(parameter)
75
+ expect(message)->toContain("region eu-west-1")
76
+ expect(message)->toContain("REVENTLESS_LAYER_ARN")
77
+ })
78
+
79
+ testSync("says when the CLI's default region was asked", () =>
80
+ expect(
81
+ Lambda.NotFound({parameter, region: None})->Lambda.missingLayerMessage->Option.getOr(""),
82
+ )->toContain("the AWS CLI's default region")
83
+ )
84
+
85
+ testSync("carries the reason a lookup could not be made", () =>
86
+ expect(
87
+ Lambda.CouldNotLook({parameter, region: None, reason: "no credentials"})
88
+ ->Lambda.missingLayerMessage
89
+ ->Option.getOr(""),
90
+ )->toContain("no credentials")
91
+ )
92
+
93
+ testSync("is silent when the layer was found", () =>
94
+ expect(Lambda.Found("arn")->Lambda.missingLayerMessage)->toEqual(None)
95
+ )
96
+ })
97
+
98
+ describe("Lambda.reventlessLayerArn", () => {
99
+ testSync("REVENTLESS_LAYER_ARN wins without asking AWS", () =>
100
+ expect(Lambda.reventlessLayerArn())->toBe(
101
+ "arn:aws:lambda:eu-central-1:000000000000:layer:reventless-test:1",
102
+ )
103
+ )
104
+ })
@@ -0,0 +1,117 @@
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 Stdlib_Nullable from "@rescript/runtime/lib/es6/Stdlib_Nullable.js";
5
+ import * as Lambda$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Lambda/Lambda.res.mjs";
6
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
7
+
8
+ let parameter = "/reventless/layer-arn/dev";
9
+
10
+ function cliResult(status, stdoutOpt, stderrOpt, error) {
11
+ let stdout = stdoutOpt !== undefined ? stdoutOpt : "";
12
+ let stderr = stderrOpt !== undefined ? stderrOpt : "";
13
+ return {
14
+ status: Stdlib_Nullable.fromOption(status),
15
+ stdout: stdout,
16
+ stderr: stderr,
17
+ error: Stdlib_Nullable.fromOption(error)
18
+ };
19
+ }
20
+
21
+ globalThis.describe("Lambda.classifySsmLookup", () => {
22
+ let classify = (regionOpt, result) => {
23
+ let region = regionOpt !== undefined ? Primitive_option.valFromOption(regionOpt) : "eu-west-1";
24
+ return Lambda$PulumiAws.classifySsmLookup(parameter, region, result);
25
+ };
26
+ globalThis.test("a stored value is the layer", () => {
27
+ globalThis.expect(classify(undefined, cliResult(0, "arn:aws:lambda:eu-west-1:1:layer:reventless:7\n", undefined, undefined))).toEqual({
28
+ TAG: "Found",
29
+ _0: "arn:aws:lambda:eu-west-1:1:layer:reventless:7"
30
+ });
31
+ });
32
+ globalThis.test("ParameterNotFound means there is no layer", () => {
33
+ globalThis.expect(classify(undefined, cliResult(254, undefined, "\nAn error occurred (ParameterNotFound) when calling the GetParameter operation: \n", undefined))).toEqual({
34
+ TAG: "NotFound",
35
+ parameter: parameter,
36
+ region: "eu-west-1"
37
+ });
38
+ });
39
+ globalThis.test("a refused call is not mistaken for a missing layer", () => {
40
+ globalThis.expect(classify(undefined, cliResult(254, undefined, "\nAn error occurred (AccessDeniedException) when calling the GetParameter operation: no\n", undefined))).toEqual({
41
+ TAG: "CouldNotLook",
42
+ parameter: parameter,
43
+ region: "eu-west-1",
44
+ reason: "An error occurred (AccessDeniedException) when calling the GetParameter operation: no"
45
+ });
46
+ });
47
+ globalThis.test("a CLI that cannot be started is not mistaken for a missing layer", () => {
48
+ let error = new Error("spawnSync aws ENOENT");
49
+ let init = cliResult(undefined, undefined, undefined, Primitive_option.some(error));
50
+ let match = classify(Primitive_option.some(undefined), {
51
+ status: init.status,
52
+ stdout: null,
53
+ stderr: null,
54
+ error: init.error
55
+ });
56
+ let reason;
57
+ switch (match.TAG) {
58
+ case "Found" :
59
+ case "NotFound" :
60
+ reason = undefined;
61
+ break;
62
+ case "CouldNotLook" :
63
+ reason = match.reason;
64
+ break;
65
+ }
66
+ globalThis.expect(reason).toEqual("the AWS CLI could not be run (spawnSync aws ENOENT)");
67
+ });
68
+ });
69
+
70
+ globalThis.describe("Lambda.missingLayerMessage", () => {
71
+ globalThis.test("names the parameter and the region it looked in", () => {
72
+ let message = Stdlib_Option.getOr(Lambda$PulumiAws.missingLayerMessage({
73
+ TAG: "NotFound",
74
+ parameter: parameter,
75
+ region: "eu-west-1"
76
+ }), "");
77
+ globalThis.expect(message).toContain(parameter);
78
+ globalThis.expect(message).toContain("region eu-west-1");
79
+ globalThis.expect(message).toContain("REVENTLESS_LAYER_ARN");
80
+ });
81
+ globalThis.test("says when the CLI's default region was asked", () => {
82
+ globalThis.expect(Stdlib_Option.getOr(Lambda$PulumiAws.missingLayerMessage({
83
+ TAG: "NotFound",
84
+ parameter: parameter,
85
+ region: undefined
86
+ }), "")).toContain("the AWS CLI's default region");
87
+ });
88
+ globalThis.test("carries the reason a lookup could not be made", () => {
89
+ globalThis.expect(Stdlib_Option.getOr(Lambda$PulumiAws.missingLayerMessage({
90
+ TAG: "CouldNotLook",
91
+ parameter: parameter,
92
+ region: undefined,
93
+ reason: "no credentials"
94
+ }), "")).toContain("no credentials");
95
+ });
96
+ globalThis.test("is silent when the layer was found", () => {
97
+ globalThis.expect(Lambda$PulumiAws.missingLayerMessage({
98
+ TAG: "Found",
99
+ _0: "arn"
100
+ })).toEqual(undefined);
101
+ });
102
+ });
103
+
104
+ globalThis.describe("Lambda.reventlessLayerArn", () => {
105
+ globalThis.test("REVENTLESS_LAYER_ARN wins without asking AWS", () => {
106
+ globalThis.expect(Lambda$PulumiAws.reventlessLayerArn()).toBe("arn:aws:lambda:eu-central-1:000000000000:layer:reventless-test:1");
107
+ });
108
+ });
109
+
110
+ let Lambda;
111
+
112
+ export {
113
+ Lambda,
114
+ parameter,
115
+ cliResult,
116
+ }
117
+ /* Not a pure module */
@@ -140,3 +140,29 @@ describe("ProvisionAccounts.looksLikeEmail", () => {
140
140
  expect(Provision.looksLikeEmail("admin@localhost"))->toBe(false)
141
141
  )
142
142
  })
143
+
144
+ // A refused pool used to leave generated passwords in the file, and the seed then
145
+ // offered accounts that exist nowhere.
146
+ describe("ProvisionAccounts.provision", () => {
147
+ let manifest = `# the cast
148
+ - username: shopper
149
+ password: ""
150
+ groups: [Shopper]
151
+ `
152
+
153
+ test("a refused pool leaves the manifest untouched", async () => {
154
+ let dir = NodeFs.mkdtempSync(NodePath.join([NodeOs.tmpdir(), "provision-accounts-"]))
155
+ let file = NodePath.join([dir, "users.yaml"])
156
+ NodeFs.writeFileSync(file, manifest)
157
+ let refuse = async (~providerId as _, ~usernames as _) => Error("pool signs in on email")
158
+ let outcome = await Provision.provision(
159
+ ~file,
160
+ ~providerId="eu-west-1_Refuses",
161
+ ~checkPool=refuse,
162
+ )
163
+ let after = NodeFs.readFileSync(file)
164
+ NodeFs.rmSync(dir, {recursive: true, force: true})
165
+ expect(outcome)->toEqual(Error("pool signs in on email"))
166
+ expect(after)->toBe(manifest)
167
+ })
168
+ })