@reventlessdev/reventless-core 3.0.0-alpha.260 → 3.0.0-alpha.262

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 (208) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/package.json +10 -10
  3. package/src/Behavior.res +1 -4
  4. package/src/Message.res +17 -14
  5. package/src/Projection.res +51 -25
  6. package/src/RequestContext.res +8 -9
  7. package/src/ResourceAttribution.res +28 -14
  8. package/src/adapter/Adapter.res +48 -4
  9. package/src/adapter/EventLogProvisioning/EventLogProvisioning.res +5 -5
  10. package/src/adapter/Messaging/Messaging_Log_Backend.res +10 -7
  11. package/src/adapter/Messaging/Messaging_Log_Backend.res.mjs +2 -4
  12. package/src/adapter/Monitoring/Monitoring.res +16 -17
  13. package/src/adapter/QueryInterception/QueryInterception.res +0 -1
  14. package/src/adapter/Runtime/AggregateRuntime_Builder_Common.res +12 -5
  15. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +11 -7
  16. package/src/adapter/Runtime/PluginRuntime_Builder_Micro.res +1 -7
  17. package/src/adapter/Runtime/PluginRuntime_Builder_Single.res +1 -7
  18. package/src/adapter/RuntimeExtension/RuntimeExtension.res +0 -1
  19. package/src/adapter/Secrets/Secrets_Node_Backend.res +60 -0
  20. package/src/adapter/Secrets/Secrets_Node_Backend.res.mjs +69 -0
  21. package/src/admin/Platform_Admin.res +17 -6
  22. package/src/admin/Platform_AdminApi.res +14 -9
  23. package/src/admin/Platform_Admin_Callback.res +1 -1
  24. package/src/admin/Platform_BakedManifest.res +34 -34
  25. package/src/admin/Platform_ComponentDefinitionsApi.res +9 -21
  26. package/src/admin/Platform_PluginStructuresApi.res +15 -12
  27. package/src/admin/UiFragmentRegistry/StateChange/UiFragmentRegistry.res +1 -1
  28. package/src/components/Aggregate/Aggregate.res +13 -12
  29. package/src/components/Aggregate/Aggregate_Builder.res +22 -21
  30. package/src/components/Aggregate/Aggregate_Callback.res +91 -86
  31. package/src/components/Api/ApiNoApiHelpers.res +1 -3
  32. package/src/components/Api/Api_Ids.res +0 -1
  33. package/src/components/Api/Api_Naming.res +41 -18
  34. package/src/components/Api/GraphQL_FragmentGenerator.res +38 -51
  35. package/src/components/Api/GraphQL_SchemaInspector.res +5 -3
  36. package/src/components/Api/GraphQL_Stitcher.res +9 -10
  37. package/src/components/Api/MCP_SchemaGenerator.res +8 -12
  38. package/src/components/Api/OwnerScopeDiagnostics.res +1 -2
  39. package/src/components/Api/QueryDbListQuery.res +13 -6
  40. package/src/components/Api/SchemaType.res +28 -23
  41. package/src/components/Api/SuryToJsonSchema.res +9 -10
  42. package/src/components/AutomationSlice/AutomationSlice.res +4 -4
  43. package/src/components/AutomationSlice/AutomationSlice_Builder.res +15 -14
  44. package/src/components/AutomationSlice/AutomationSlice_Callback.res +3 -3
  45. package/src/components/Builder_Helpers.res +12 -23
  46. package/src/components/CommandGenerator/CommandGenerator.res +6 -1
  47. package/src/components/CommandGenerator/CommandGenerator_Builder.res +29 -21
  48. package/src/components/CommandGenerator/CommandGenerator_Callback.res +17 -21
  49. package/src/components/CommandTopic/CommandTopic.res +3 -3
  50. package/src/components/CommandTopic/CommandTopic_Builder.res +10 -10
  51. package/src/components/CommandTopic/CommandTopic_Callback.res +1 -4
  52. package/src/components/CommandTopic/CommandTopic_Helpers.res +1 -2
  53. package/src/components/Counter/Counter_Builder.res +1 -6
  54. package/src/components/Counter/Counter_Callback.res +3 -3
  55. package/src/components/Dcb/Dcb_Builder.res +970 -956
  56. package/src/components/DcbEventLog/DcbEventLog.res +4 -4
  57. package/src/components/DcbEventLog/DcbEventLog_Builder.res +0 -1
  58. package/src/components/DcbEventLog/DcbEventLog_Operations.res +17 -18
  59. package/src/components/EventCollector/EventCollector_Builder.res +2 -1
  60. package/src/components/EventLog/EventLog.res +5 -1
  61. package/src/components/EventLog/EventLog_Operations.res +12 -8
  62. package/src/components/EventMapper/EventMapper.res +8 -10
  63. package/src/components/EventMapper/EventMapper_Builder.res +4 -1
  64. package/src/components/EventMapper/EventMapper_Callback.res +46 -31
  65. package/src/components/EventTopic/EventTopic.res +3 -3
  66. package/src/components/EventTopic/EventTopic_Builder.res +1 -1
  67. package/src/components/Extension/Extension.res +4 -1
  68. package/src/components/Extension/Extension_Operations.res +11 -3
  69. package/src/components/ExtensionPoint/ExtensionPoint.res +3 -3
  70. package/src/components/ExtensionPoint/ExtensionPoint_Builder.res +9 -10
  71. package/src/components/ExtensionPoint/ExtensionPoint_Callback.res +2 -6
  72. package/src/components/ExtensionPoint/ExtensionPoint_Operations.res +13 -5
  73. package/src/components/Heartbeat/Heartbeat_Callback.res +3 -3
  74. package/src/components/InboundTranslationSlice/InboundTranslationSlice.res +4 -4
  75. package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res +7 -10
  76. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice.res +4 -4
  77. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res +15 -14
  78. package/src/components/QueryDb/QueryDb_Adapter.res +1 -3
  79. package/src/components/QueryDb/QueryDb_Operations.res +10 -10
  80. package/src/components/ReadModel/ReadModel.res +3 -3
  81. package/src/components/ReadModel/ReadModel_Builder.res +1 -2
  82. package/src/components/ReadModel/ReadModel_Callback.res +2 -1
  83. package/src/components/SideEffectHandler/SideEffectHandler_Callback.res +5 -2
  84. package/src/components/StateChangeSlice/StateChangeSlice.res +3 -3
  85. package/src/components/StateChangeSlice/StateChangeSlice_Builder.res +23 -4
  86. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +22 -6
  87. package/src/components/StateTopic.res +1 -3
  88. package/src/components/StateViewSlice/StateViewSlice.res +4 -4
  89. package/src/components/StateViewSlice/StateViewSlice_Builder.res +79 -68
  90. package/src/components/StateViewSlice/StateViewSlice_Callback.res +3 -1
  91. package/src/components/Task/Task.res +3 -3
  92. package/src/components/Task/Task_Adapter.res +4 -1
  93. package/src/components/Task/Task_Builder.res +24 -21
  94. package/src/plugin/api/PluginBaseFragment.res +9 -3
  95. package/src/plugin/component/Capability_Inference.res +8 -10
  96. package/src/plugin/component/Plugin.res +6 -6
  97. package/src/plugin/component/Plugin_Builder.res +54 -50
  98. package/src/plugin/component/Plugin_Callback.res +10 -6
  99. package/src/plugin/component/Plugin_Helpers.res +264 -230
  100. package/src/plugin/component/Plugin_Structure.res +370 -324
  101. package/src/plugin/component/Plugin_SubscriptionSchema.res +8 -6
  102. package/src/plugin/component/SchemaWalker.res +11 -12
  103. package/src/plugin/connect/PluginConnectExtension_Mapping.res +45 -50
  104. package/src/plugin/lifecycle/PluginBehavior.res +3 -4
  105. package/src/plugin/lifecycle/PluginSpec.res +1 -2
  106. package/src/plugin/lifecycle/PluginsProjection.res +16 -5
  107. package/src/plugin/lifecycle/PluginsReadModelSpec.res +0 -2
  108. package/src/util/CommandPublisher.res +9 -2
  109. package/src/util/CsvStream.res +3 -3
  110. package/src/util/Interstack.res +7 -15
  111. package/src/util/Interstack.resi +3 -3
  112. package/src/util/LogFormat.res +5 -9
  113. package/src/util/Logger.res +1 -2
  114. package/src/util/Lru.res +2 -1
  115. package/src/util/Lru.resi +2 -3
  116. package/src/util/ScheduleOps.res +24 -27
  117. package/src/util/StorePrefixCollision.res +6 -9
  118. package/src/util/Util_Promise.res +4 -1
  119. package/src/util/Util_Promise.res.mjs +1 -1
  120. package/tests/IdentityTest.res +166 -113
  121. package/tests/ResourceAttributionTest.res +28 -25
  122. package/tests/Secrets_Node_BackendTest.res +141 -0
  123. package/tests/Secrets_Node_BackendTest.res.mjs +152 -0
  124. package/tests/StorePrefixCollisionTest.res +2 -1
  125. package/tests/TaskBucketNamingTest.res +3 -2
  126. package/tests/adapter/EventLogProvisioningTest.res +51 -48
  127. package/tests/adapter/MonitoringTest.res +81 -38
  128. package/tests/adapter/QueryInterceptionTest.res +2 -2
  129. package/tests/adapter/RefusalVocabularyTest.res +31 -23
  130. package/tests/adapter/RuntimeExtensionHookReachTest.res +9 -8
  131. package/tests/adapter/RuntimeExtensionTest.res +5 -3
  132. package/tests/admin/AdminApiSchemaDriftTest.res +22 -16
  133. package/tests/admin/Platform_Admin_StructureTest.res +22 -14
  134. package/tests/admin/Platform_Admin_StructureTest.res.mjs +4 -3
  135. package/tests/admin/Platform_BakedManifestTest.res +31 -35
  136. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +18 -15
  137. package/tests/admin/Platform_PluginStructuresApiTest.res +5 -3
  138. package/tests/admin/Platform_UIFragmentsApiTest.res +6 -3
  139. package/tests/aggregate/AggregateCacheTest.res +10 -12
  140. package/tests/aggregate/AggregateCallbackTest.res +75 -46
  141. package/tests/aggregate/AggregateCausationTest.res +19 -20
  142. package/tests/aggregate/AggregateConflictTest.res +41 -19
  143. package/tests/aggregate/AggregateFixtures.res +15 -5
  144. package/tests/aggregate/AggregateRejectionTest.res +97 -80
  145. package/tests/aggregate/AggregateSnapshotTest.res +44 -16
  146. package/tests/api/Api_IdsTest.res +10 -4
  147. package/tests/api/BrandedMarkerCompositionTest.res +48 -36
  148. package/tests/api/GeolocationSemanticTest.res +3 -3
  149. package/tests/api/GraphQL_FragmentGeneratorTest.res +66 -54
  150. package/tests/api/OwnerScopeDiagnosticsTest.res +31 -27
  151. package/tests/api/StorageRefFieldsTest.res +30 -18
  152. package/tests/api/SuryToJsonSchemaTest.res +1315 -1014
  153. package/tests/api/TaggedUnionSchemaTest.res +17 -24
  154. package/tests/commandgenerator/CommandGeneratorCallbackTest.res +235 -179
  155. package/tests/commandgenerator/CommandGeneratorFixtures.res +8 -13
  156. package/tests/commandgenerator/OwnerStampingTest.res +91 -76
  157. package/tests/commandtopic/CommandOutcomeHookTest.res +198 -155
  158. package/tests/commandtopic/CommandTopicCallbackFixtures.res +5 -4
  159. package/tests/commandtopic/CommandTopicCallbackTest.res +61 -43
  160. package/tests/commandtopic/CommandTopicHelpersRejectionTest.res +70 -55
  161. package/tests/counter/CounterCallbackTest.res +40 -34
  162. package/tests/counter/CounterFixtures.res +4 -4
  163. package/tests/dcb/DcbDecodeTest.res +115 -84
  164. package/tests/dcb/DcbEventLogOperationsTest.res +194 -143
  165. package/tests/dcb/DcbEventLogSchemaTest.res +13 -13
  166. package/tests/dcb/DcbFixtures.res +4 -5
  167. package/tests/dcb/DcbScopeInferenceTest.res +173 -114
  168. package/tests/dcb/DcbStateChangeSliceTest.res +391 -262
  169. package/tests/dcb/DcbTagTest.res +22 -11
  170. package/tests/dcb/DcbValidationTest.res +288 -218
  171. package/tests/eventlog/EventLogFixtures.res +4 -4
  172. package/tests/eventlog/EventLogOperationsTest.res +102 -67
  173. package/tests/eventlog/EventLogRetryTest.res +104 -65
  174. package/tests/eventmapper/EventMapperCallbackTest.res +118 -83
  175. package/tests/eventmapper/EventMapperFixtures.res +15 -23
  176. package/tests/eventmapper/EventMapperTest.res +54 -38
  177. package/tests/eventtopic/EventTopicFixtures.res +3 -4
  178. package/tests/eventtopic/EventTopicOperationsTest.res +41 -29
  179. package/tests/extensionpoint/ExtensionPointCallbackTest.res +58 -45
  180. package/tests/extensionpoint/ExtensionPointFixtures.res +16 -20
  181. package/tests/extensionpoint/ExtensionPointOperationsTest.res +70 -49
  182. package/tests/extensionpoint/PluginDisconnectScheduleTest.res +2 -1
  183. package/tests/logger/LogFormatTest.res +104 -82
  184. package/tests/logger/LoggerLazyTest.res +21 -12
  185. package/tests/message/MessageTest.res +96 -93
  186. package/tests/message/MetaEnvelopeTest.res +26 -30
  187. package/tests/plugin/HeartbeatDisconnectGraceTest.res +31 -25
  188. package/tests/plugin/ManifestVisibilityTest.res +20 -12
  189. package/tests/plugin/PluginDefinitionScalars.res +1 -2
  190. package/tests/plugin/PluginLifecycleCorpusTest.res +15 -12
  191. package/tests/plugin/PluginSpecExperiment.res +0 -1
  192. package/tests/plugin/PluginStructureTest.res +1529 -1045
  193. package/tests/plugin/Plugin_EventQuerySchemaTest.res +21 -10
  194. package/tests/plugin/SchemaWalkerTest.res +116 -70
  195. package/tests/plugin/StateChange/PsReserveStock.res +4 -4
  196. package/tests/plugin/StateView/PsAnnotatedView.res +4 -4
  197. package/tests/plugin/StateView/PsCustomersView.res +4 -4
  198. package/tests/plugin/StateView/PsShipmentsView.res +3 -2
  199. package/tests/projection/LifecycleTrailTest.res +1 -4
  200. package/tests/projection/ProjectionOptimizeTest.res +13 -9
  201. package/tests/querydb/QueryDbFixtures.res +0 -1
  202. package/tests/querydb/QueryDbOperationsTest.res +99 -64
  203. package/tests/querydb/TaggedUnionStampTest.res +10 -8
  204. package/tests/sideeffecthandler/SideEffectHandlerCallbackTest.res +50 -35
  205. package/tests/sideeffecthandler/SideEffectHandlerFixtures.res +4 -10
  206. package/tests/util/CommandPublisherTest.res +0 -1
  207. package/tests/util/CsvStreamTest.res +3 -3
  208. package/tests/util/LruTest.res +84 -56
@@ -1,19 +1,27 @@
1
1
  open JestGlobals
2
2
 
3
-
4
3
  describe("Identity", () => {
5
4
  describe("anonymous", () => {
6
- testSync("has userId 'anonymous'", () => {
7
- expect(Reventless.Identity.anonymous.userId)->toBe("anonymous")
8
- })
9
-
10
- testSync("has empty groups", () => {
11
- expect(Reventless.Identity.anonymous.groups)->toEqual([])
12
- })
13
-
14
- testSync("has InMemory provider", () => {
15
- expect(Reventless.Identity.anonymous.provider)->toEqual(Reventless.Identity.InMemory)
16
- })
5
+ testSync(
6
+ "has userId 'anonymous'",
7
+ () => {
8
+ expect(Reventless.Identity.anonymous.userId)->toBe("anonymous")
9
+ },
10
+ )
11
+
12
+ testSync(
13
+ "has empty groups",
14
+ () => {
15
+ expect(Reventless.Identity.anonymous.groups)->toEqual([])
16
+ },
17
+ )
18
+
19
+ testSync(
20
+ "has InMemory provider",
21
+ () => {
22
+ expect(Reventless.Identity.anonymous.provider)->toEqual(Reventless.Identity.InMemory)
23
+ },
24
+ )
17
25
  })
18
26
 
19
27
  describe("hasGroup", () => {
@@ -24,13 +32,19 @@ describe("Identity", () => {
24
32
  provider: Cognito,
25
33
  }
26
34
 
27
- testSync("returns true when group is present", () => {
28
- expect(Reventless.Identity.hasGroup(identity, "admin"))->toBe(true)
29
- })
30
-
31
- testSync("returns false when group is absent", () => {
32
- expect(Reventless.Identity.hasGroup(identity, "viewers"))->toBe(false)
33
- })
35
+ testSync(
36
+ "returns true when group is present",
37
+ () => {
38
+ expect(Reventless.Identity.hasGroup(identity, "admin"))->toBe(true)
39
+ },
40
+ )
41
+
42
+ testSync(
43
+ "returns false when group is absent",
44
+ () => {
45
+ expect(Reventless.Identity.hasGroup(identity, "viewers"))->toBe(false)
46
+ },
47
+ )
34
48
  })
35
49
 
36
50
  describe("getClaim", () => {
@@ -42,113 +56,152 @@ describe("Identity", () => {
42
56
  provider: InMemory,
43
57
  }
44
58
 
45
- testSync("returns Some for existing claim", () => {
46
- expect(Reventless.Identity.getClaim(identity, "tenant"))->toEqual(Some("acme"))
47
- })
48
-
49
- testSync("returns None for missing claim", () => {
50
- expect(Reventless.Identity.getClaim(identity, "missing"))->toEqual(None)
51
- })
52
-
53
- testSync("returns None when claims are absent", () => {
54
- let noClaims: Reventless.Identity.t = {
55
- userId: "user-2",
56
- username: "bob",
57
- groups: [],
58
- provider: InMemory,
59
- }
60
- expect(Reventless.Identity.getClaim(noClaims, "tenant"))->toEqual(None)
61
- })
59
+ testSync(
60
+ "returns Some for existing claim",
61
+ () => {
62
+ expect(Reventless.Identity.getClaim(identity, "tenant"))->toEqual(Some("acme"))
63
+ },
64
+ )
65
+
66
+ testSync(
67
+ "returns None for missing claim",
68
+ () => {
69
+ expect(Reventless.Identity.getClaim(identity, "missing"))->toEqual(None)
70
+ },
71
+ )
72
+
73
+ testSync(
74
+ "returns None when claims are absent",
75
+ () => {
76
+ let noClaims: Reventless.Identity.t = {
77
+ userId: "user-2",
78
+ username: "bob",
79
+ groups: [],
80
+ provider: InMemory,
81
+ }
82
+ expect(Reventless.Identity.getClaim(noClaims, "tenant"))->toEqual(None)
83
+ },
84
+ )
62
85
  })
63
86
 
64
87
  describe("schema roundtrip", () => {
65
- testSync("encode then decode preserves identity", () => {
66
- let identity: Reventless.Identity.t = {
67
- userId: "user-1",
68
- username: "alice",
69
- groups: ["admin"],
70
- claims: Dict.fromArray([("tenant", "acme")]),
71
- provider: Cognito,
72
- }
73
- let json = identity->Reventless.Util_Sury.toJson(Reventless.Identity.schema)
74
- let decoded = json->Reventless.Util_Sury.fromJson(Reventless.Identity.schema)
75
- expect(decoded)->toEqual(identity)
76
- })
77
-
78
- testSync("roundtrip with Custom provider", () => {
79
- let identity: Reventless.Identity.t = {
80
- userId: "ext-1",
81
- username: "external",
82
- groups: [],
83
- provider: Custom("oauth2"),
84
- }
85
- let json = identity->Reventless.Util_Sury.toJson(Reventless.Identity.schema)
86
- let decoded = json->Reventless.Util_Sury.fromJson(Reventless.Identity.schema)
87
- expect(decoded)->toEqual(identity)
88
- })
89
-
90
- testSync("roundtrip without optional claims", () => {
91
- let identity: Reventless.Identity.t = {
92
- userId: "user-3",
93
- username: "charlie",
94
- groups: [],
95
- provider: InMemory,
96
- }
97
- let json = identity->Reventless.Util_Sury.toJson(Reventless.Identity.schema)
98
- let decoded = json->Reventless.Util_Sury.fromJson(Reventless.Identity.schema)
99
- expect(decoded.userId)->toBe("user-3")->ignore
100
- expect(decoded.provider)->toEqual(Reventless.Identity.InMemory)
101
- })
88
+ testSync(
89
+ "encode then decode preserves identity",
90
+ () => {
91
+ let identity: Reventless.Identity.t = {
92
+ userId: "user-1",
93
+ username: "alice",
94
+ groups: ["admin"],
95
+ claims: Dict.fromArray([("tenant", "acme")]),
96
+ provider: Cognito,
97
+ }
98
+ let json = identity->Reventless.Util_Sury.toJson(Reventless.Identity.schema)
99
+ let decoded = json->Reventless.Util_Sury.fromJson(Reventless.Identity.schema)
100
+ expect(decoded)->toEqual(identity)
101
+ },
102
+ )
103
+
104
+ testSync(
105
+ "roundtrip with Custom provider",
106
+ () => {
107
+ let identity: Reventless.Identity.t = {
108
+ userId: "ext-1",
109
+ username: "external",
110
+ groups: [],
111
+ provider: Custom("oauth2"),
112
+ }
113
+ let json = identity->Reventless.Util_Sury.toJson(Reventless.Identity.schema)
114
+ let decoded = json->Reventless.Util_Sury.fromJson(Reventless.Identity.schema)
115
+ expect(decoded)->toEqual(identity)
116
+ },
117
+ )
118
+
119
+ testSync(
120
+ "roundtrip without optional claims",
121
+ () => {
122
+ let identity: Reventless.Identity.t = {
123
+ userId: "user-3",
124
+ username: "charlie",
125
+ groups: [],
126
+ provider: InMemory,
127
+ }
128
+ let json = identity->Reventless.Util_Sury.toJson(Reventless.Identity.schema)
129
+ let decoded = json->Reventless.Util_Sury.fromJson(Reventless.Identity.schema)
130
+ expect(decoded.userId)->toBe("user-3")->ignore
131
+ expect(decoded.provider)->toEqual(Reventless.Identity.InMemory)
132
+ },
133
+ )
102
134
  })
103
135
  })
104
136
 
105
137
  describe("RequestContext", () => {
106
138
  describe("test() constructor", () => {
107
- testSync("defaults to anonymous identity", () => {
108
- let ctx = RequestContext.test()
109
- expect(ctx.identity.userId)->toBe("anonymous")
110
- })
111
-
112
- testSync("accepts custom identity", () => {
113
- let identity: Reventless.Identity.t = {
114
- userId: "user-1",
115
- username: "alice",
116
- groups: ["admin"],
117
- provider: Cognito,
118
- }
119
- let ctx = RequestContext.test(~identity)
120
- expect(ctx.identity.userId)->toBe("user-1")
121
- })
122
-
123
- testSync("defaults to empty claims", () => {
124
- let ctx = RequestContext.test()
125
- expect(ctx.claims->Dict.keysToArray)->toEqual([])
126
- })
139
+ testSync(
140
+ "defaults to anonymous identity",
141
+ () => {
142
+ let ctx = RequestContext.test()
143
+ expect(ctx.identity.userId)->toBe("anonymous")
144
+ },
145
+ )
146
+
147
+ testSync(
148
+ "accepts custom identity",
149
+ () => {
150
+ let identity: Reventless.Identity.t = {
151
+ userId: "user-1",
152
+ username: "alice",
153
+ groups: ["admin"],
154
+ provider: Cognito,
155
+ }
156
+ let ctx = RequestContext.test(~identity)
157
+ expect(ctx.identity.userId)->toBe("user-1")
158
+ },
159
+ )
160
+
161
+ testSync(
162
+ "defaults to empty claims",
163
+ () => {
164
+ let ctx = RequestContext.test()
165
+ expect(ctx.claims->Dict.keysToArray)->toEqual([])
166
+ },
167
+ )
127
168
  })
128
169
 
129
170
  describe("getClaim", () => {
130
- testSync("returns claim value when present", () => {
131
- let ctx = RequestContext.test(~claims=Dict.fromArray([("tenant", "acme")]))
132
- expect(RequestContext.getClaim(ctx, "tenant"))->toEqual(Some("acme"))
133
- })
134
-
135
- testSync("returns None when absent", () => {
136
- let ctx = RequestContext.test()
137
- expect(RequestContext.getClaim(ctx, "missing"))->toEqual(None)
138
- })
171
+ testSync(
172
+ "returns claim value when present",
173
+ () => {
174
+ let ctx = RequestContext.test(~claims=Dict.fromArray([("tenant", "acme")]))
175
+ expect(RequestContext.getClaim(ctx, "tenant"))->toEqual(Some("acme"))
176
+ },
177
+ )
178
+
179
+ testSync(
180
+ "returns None when absent",
181
+ () => {
182
+ let ctx = RequestContext.test()
183
+ expect(RequestContext.getClaim(ctx, "missing"))->toEqual(None)
184
+ },
185
+ )
139
186
  })
140
187
 
141
188
  describe("withClaim", () => {
142
- testSync("adds a claim to the context", () => {
143
- let ctx = RequestContext.test()
144
- let updated = RequestContext.withClaim(ctx, "tenant", "acme")
145
- expect(RequestContext.getClaim(updated, "tenant"))->toEqual(Some("acme"))
146
- })
147
-
148
- testSync("does not mutate the original context", () => {
149
- let ctx = RequestContext.test()
150
- let _updated = RequestContext.withClaim(ctx, "tenant", "acme")
151
- expect(RequestContext.getClaim(ctx, "tenant"))->toEqual(None)
152
- })
189
+ testSync(
190
+ "adds a claim to the context",
191
+ () => {
192
+ let ctx = RequestContext.test()
193
+ let updated = RequestContext.withClaim(ctx, "tenant", "acme")
194
+ expect(RequestContext.getClaim(updated, "tenant"))->toEqual(Some("acme"))
195
+ },
196
+ )
197
+
198
+ testSync(
199
+ "does not mutate the original context",
200
+ () => {
201
+ let ctx = RequestContext.test()
202
+ let _updated = RequestContext.withClaim(ctx, "tenant", "acme")
203
+ expect(RequestContext.getClaim(ctx, "tenant"))->toEqual(None)
204
+ },
205
+ )
153
206
  })
154
207
  })
@@ -104,15 +104,15 @@ describe("ResourceAttribution — carrying the context into deferred work", () =
104
104
  testSync("two plugins deferred from one apply are each attributed to themselves", () => {
105
105
  let outer = Attribution.enter(~platform="online-shop", ~plugin="Catalog")
106
106
  let catalogSaw = ref(None)
107
- let catalogFinish = Attribution.deferred(() =>
108
- catalogSaw := Attribution.current.contents.plugin
107
+ let catalogFinish = Attribution.deferred(
108
+ () => catalogSaw := Attribution.current.contents.plugin,
109
109
  )
110
110
  Attribution.restore(outer)
111
111
 
112
112
  let previous = Attribution.enter(~platform="online-shop", ~plugin="Ordering")
113
113
  let orderingSaw = ref(None)
114
- let orderingFinish = Attribution.deferred(() =>
115
- orderingSaw := Attribution.current.contents.plugin
114
+ let orderingFinish = Attribution.deferred(
115
+ () => orderingSaw := Attribution.current.contents.plugin,
116
116
  )
117
117
  Attribution.restore(previous)
118
118
 
@@ -126,8 +126,8 @@ describe("ResourceAttribution — carrying the context into deferred work", () =
126
126
 
127
127
  testSync("a throwing callback still puts the context back", () => {
128
128
  let previous = Attribution.enter(~platform="online-shop", ~plugin="Catalog")
129
- let finish: unit => unit = Attribution.deferred(() =>
130
- JsError.throwWithMessage("finish blew up")
129
+ let finish: unit => unit = Attribution.deferred(
130
+ () => JsError.throwWithMessage("finish blew up"),
131
131
  )
132
132
  Attribution.restore(previous)
133
133
 
@@ -164,22 +164,25 @@ describe("ResourceAttribution — carrying the context into deferred work", () =
164
164
  // exercises the production choke point: a task side-effect handler registered
165
165
  // during construct, then run the way `finishTasks` runs it — from an apply,
166
166
  // with nothing attributed.
167
- describe("Builder_Helpers.registerTaskSideEffectHandler — attribution survives the deferral", () => {
168
- testSync("the registered finish sees the plugin that registered it", () => {
169
- let before = Builder_Helpers.taskSideEffectFinishFns->Array.length
170
- let observed = ref(None)
171
-
172
- let previous = Attribution.enter(~platform="online-shop", ~plugin="Catalog")
173
- Builder_Helpers.registerTaskSideEffectHandler(
174
- ~gate=Pulumi.Output.make(),
175
- ~finish=() => observed := Attribution.current.contents.plugin,
176
- )
177
- Attribution.restore(previous)
178
-
179
- switch Builder_Helpers.taskSideEffectFinishFns->Array.get(before) {
180
- | Some(finish) => finish()
181
- | None => JsError.throwWithMessage("handler was not registered")
182
- }
183
- expect(observed.contents)->toEqual(Some("Catalog"))
184
- })
185
- })
167
+ describe(
168
+ "Builder_Helpers.registerTaskSideEffectHandler — attribution survives the deferral",
169
+ () => {
170
+ testSync("the registered finish sees the plugin that registered it", () => {
171
+ let before = Builder_Helpers.taskSideEffectFinishFns->Array.length
172
+ let observed = ref(None)
173
+
174
+ let previous = Attribution.enter(~platform="online-shop", ~plugin="Catalog")
175
+ Builder_Helpers.registerTaskSideEffectHandler(
176
+ ~gate=Pulumi.Output.make(),
177
+ ~finish=() => observed := Attribution.current.contents.plugin,
178
+ )
179
+ Attribution.restore(previous)
180
+
181
+ switch Builder_Helpers.taskSideEffectFinishFns->Array.get(before) {
182
+ | Some(finish) => finish()
183
+ | None => JsError.throwWithMessage("handler was not registered")
184
+ }
185
+ expect(observed.contents)->toEqual(Some("Catalog"))
186
+ })
187
+ },
188
+ )
@@ -0,0 +1,141 @@
1
+ open JestGlobals
2
+
3
+ // The secret source. What matters here is not that it produces characters, but
4
+ // that it produces them uniformly — a token drawn from a skewed distribution is
5
+ // guessable in a way that no other test would notice.
6
+
7
+ module Secrets = Reventless.Secrets
8
+ module Backend = Secrets_Node_Backend
9
+
10
+ let token = (~length, ~alphabet) =>
11
+ Backend.provider.randomToken(~length, ~alphabet)->Result.getOr("")
12
+
13
+ describe("randomToken", () => {
14
+ testSync("draws the requested length", () =>
15
+ expect(
16
+ [1, 6, 32, 64]->Array.map(length => token(~length, ~alphabet=Digits)->String.length),
17
+ )->toEqual([1, 6, 32, 64])
18
+ )
19
+
20
+ testSync("draws only from the alphabet it was given", () => {
21
+ let allowed = Secrets.characters(UrlSafe)
22
+ expect(
23
+ token(~length=512, ~alphabet=UrlSafe)
24
+ ->String.split("")
25
+ ->Array.every(c => allowed->String.includes(c)),
26
+ )->toBe(true)
27
+ })
28
+
29
+ testSync("digits are digits", () =>
30
+ expect(
31
+ token(~length=512, ~alphabet=Digits)
32
+ ->String.split("")
33
+ ->Array.every(c => "0123456789"->String.includes(c)),
34
+ )->toBe(true)
35
+ )
36
+
37
+ // Not proof of randomness — it is proof the source is not a constant, which is
38
+ // the failure a fixed or broken binding would produce.
39
+ testSync("two draws differ", () =>
40
+ expect(token(~length=32, ~alphabet=UrlSafe) == token(~length=32, ~alphabet=UrlSafe))->toBe(
41
+ false,
42
+ )
43
+ )
44
+
45
+ // A zero-length token would compare equal to nothing and settle every
46
+ // challenge presented against it, so it is refused rather than clamped.
47
+ testSync("a non-positive length is refused, not clamped", () =>
48
+ expect(
49
+ [0, -1]->Array.map(
50
+ length => Backend.provider.randomToken(~length, ~alphabet=Digits)->Result.isError,
51
+ ),
52
+ )->toEqual([true, true])
53
+ )
54
+ })
55
+
56
+ // 🚨 Checked over every byte value rather than by sampling the output, and the
57
+ // first attempt here got that wrong. `byte % 10` skews the low six digits by
58
+ // about four percent each — roughly 0.609 of draws landing in the low half
59
+ // against 0.600 for a fair source. At any sample size a test can afford that
60
+ // gap is a couple of standard errors, so a sampled assertion either flakes or,
61
+ // as the first version did, passes happily with the defence deleted.
62
+ describe("accepts — the rejection rule", () => {
63
+ let rejected = size =>
64
+ Array.fromInitializer(~length=256, i => i)->Array.filter(byte => !Backend.accepts(~byte, ~size))
65
+
66
+ testSync("rejects exactly the bytes that would skew a 10-character alphabet", () =>
67
+ expect(rejected(10))->toEqual([250, 251, 252, 253, 254, 255])
68
+ )
69
+
70
+ // 64 divides 256 exactly, so nothing needs discarding and a url-safe token
71
+ // costs no extra draws.
72
+ testSync("rejects nothing when the alphabet divides evenly", () =>
73
+ expect(rejected(64))->toEqual([])
74
+ )
75
+
76
+ // What survives always folds onto a whole number of cycles, which is the
77
+ // property that makes every character equally likely.
78
+ testSync("what it accepts is a whole multiple of the alphabet", () =>
79
+ expect(
80
+ [2, 3, 7, 10, 16, 26, 62, 64]->Array.map(
81
+ size => mod(256 - rejected(size)->Array.length, size),
82
+ ),
83
+ )->toEqual([0, 0, 0, 0, 0, 0, 0, 0])
84
+ )
85
+
86
+ testSync("never discards more than one cycle's worth", () =>
87
+ expect([3, 7, 10, 26, 62]->Array.map(size => rejected(size)->Array.length < size))->toEqual([
88
+ true,
89
+ true,
90
+ true,
91
+ true,
92
+ true,
93
+ ])
94
+ )
95
+ })
96
+
97
+ describe("hash", () => {
98
+ testSync("is deterministic, which is what lets a proof be compared", () =>
99
+ expect(Backend.provider.hash("a-secret") == Backend.provider.hash("a-secret"))->toBe(true)
100
+ )
101
+
102
+ testSync("separates different inputs", () =>
103
+ expect(Backend.provider.hash("a-secret") == Backend.provider.hash("a-secret "))->toBe(false)
104
+ )
105
+
106
+ // One-way: the digest must not carry the secret it was made from.
107
+ testSync("does not contain its input", () =>
108
+ expect(
109
+ Backend.provider.hash("a-secret")
110
+ ->Result.getOr("")
111
+ ->String.includes("a-secret"),
112
+ )->toBe(false)
113
+ )
114
+ })
115
+
116
+ describe("Capabilities.none.secrets", () => {
117
+ // Refusing rather than degrading: there is no weaker token that would be safe
118
+ // to hand back, so an unwired platform must fail rather than produce one.
119
+ testSync("refuses both operations", () =>
120
+ expect((
121
+ Reventless.Capabilities.none.secrets.randomToken(
122
+ ~length=32,
123
+ ~alphabet=UrlSafe,
124
+ )->Result.isError,
125
+ Reventless.Capabilities.none.secrets.hash("x")->Result.isError,
126
+ ))->toEqual((true, true))
127
+ )
128
+ })
129
+
130
+ describe("Secrets.fixed", () => {
131
+ // What a consumer's suite uses to know the secret before presenting it.
132
+ let provider = Secrets.fixed(~token="known", ~hash=input => `hashed:${input}`)
133
+
134
+ testSync("hands back the token it was built with", () =>
135
+ expect(provider.randomToken(~length=32, ~alphabet=UrlSafe))->toEqual(Ok("known"))
136
+ )
137
+
138
+ testSync("hashes through the function it was given", () =>
139
+ expect(provider.hash("known"))->toEqual(Ok("hashed:known"))
140
+ )
141
+ })