@reventlessdev/reventless-local 3.0.0-alpha.113
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 +1197 -0
- package/LICENSE +202 -0
- package/__mocks__/emptyModule.js +2 -0
- package/__mocks__/nodeSqlite.mjs +6 -0
- package/package.json +67 -0
- package/rescript.json +39 -0
- package/src/Platform.res +2162 -0
- package/src/Platform.res.mjs +3389 -0
- package/src/adapter/Api/LocalGraphQL_Adapter.res +29 -0
- package/src/adapter/Api/LocalGraphQL_Adapter.res.mjs +26 -0
- package/src/adapter/Api/LocalGraphQL_SubscriptionResolvers.res +146 -0
- package/src/adapter/Api/LocalGraphQL_SubscriptionResolvers.res.mjs +88 -0
- package/src/adapter/Auth/Auth_GraphqlContext.res +83 -0
- package/src/adapter/Auth/Auth_GraphqlContext.res.mjs +76 -0
- package/src/adapter/Auth/LocalAuth.res +239 -0
- package/src/adapter/Auth/LocalAuth.res.mjs +247 -0
- package/src/adapter/Auth/UserStore.res +134 -0
- package/src/adapter/Auth/UserStore.res.mjs +146 -0
- package/src/adapter/Backend.res +46 -0
- package/src/adapter/Backend.res.mjs +66 -0
- package/src/adapter/BackendState.res +34 -0
- package/src/adapter/BackendState.res.mjs +42 -0
- package/src/adapter/Cloner/LocalClonerRunner.res +13 -0
- package/src/adapter/Cloner/LocalClonerRunner.res.mjs +12 -0
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +329 -0
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res.mjs +301 -0
- package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +76 -0
- package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res.mjs +57 -0
- package/src/adapter/CommandGenerator/LocalCommandGeneratorResolvers.res +19 -0
- package/src/adapter/CommandGenerator/LocalCommandGeneratorResolvers.res.mjs +19 -0
- package/src/adapter/CommandTopic/LocalCommandTopicChannel.res +104 -0
- package/src/adapter/CommandTopic/LocalCommandTopicChannel.res.mjs +82 -0
- package/src/adapter/CommandTopic/LocalCommandTopicRemoteChannel.res +32 -0
- package/src/adapter/CommandTopic/LocalCommandTopicRemoteChannel.res.mjs +38 -0
- package/src/adapter/Counter/LocalCounterHandler.res +42 -0
- package/src/adapter/Counter/LocalCounterHandler.res.mjs +45 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +131 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +140 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +334 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +299 -0
- package/src/adapter/DomainGraphQL_Server.res +565 -0
- package/src/adapter/DomainGraphQL_Server.res.mjs +576 -0
- package/src/adapter/DomainMCP_Server.res +369 -0
- package/src/adapter/DomainMCP_Server.res.mjs +358 -0
- package/src/adapter/EventCollector/LocalEventCollectorChannel.res +87 -0
- package/src/adapter/EventCollector/LocalEventCollectorChannel.res.mjs +45 -0
- package/src/adapter/EventLog/EventLogStorage_InMemory.res +105 -0
- package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +117 -0
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res +145 -0
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +155 -0
- package/src/adapter/EventTopic/LocalEventTopicPublisher.res +36 -0
- package/src/adapter/EventTopic/LocalEventTopicPublisher.res.mjs +31 -0
- package/src/adapter/GraphQL_Server.res +6 -0
- package/src/adapter/GraphQL_Server.res.mjs +154 -0
- package/src/adapter/GraphQL_ServerInstance.res +310 -0
- package/src/adapter/GraphQL_ServerInstance.res.mjs +257 -0
- package/src/adapter/Heartbeat/LocalHeartbeatRunner.res +57 -0
- package/src/adapter/Heartbeat/LocalHeartbeatRunner.res.mjs +34 -0
- package/src/adapter/LocalBus.res +498 -0
- package/src/adapter/LocalBus.res.mjs +1011 -0
- package/src/adapter/LocalPluginSpec.res +16 -0
- package/src/adapter/LocalPluginSpec.res.mjs +33 -0
- package/src/adapter/LocalSideEffectHandler.res +44 -0
- package/src/adapter/LocalSideEffectHandler.res.mjs +34 -0
- package/src/adapter/MCP_Server.res +4 -0
- package/src/adapter/MCP_Server.res.mjs +67 -0
- package/src/adapter/MCP_ServerInstance.res +362 -0
- package/src/adapter/MCP_ServerInstance.res.mjs +313 -0
- package/src/adapter/PlatformGraphQL_Server.res +75 -0
- package/src/adapter/PlatformGraphQL_Server.res.mjs +84 -0
- package/src/adapter/PlatformMCP_Server.res +23 -0
- package/src/adapter/PlatformMCP_Server.res.mjs +46 -0
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +793 -0
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +569 -0
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +242 -0
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +241 -0
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +339 -0
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res.mjs +318 -0
- package/src/adapter/QueryDb/SortKey_Filter.res +48 -0
- package/src/adapter/QueryDb/SortKey_Filter.res.mjs +46 -0
- package/src/adapter/QueryEngine/LocalQueryEngine.res +57 -0
- package/src/adapter/QueryEngine/LocalQueryEngine.res.mjs +56 -0
- package/src/adapter/Runtime/LocalAggregateRuntime_Builder.res +91 -0
- package/src/adapter/Runtime/LocalAggregateRuntime_Builder.res.mjs +50 -0
- package/src/adapter/Runtime/LocalEventCollectorRuntime_Builder.res +47 -0
- package/src/adapter/Runtime/LocalEventCollectorRuntime_Builder.res.mjs +37 -0
- package/src/adapter/Runtime/LocalPluginRuntime_Builder.res +11 -0
- package/src/adapter/Runtime/LocalPluginRuntime_Builder.res.mjs +30 -0
- package/src/adapter/Runtime/LocalRuntimeEnvironment.res +54 -0
- package/src/adapter/Runtime/LocalRuntimeEnvironment.res.mjs +40 -0
- package/src/adapter/Scheduler/LocalScheduledPublisher.res +102 -0
- package/src/adapter/Scheduler/LocalScheduledPublisher.res.mjs +84 -0
- package/src/adapter/SqliteDriver.res +52 -0
- package/src/adapter/SqliteDriver.res.mjs +76 -0
- package/src/adapter/SqliteDriver.resi +31 -0
- package/src/adapter/Task/LocalTaskBucket.res +75 -0
- package/src/adapter/Task/LocalTaskBucket.res.mjs +48 -0
- package/src/adapter/Task/TaskBucket_Sqlite.res +47 -0
- package/src/adapter/Task/TaskBucket_Sqlite.res.mjs +40 -0
- package/src/components/Aggregate_Builder.res +55 -0
- package/src/components/Aggregate_Builder.res.mjs +140 -0
- package/src/components/AutomationSlice_Builder.res +41 -0
- package/src/components/AutomationSlice_Builder.res.mjs +57 -0
- package/src/components/Counter_Builder.res +14 -0
- package/src/components/Counter_Builder.res.mjs +32 -0
- package/src/components/DcbEventLog_Builder.res +18 -0
- package/src/components/DcbEventLog_Builder.res.mjs +24 -0
- package/src/components/ExtensionPoint_Builder.res +25 -0
- package/src/components/ExtensionPoint_Builder.res.mjs +44 -0
- package/src/components/InboundTranslationSlice_Builder.res +34 -0
- package/src/components/InboundTranslationSlice_Builder.res.mjs +45 -0
- package/src/components/OutboundTranslationSlice_Builder.res +43 -0
- package/src/components/OutboundTranslationSlice_Builder.res.mjs +59 -0
- package/src/components/Plugin_Builder.res +37 -0
- package/src/components/Plugin_Builder.res.mjs +67 -0
- package/src/components/ReadModel_Builder.res +47 -0
- package/src/components/ReadModel_Builder.res.mjs +54 -0
- package/src/components/StateChangeSlice_Builder.res +19 -0
- package/src/components/StateChangeSlice_Builder.res.mjs +21 -0
- package/src/components/StateViewSlice_Builder.res +42 -0
- package/src/components/StateViewSlice_Builder.res.mjs +58 -0
- package/src/components/Task_Builder.res +28 -0
- package/src/components/Task_Builder.res.mjs +41 -0
- package/src/reexport/CommandTopic.res +3 -0
- package/src/reexport/CommandTopic.res.mjs +55 -0
- package/src/test/Mocks/MockDcbEventLogStorage.res +136 -0
- package/src/test/Mocks/MockDcbEventLogStorage.res.mjs +146 -0
- package/src/test/Mocks/MockEventLogStorage.res +57 -0
- package/src/test/Mocks/MockEventLogStorage.res.mjs +65 -0
- package/src/test/Mocks/MockPublisher.res +29 -0
- package/src/test/Mocks/MockPublisher.res.mjs +28 -0
- package/src/test/Mocks/MockQueryDbStorage.res +104 -0
- package/src/test/Mocks/MockQueryDbStorage.res.mjs +136 -0
- package/src/test/TestRunner.res +96 -0
- package/src/test/TestRunner.res.mjs +71 -0
- package/tests/SplitApiFixtures.res +84 -0
- package/tests/SplitApiFixtures.res.mjs +88 -0
- package/tests/SplitApiTest.res +102 -0
- package/tests/SplitApiTest.res.mjs +82 -0
- package/tests/TestFixtures.res +54 -0
- package/tests/TestFixtures.res.mjs +65 -0
- package/tests/adapter/BackendParityTest.res +94 -0
- package/tests/adapter/BackendParityTest.res.mjs +96 -0
- package/tests/adapter/CollectNEventsTest.res +82 -0
- package/tests/adapter/CollectNEventsTest.res.mjs +83 -0
- package/tests/adapter/CommandAuthorizationTest.res +200 -0
- package/tests/adapter/CommandAuthorizationTest.res.mjs +190 -0
- package/tests/adapter/CommandTopicChannelTest.res +174 -0
- package/tests/adapter/CommandTopicChannelTest.res.mjs +170 -0
- package/tests/adapter/CounterHandlerTest.res +113 -0
- package/tests/adapter/CounterHandlerTest.res.mjs +96 -0
- package/tests/adapter/DcbEventLogStorageSqliteTest.res +157 -0
- package/tests/adapter/DcbEventLogStorageSqliteTest.res.mjs +165 -0
- package/tests/adapter/DcbEventLogStorageTest.res +187 -0
- package/tests/adapter/DcbEventLogStorageTest.res.mjs +195 -0
- package/tests/adapter/EventCollectorChannelTest.res +166 -0
- package/tests/adapter/EventCollectorChannelTest.res.mjs +178 -0
- package/tests/adapter/EventLogStorageSqliteTest.res +105 -0
- package/tests/adapter/EventLogStorageSqliteTest.res.mjs +116 -0
- package/tests/adapter/EventTapTest.res +102 -0
- package/tests/adapter/EventTapTest.res.mjs +94 -0
- package/tests/adapter/EventTopicPublisherTest.res +76 -0
- package/tests/adapter/EventTopicPublisherTest.res.mjs +86 -0
- package/tests/adapter/GraphQL_SchemaInspectorTest.res +763 -0
- package/tests/adapter/GraphQL_SchemaInspectorTest.res.mjs +578 -0
- package/tests/adapter/GraphQL_SubscriptionResolversTest.res +161 -0
- package/tests/adapter/GraphQL_SubscriptionResolversTest.res.mjs +134 -0
- package/tests/adapter/HeartbeatRunnerTest.res +136 -0
- package/tests/adapter/HeartbeatRunnerTest.res.mjs +96 -0
- package/tests/adapter/LocalAuthHttpTest.res +224 -0
- package/tests/adapter/LocalAuthHttpTest.res.mjs +237 -0
- package/tests/adapter/LocalAuthLoginTest.res +180 -0
- package/tests/adapter/LocalAuthLoginTest.res.mjs +207 -0
- package/tests/adapter/LocalAuthTest.res +106 -0
- package/tests/adapter/LocalAuthTest.res.mjs +145 -0
- package/tests/adapter/LocalAuthUserStoreTest.res +172 -0
- package/tests/adapter/LocalAuthUserStoreTest.res.mjs +208 -0
- package/tests/adapter/LocalBusBoundedTest.res +157 -0
- package/tests/adapter/LocalBusBoundedTest.res.mjs +168 -0
- package/tests/adapter/LocalBusPubSubTest.res +81 -0
- package/tests/adapter/LocalBusPubSubTest.res.mjs +81 -0
- package/tests/adapter/QueryDbGsiTtlTest.res +268 -0
- package/tests/adapter/QueryDbGsiTtlTest.res.mjs +209 -0
- package/tests/adapter/QueryDbListResolverTest.res +361 -0
- package/tests/adapter/QueryDbListResolverTest.res.mjs +426 -0
- package/tests/adapter/QueryDbStorageSqliteTest.res +159 -0
- package/tests/adapter/QueryDbStorageSqliteTest.res.mjs +126 -0
- package/tests/adapter/QueryDbStorageTest.res +171 -0
- package/tests/adapter/QueryDbStorageTest.res.mjs +170 -0
- package/tests/adapter/QueryEngineTest.res +152 -0
- package/tests/adapter/QueryEngineTest.res.mjs +148 -0
- package/tests/adapter/ScheduledPublisherTest.res +199 -0
- package/tests/adapter/ScheduledPublisherTest.res.mjs +203 -0
- package/tests/adapter/SortKey_FilterTest.res +103 -0
- package/tests/adapter/SortKey_FilterTest.res.mjs +135 -0
- package/tests/adapter/SqliteDriverTest.res +86 -0
- package/tests/adapter/SqliteDriverTest.res.mjs +101 -0
- package/tests/adapter/TaskBucketSqliteTest.res +44 -0
- package/tests/adapter/TaskBucketSqliteTest.res.mjs +40 -0
- package/tests/adapter/TaskBucketTest.res +106 -0
- package/tests/adapter/TaskBucketTest.res.mjs +118 -0
- package/tests/components/aggregate/AggregateFixtures.res +78 -0
- package/tests/components/aggregate/AggregateFixtures.res.mjs +109 -0
- package/tests/components/aggregate/AggregateTest.res +129 -0
- package/tests/components/aggregate/AggregateTest.res.mjs +129 -0
- package/tests/components/automationslice/AutomationSliceCallbackTest.res +212 -0
- package/tests/components/automationslice/AutomationSliceCallbackTest.res.mjs +233 -0
- package/tests/components/automationslice/AutomationSliceFixtures.res +189 -0
- package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +276 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +289 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +453 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res +47 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res.mjs +31 -0
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +213 -0
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +268 -0
- package/tests/components/automationslice/MixedSourceAutomationSliceTest.res +118 -0
- package/tests/components/automationslice/MixedSourceAutomationSliceTest.res.mjs +99 -0
- package/tests/components/commandgenerator/CommandGeneratorFixtures.res +73 -0
- package/tests/components/commandgenerator/CommandGeneratorFixtures.res.mjs +109 -0
- package/tests/components/commandgenerator/CommandGeneratorTest.res +43 -0
- package/tests/components/commandgenerator/CommandGeneratorTest.res.mjs +46 -0
- package/tests/components/commandtopic/CommandTopicFixtures.res +37 -0
- package/tests/components/commandtopic/CommandTopicFixtures.res.mjs +68 -0
- package/tests/components/commandtopic/CommandTopicStreamFixtures.res +43 -0
- package/tests/components/commandtopic/CommandTopicStreamFixtures.res.mjs +91 -0
- package/tests/components/commandtopic/CommandTopicStreamTest.res +30 -0
- package/tests/components/commandtopic/CommandTopicStreamTest.res.mjs +35 -0
- package/tests/components/commandtopic/CommandTopicTest.res +55 -0
- package/tests/components/commandtopic/CommandTopicTest.res.mjs +62 -0
- package/tests/components/counter/CounterFixtures.res +42 -0
- package/tests/components/counter/CounterFixtures.res.mjs +34 -0
- package/tests/components/counter/CounterTest.res +46 -0
- package/tests/components/counter/CounterTest.res.mjs +61 -0
- package/tests/components/dcb/DcbCrossPartitionFixtures.res +170 -0
- package/tests/components/dcb/DcbCrossPartitionFixtures.res.mjs +276 -0
- package/tests/components/dcb/DcbCrossPartitionReadTest.res +68 -0
- package/tests/components/dcb/DcbCrossPartitionReadTest.res.mjs +78 -0
- package/tests/components/dcb/DcbEventLogAppendStreamTest.res +72 -0
- package/tests/components/dcb/DcbEventLogAppendStreamTest.res.mjs +82 -0
- package/tests/components/dcb/DcbEventLogStreamTest.res +103 -0
- package/tests/components/dcb/DcbEventLogStreamTest.res.mjs +91 -0
- package/tests/components/dcb/DcbFixtures.res +145 -0
- package/tests/components/dcb/DcbFixtures.res.mjs +221 -0
- package/tests/components/dcb/DcbTest.res +55 -0
- package/tests/components/dcb/DcbTest.res.mjs +55 -0
- package/tests/components/eventcollector/EventCollectorFixtures.res +42 -0
- package/tests/components/eventcollector/EventCollectorFixtures.res.mjs +49 -0
- package/tests/components/eventcollector/EventCollectorTest.res +37 -0
- package/tests/components/eventcollector/EventCollectorTest.res.mjs +36 -0
- package/tests/components/eventlog/EventLogAppendStreamTest.res +93 -0
- package/tests/components/eventlog/EventLogAppendStreamTest.res.mjs +113 -0
- package/tests/components/eventlog/EventLogConflictTest.res +86 -0
- package/tests/components/eventlog/EventLogConflictTest.res.mjs +81 -0
- package/tests/components/eventlog/EventLogFixtures.res +52 -0
- package/tests/components/eventlog/EventLogFixtures.res.mjs +80 -0
- package/tests/components/eventlog/EventLogStreamTest.res +98 -0
- package/tests/components/eventlog/EventLogStreamTest.res.mjs +142 -0
- package/tests/components/eventlog/EventLogTest.res +53 -0
- package/tests/components/eventlog/EventLogTest.res.mjs +74 -0
- package/tests/components/eventtopic/EventTopicFixtures.res +47 -0
- package/tests/components/eventtopic/EventTopicFixtures.res.mjs +77 -0
- package/tests/components/eventtopic/EventTopicStreamFixtures.res +27 -0
- package/tests/components/eventtopic/EventTopicStreamFixtures.res.mjs +62 -0
- package/tests/components/eventtopic/EventTopicStreamTest.res +35 -0
- package/tests/components/eventtopic/EventTopicStreamTest.res.mjs +44 -0
- package/tests/components/eventtopic/EventTopicTest.res +35 -0
- package/tests/components/eventtopic/EventTopicTest.res.mjs +43 -0
- package/tests/components/extensionpoint/ExtensionPointFixtures.res +145 -0
- package/tests/components/extensionpoint/ExtensionPointFixtures.res.mjs +188 -0
- package/tests/components/extensionpoint/ExtensionPointTest.res +62 -0
- package/tests/components/extensionpoint/ExtensionPointTest.res.mjs +55 -0
- package/tests/components/heartbeat/HeartbeatFixtures.res +48 -0
- package/tests/components/heartbeat/HeartbeatFixtures.res.mjs +46 -0
- package/tests/components/heartbeat/HeartbeatTest.res +67 -0
- package/tests/components/heartbeat/HeartbeatTest.res.mjs +65 -0
- package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +241 -0
- package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res.mjs +239 -0
- package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +36 -0
- package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res.mjs +73 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +191 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs +297 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +78 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res.mjs +137 -0
- package/tests/components/querydb/QueryDbFixtures.res +39 -0
- package/tests/components/querydb/QueryDbFixtures.res.mjs +73 -0
- package/tests/components/querydb/QueryDbSubIdFixtures.res +44 -0
- package/tests/components/querydb/QueryDbSubIdFixtures.res.mjs +80 -0
- package/tests/components/querydb/QueryDbSubIdTest.res +104 -0
- package/tests/components/querydb/QueryDbSubIdTest.res.mjs +104 -0
- package/tests/components/querydb/QueryDbTest.res +81 -0
- package/tests/components/querydb/QueryDbTest.res.mjs +126 -0
- package/tests/components/readmodel/DcbReadModelE2EFixtures.res +215 -0
- package/tests/components/readmodel/DcbReadModelE2EFixtures.res.mjs +308 -0
- package/tests/components/readmodel/DcbReadModelE2ETest.res +45 -0
- package/tests/components/readmodel/DcbReadModelE2ETest.res.mjs +37 -0
- package/tests/components/readmodel/DcbReadModelTypoFailFastTest.res +88 -0
- package/tests/components/readmodel/DcbReadModelTypoFailFastTest.res.mjs +152 -0
- package/tests/components/readmodel/MixedSourceReadModelFixtures.res +181 -0
- package/tests/components/readmodel/MixedSourceReadModelFixtures.res.mjs +277 -0
- package/tests/components/readmodel/MixedSourceReadModelTest.res +61 -0
- package/tests/components/readmodel/MixedSourceReadModelTest.res.mjs +68 -0
- package/tests/components/readmodel/ReadModelFixtures.res +138 -0
- package/tests/components/readmodel/ReadModelFixtures.res.mjs +169 -0
- package/tests/components/readmodel/ReadModelTest.res +46 -0
- package/tests/components/readmodel/ReadModelTest.res.mjs +36 -0
- package/tests/components/scheduler/SchedulerFixtures.res +59 -0
- package/tests/components/scheduler/SchedulerFixtures.res.mjs +63 -0
- package/tests/components/scheduler/SchedulerTest.res +76 -0
- package/tests/components/scheduler/SchedulerTest.res.mjs +86 -0
- package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res +133 -0
- package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res.mjs +149 -0
- package/tests/components/sideeffecthandler/SideEffectHandlerTest.res +39 -0
- package/tests/components/sideeffecthandler/SideEffectHandlerTest.res.mjs +28 -0
- package/tests/components/stateviewslice/StateViewSliceFixtures.res +117 -0
- package/tests/components/stateviewslice/StateViewSliceFixtures.res.mjs +188 -0
- package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res +117 -0
- package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res.mjs +183 -0
- package/tests/components/stateviewslice/StateViewSliceSubIdTest.res +78 -0
- package/tests/components/stateviewslice/StateViewSliceSubIdTest.res.mjs +107 -0
- package/tests/components/stateviewslice/StateViewSliceTest.res +59 -0
- package/tests/components/stateviewslice/StateViewSliceTest.res.mjs +72 -0
- package/tests/components/task/TaskFixtures.res +87 -0
- package/tests/components/task/TaskFixtures.res.mjs +82 -0
- package/tests/components/task/TaskTest.res +98 -0
- package/tests/components/task/TaskTest.res.mjs +46 -0
- package/tests/plugin/PluginBehavior_GWT.res +146 -0
- package/tests/plugin/PluginBehavior_GWT.res.mjs +281 -0
- package/tests/plugin/Plugin_Fixtures.res +61 -0
- package/tests/plugin/Plugin_Fixtures.res.mjs +105 -0
- package/tests/plugin/PluginsProjection_GWT.res +95 -0
- package/tests/plugin/PluginsProjection_GWT.res.mjs +120 -0
- package/tests/plugin/UIFragmentRegistryProjection_GWT.res +72 -0
- package/tests/plugin/UIFragmentRegistryProjection_GWT.res.mjs +117 -0
- package/tests/setup/sqliteGlobal.cjs +12 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// Stage A4 — end-to-end test for the POST /__inmemory/login + /logout HTTP
|
|
2
|
+
// endpoints. Boots a real DomainGraphQL_Server on a private port, posts
|
|
3
|
+
// credentials with the global `fetch` (Node 22+), then stops the server.
|
|
4
|
+
|
|
5
|
+
@@warning("-44")
|
|
6
|
+
|
|
7
|
+
open JestGlobals
|
|
8
|
+
|
|
9
|
+
let _ = TestRunner.setup()
|
|
10
|
+
|
|
11
|
+
let port = 4321
|
|
12
|
+
|
|
13
|
+
// ── Minimal node:http POST helper ──────────────────────────────────────────
|
|
14
|
+
// Jest 27's VM context strips Node's global `fetch` (and undici needs more
|
|
15
|
+
// polyfills than we want to maintain), so the test talks to the server via
|
|
16
|
+
// `node:http` directly. Same wire format — the SPA still uses fetch.
|
|
17
|
+
|
|
18
|
+
type httpRequestOptions = {
|
|
19
|
+
hostname: string,
|
|
20
|
+
port: int,
|
|
21
|
+
path: string,
|
|
22
|
+
method: string,
|
|
23
|
+
headers: dict<string>,
|
|
24
|
+
}
|
|
25
|
+
type httpReq
|
|
26
|
+
type httpRes
|
|
27
|
+
@module("node:http") external _request: (httpRequestOptions, httpRes => unit) => httpReq = "request"
|
|
28
|
+
@send external _reqWrite: (httpReq, string) => unit = "write"
|
|
29
|
+
@send external _reqEnd: (httpReq, @as(json`null`) _) => unit = "end"
|
|
30
|
+
@send external _reqOnError: (httpReq, @as("error") _, 'err => unit) => unit = "on"
|
|
31
|
+
@send external _resOnData: (httpRes, @as("data") _, 'chunk => unit) => unit = "on"
|
|
32
|
+
@send external _resOnEnd: (httpRes, @as("end") _, unit => unit) => unit = "on"
|
|
33
|
+
@send external _resSetEncoding: (httpRes, string) => unit = "setEncoding"
|
|
34
|
+
@get external _resStatusCode: httpRes => int = "statusCode"
|
|
35
|
+
|
|
36
|
+
let postJson = (path: string, body: dict<JSON.t>): promise<(int, JSON.t)> =>
|
|
37
|
+
Promise.make((resolve, reject) => {
|
|
38
|
+
let bodyStr = body->JSON.Encode.object->JSON.stringify
|
|
39
|
+
let req =
|
|
40
|
+
_request(
|
|
41
|
+
{
|
|
42
|
+
hostname: "localhost",
|
|
43
|
+
port,
|
|
44
|
+
path,
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: Dict.fromArray([
|
|
47
|
+
("content-type", "application/json"),
|
|
48
|
+
("content-length", bodyStr->String.length->Int.toString),
|
|
49
|
+
]),
|
|
50
|
+
},
|
|
51
|
+
res => {
|
|
52
|
+
let status = _resStatusCode(res)
|
|
53
|
+
let buf = ref("")
|
|
54
|
+
res->_resSetEncoding("utf8")
|
|
55
|
+
res->_resOnData(chunk => buf := buf.contents ++ Obj.magic(chunk))
|
|
56
|
+
res->_resOnEnd(() => {
|
|
57
|
+
let parsed = try {
|
|
58
|
+
buf.contents->JSON.parseOrThrow
|
|
59
|
+
} catch {
|
|
60
|
+
| _ => JSON.Encode.null
|
|
61
|
+
}
|
|
62
|
+
resolve((status, parsed))
|
|
63
|
+
})
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
req->_reqOnError(err => reject(Obj.magic(err)))
|
|
67
|
+
req->_reqWrite(bodyStr)
|
|
68
|
+
req->_reqEnd
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
let postEmpty = (path: string): promise<int> =>
|
|
72
|
+
Promise.make((resolve, reject) => {
|
|
73
|
+
let req =
|
|
74
|
+
_request(
|
|
75
|
+
{hostname: "localhost", port, path, method: "POST", headers: Dict.make()},
|
|
76
|
+
res => {
|
|
77
|
+
let status = _resStatusCode(res)
|
|
78
|
+
// Drain (otherwise the socket may stay open under HTTP keep-alive).
|
|
79
|
+
res->_resOnData(_ => ())
|
|
80
|
+
res->_resOnEnd(() => resolve(status))
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
req->_reqOnError(err => reject(Obj.magic(err)))
|
|
84
|
+
req->_reqEnd
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
let postRaw = (path: string, body: string): promise<int> =>
|
|
88
|
+
Promise.make((resolve, reject) => {
|
|
89
|
+
let req =
|
|
90
|
+
_request(
|
|
91
|
+
{
|
|
92
|
+
hostname: "localhost",
|
|
93
|
+
port,
|
|
94
|
+
path,
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: Dict.fromArray([
|
|
97
|
+
("content-type", "application/json"),
|
|
98
|
+
("content-length", body->String.length->Int.toString),
|
|
99
|
+
]),
|
|
100
|
+
},
|
|
101
|
+
res => {
|
|
102
|
+
let status = _resStatusCode(res)
|
|
103
|
+
res->_resOnData(_ => ())
|
|
104
|
+
res->_resOnEnd(() => resolve(status))
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
req->_reqOnError(err => reject(Obj.magic(err)))
|
|
108
|
+
req->_reqWrite(body)
|
|
109
|
+
req->_reqEnd
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
let getJsonField = (j: JSON.t, k: string): option<JSON.t> =>
|
|
113
|
+
j->JSON.Decode.object->Option.flatMap(d => d->Dict.get(k))
|
|
114
|
+
|
|
115
|
+
let getString = (j: JSON.t, k: string): string =>
|
|
116
|
+
getJsonField(j, k)->Option.flatMap(JSON.Decode.string)->Option.getOr("")
|
|
117
|
+
|
|
118
|
+
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
beforeAllAsync(async () => {
|
|
121
|
+
LocalAuth.resetUsers()
|
|
122
|
+
LocalAuth.Login.resetStore()
|
|
123
|
+
LocalAuth.Login.setTokenSecret("http-test-secret")
|
|
124
|
+
LocalAuth.Login.setCredentials(
|
|
125
|
+
~username="alice",
|
|
126
|
+
~password="alice-pw",
|
|
127
|
+
~identity={
|
|
128
|
+
userId: "u-alice",
|
|
129
|
+
username: "alice",
|
|
130
|
+
groups: ["Editor"],
|
|
131
|
+
provider: InMemory,
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
DomainGraphQL_Server.reset()
|
|
135
|
+
DomainGraphQL_Server.start(~port, ())
|
|
136
|
+
// Give the listener a tick to bind.
|
|
137
|
+
await Promise.make((resolve, _) => setTimeout(() => resolve(), 50)->ignore)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
afterAll(() => {
|
|
141
|
+
DomainGraphQL_Server.stop()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// ── Tests ──────────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
testPromise("POST /__inmemory/login returns {token, identity} for valid credentials", async () => {
|
|
147
|
+
let (status, body) =
|
|
148
|
+
await postJson(
|
|
149
|
+
"/__inmemory/login",
|
|
150
|
+
Dict.fromArray([
|
|
151
|
+
("username", JSON.Encode.string("alice")),
|
|
152
|
+
("password", JSON.Encode.string("alice-pw")),
|
|
153
|
+
]),
|
|
154
|
+
)
|
|
155
|
+
expect(status)->toEqual(200)
|
|
156
|
+
let token = getString(body, "token")
|
|
157
|
+
expect(token->String.length > 0)->toEqual(true)
|
|
158
|
+
expect(token->String.includes("."))->toEqual(true)
|
|
159
|
+
// The echoed identity matches what LocalAuth.lookupUser returns.
|
|
160
|
+
switch getJsonField(body, "identity") {
|
|
161
|
+
| Some(idJson) =>
|
|
162
|
+
let id = idJson->S.parseJsonOrThrow(Reventless.Identity.schema)
|
|
163
|
+
expect(id.username)->toEqual("alice")
|
|
164
|
+
expect(id.groups)->toEqual(["Editor"])
|
|
165
|
+
| None => JsError.throwWithMessage("response missing identity field")
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
testPromise("POST /__inmemory/login returns 401 for wrong password", async () => {
|
|
170
|
+
let (status, body) =
|
|
171
|
+
await postJson(
|
|
172
|
+
"/__inmemory/login",
|
|
173
|
+
Dict.fromArray([
|
|
174
|
+
("username", JSON.Encode.string("alice")),
|
|
175
|
+
("password", JSON.Encode.string("WRONG")),
|
|
176
|
+
]),
|
|
177
|
+
)
|
|
178
|
+
expect(status)->toEqual(401)
|
|
179
|
+
expect(getString(body, "error")->String.length > 0)->toEqual(true)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
testPromise("POST /__inmemory/login returns 401 for missing user", async () => {
|
|
183
|
+
let (status, _) =
|
|
184
|
+
await postJson(
|
|
185
|
+
"/__inmemory/login",
|
|
186
|
+
Dict.fromArray([
|
|
187
|
+
("username", JSON.Encode.string("ghost")),
|
|
188
|
+
("password", JSON.Encode.string("x")),
|
|
189
|
+
]),
|
|
190
|
+
)
|
|
191
|
+
expect(status)->toEqual(401)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
testPromise("POST /__inmemory/login returns 401 for malformed JSON body", async () => {
|
|
195
|
+
let status = await postRaw("/__inmemory/login", "not-json")
|
|
196
|
+
expect(status)->toEqual(401)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
testPromise("POST /__inmemory/logout always returns 204", async () => {
|
|
200
|
+
let status = await postEmpty("/__inmemory/logout")
|
|
201
|
+
expect(status)->toEqual(204)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
testPromise("Bearer token issued via HTTP round-trips through authenticate", async () => {
|
|
205
|
+
let (_, body) =
|
|
206
|
+
await postJson(
|
|
207
|
+
"/__inmemory/login",
|
|
208
|
+
Dict.fromArray([
|
|
209
|
+
("username", JSON.Encode.string("alice")),
|
|
210
|
+
("password", JSON.Encode.string("alice-pw")),
|
|
211
|
+
]),
|
|
212
|
+
)
|
|
213
|
+
let token = getString(body, "token")
|
|
214
|
+
let result =
|
|
215
|
+
await LocalAuth.authenticate({
|
|
216
|
+
headers: Dict.fromArray([("authorization", "Bearer " ++ token)]),
|
|
217
|
+
})
|
|
218
|
+
switch result {
|
|
219
|
+
| Authenticated(identity) =>
|
|
220
|
+
expect(identity.username)->toEqual("alice")
|
|
221
|
+
expect(identity.groups)->toEqual(["Editor"])
|
|
222
|
+
| _ => JsError.throwWithMessage("expected Authenticated(alice) from issued token")
|
|
223
|
+
}
|
|
224
|
+
})
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Nodehttp from "node:http";
|
|
5
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
6
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
8
|
+
import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
|
|
9
|
+
import * as LocalAuth$ReventlessLocal from "../../src/adapter/Auth/LocalAuth.res.mjs";
|
|
10
|
+
import * as TestRunner$ReventlessLocal from "../../src/test/TestRunner.res.mjs";
|
|
11
|
+
import * as DomainGraphQL_Server$ReventlessLocal from "../../src/adapter/DomainGraphQL_Server.res.mjs";
|
|
12
|
+
|
|
13
|
+
TestRunner$ReventlessLocal.setup();
|
|
14
|
+
|
|
15
|
+
function postJson(path, body) {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
let bodyStr = JSON.stringify(body);
|
|
18
|
+
let req = Nodehttp.request({
|
|
19
|
+
hostname: "localhost",
|
|
20
|
+
port: 4321,
|
|
21
|
+
path: path,
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: Object.fromEntries([
|
|
24
|
+
[
|
|
25
|
+
"content-type",
|
|
26
|
+
"application/json"
|
|
27
|
+
],
|
|
28
|
+
[
|
|
29
|
+
"content-length",
|
|
30
|
+
bodyStr.length.toString()
|
|
31
|
+
]
|
|
32
|
+
])
|
|
33
|
+
}, res => {
|
|
34
|
+
let status = res.statusCode;
|
|
35
|
+
let buf = {
|
|
36
|
+
contents: ""
|
|
37
|
+
};
|
|
38
|
+
res.setEncoding("utf8");
|
|
39
|
+
res.on("data", chunk => {
|
|
40
|
+
buf.contents = buf.contents + chunk;
|
|
41
|
+
});
|
|
42
|
+
res.on("end", () => {
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = JSON.parse(buf.contents);
|
|
46
|
+
} catch (exn) {
|
|
47
|
+
parsed = null;
|
|
48
|
+
}
|
|
49
|
+
resolve([
|
|
50
|
+
status,
|
|
51
|
+
parsed
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
req.on("error", err => reject(err));
|
|
56
|
+
req.write(bodyStr);
|
|
57
|
+
req.end(null);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function postEmpty(path) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
let req = Nodehttp.request({
|
|
64
|
+
hostname: "localhost",
|
|
65
|
+
port: 4321,
|
|
66
|
+
path: path,
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: {}
|
|
69
|
+
}, res => {
|
|
70
|
+
let status = res.statusCode;
|
|
71
|
+
res.on("data", param => {});
|
|
72
|
+
res.on("end", () => resolve(status));
|
|
73
|
+
});
|
|
74
|
+
req.on("error", err => reject(err));
|
|
75
|
+
req.end(null);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function postRaw(path, body) {
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
let req = Nodehttp.request({
|
|
82
|
+
hostname: "localhost",
|
|
83
|
+
port: 4321,
|
|
84
|
+
path: path,
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: Object.fromEntries([
|
|
87
|
+
[
|
|
88
|
+
"content-type",
|
|
89
|
+
"application/json"
|
|
90
|
+
],
|
|
91
|
+
[
|
|
92
|
+
"content-length",
|
|
93
|
+
body.length.toString()
|
|
94
|
+
]
|
|
95
|
+
])
|
|
96
|
+
}, res => {
|
|
97
|
+
let status = res.statusCode;
|
|
98
|
+
res.on("data", param => {});
|
|
99
|
+
res.on("end", () => resolve(status));
|
|
100
|
+
});
|
|
101
|
+
req.on("error", err => reject(err));
|
|
102
|
+
req.write(body);
|
|
103
|
+
req.end(null);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getJsonField(j, k) {
|
|
108
|
+
return Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(j), d => d[k]);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getString(j, k) {
|
|
112
|
+
return Stdlib_Option.getOr(Stdlib_Option.flatMap(getJsonField(j, k), Stdlib_JSON.Decode.string), "");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
globalThis.beforeAll(async () => {
|
|
116
|
+
LocalAuth$ReventlessLocal.resetUsers();
|
|
117
|
+
LocalAuth$ReventlessLocal.Login.resetStore();
|
|
118
|
+
LocalAuth$ReventlessLocal.Login.setTokenSecret("http-test-secret");
|
|
119
|
+
LocalAuth$ReventlessLocal.Login.setCredentials("alice", "alice-pw", {
|
|
120
|
+
userId: "u-alice",
|
|
121
|
+
username: "alice",
|
|
122
|
+
groups: ["Editor"],
|
|
123
|
+
provider: "InMemory"
|
|
124
|
+
});
|
|
125
|
+
DomainGraphQL_Server$ReventlessLocal.reset();
|
|
126
|
+
DomainGraphQL_Server$ReventlessLocal.start(4321, undefined, undefined);
|
|
127
|
+
return await new Promise((resolve, param) => {
|
|
128
|
+
setTimeout(() => resolve(), 50);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
globalThis.afterAll(() => DomainGraphQL_Server$ReventlessLocal.stop());
|
|
133
|
+
|
|
134
|
+
globalThis.test("POST /__inmemory/login returns {token, identity} for valid credentials", async () => {
|
|
135
|
+
let match = await postJson("/__inmemory/login", Object.fromEntries([
|
|
136
|
+
[
|
|
137
|
+
"username",
|
|
138
|
+
"alice"
|
|
139
|
+
],
|
|
140
|
+
[
|
|
141
|
+
"password",
|
|
142
|
+
"alice-pw"
|
|
143
|
+
]
|
|
144
|
+
]));
|
|
145
|
+
let body = match[1];
|
|
146
|
+
globalThis.expect(match[0]).toEqual(200);
|
|
147
|
+
let token = getString(body, "token");
|
|
148
|
+
globalThis.expect(token.length > 0).toEqual(true);
|
|
149
|
+
globalThis.expect(token.includes(".")).toEqual(true);
|
|
150
|
+
let idJson = getJsonField(body, "identity");
|
|
151
|
+
if (idJson === undefined) {
|
|
152
|
+
return Stdlib_JsError.throwWithMessage("response missing identity field");
|
|
153
|
+
}
|
|
154
|
+
let id = S.parseJsonOrThrow(idJson, Identity$Reventless.schema);
|
|
155
|
+
globalThis.expect(id.username).toEqual("alice");
|
|
156
|
+
globalThis.expect(id.groups).toEqual(["Editor"]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
globalThis.test("POST /__inmemory/login returns 401 for wrong password", async () => {
|
|
160
|
+
let match = await postJson("/__inmemory/login", Object.fromEntries([
|
|
161
|
+
[
|
|
162
|
+
"username",
|
|
163
|
+
"alice"
|
|
164
|
+
],
|
|
165
|
+
[
|
|
166
|
+
"password",
|
|
167
|
+
"WRONG"
|
|
168
|
+
]
|
|
169
|
+
]));
|
|
170
|
+
globalThis.expect(match[0]).toEqual(401);
|
|
171
|
+
globalThis.expect(getString(match[1], "error").length > 0).toEqual(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
globalThis.test("POST /__inmemory/login returns 401 for missing user", async () => {
|
|
175
|
+
let match = await postJson("/__inmemory/login", Object.fromEntries([
|
|
176
|
+
[
|
|
177
|
+
"username",
|
|
178
|
+
"ghost"
|
|
179
|
+
],
|
|
180
|
+
[
|
|
181
|
+
"password",
|
|
182
|
+
"x"
|
|
183
|
+
]
|
|
184
|
+
]));
|
|
185
|
+
globalThis.expect(match[0]).toEqual(401);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
globalThis.test("POST /__inmemory/login returns 401 for malformed JSON body", async () => {
|
|
189
|
+
let status = await postRaw("/__inmemory/login", "not-json");
|
|
190
|
+
globalThis.expect(status).toEqual(401);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
globalThis.test("POST /__inmemory/logout always returns 204", async () => {
|
|
194
|
+
let status = await postEmpty("/__inmemory/logout");
|
|
195
|
+
globalThis.expect(status).toEqual(204);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
globalThis.test("Bearer token issued via HTTP round-trips through authenticate", async () => {
|
|
199
|
+
let match = await postJson("/__inmemory/login", Object.fromEntries([
|
|
200
|
+
[
|
|
201
|
+
"username",
|
|
202
|
+
"alice"
|
|
203
|
+
],
|
|
204
|
+
[
|
|
205
|
+
"password",
|
|
206
|
+
"alice-pw"
|
|
207
|
+
]
|
|
208
|
+
]));
|
|
209
|
+
let token = getString(match[1], "token");
|
|
210
|
+
let result = await LocalAuth$ReventlessLocal.authenticate({
|
|
211
|
+
headers: Object.fromEntries([[
|
|
212
|
+
"authorization",
|
|
213
|
+
"Bearer " + token
|
|
214
|
+
]])
|
|
215
|
+
});
|
|
216
|
+
if (typeof result !== "object") {
|
|
217
|
+
return Stdlib_JsError.throwWithMessage("expected Authenticated(alice) from issued token");
|
|
218
|
+
}
|
|
219
|
+
if (result.TAG !== "Authenticated") {
|
|
220
|
+
return Stdlib_JsError.throwWithMessage("expected Authenticated(alice) from issued token");
|
|
221
|
+
}
|
|
222
|
+
let identity = result._0;
|
|
223
|
+
globalThis.expect(identity.username).toEqual("alice");
|
|
224
|
+
globalThis.expect(identity.groups).toEqual(["Editor"]);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
let port = 4321;
|
|
228
|
+
|
|
229
|
+
export {
|
|
230
|
+
port,
|
|
231
|
+
postJson,
|
|
232
|
+
postEmpty,
|
|
233
|
+
postRaw,
|
|
234
|
+
getJsonField,
|
|
235
|
+
getString,
|
|
236
|
+
}
|
|
237
|
+
/* Not a pure module */
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Stage A4 — LocalAuth.Login (token issuance) + Bearer-token recognition
|
|
2
|
+
// in LocalAuth.authenticate.
|
|
3
|
+
|
|
4
|
+
@@warning("-44")
|
|
5
|
+
|
|
6
|
+
open JestGlobals
|
|
7
|
+
|
|
8
|
+
let alice: Reventless.Identity.t = {
|
|
9
|
+
userId: "u-alice",
|
|
10
|
+
username: "alice",
|
|
11
|
+
groups: ["Editor"],
|
|
12
|
+
provider: InMemory,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let bob: Reventless.Identity.t = {
|
|
16
|
+
userId: "u-bob",
|
|
17
|
+
username: "bob",
|
|
18
|
+
groups: ["Viewer"],
|
|
19
|
+
provider: InMemory,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let buildContext = (headers: array<(string, string)>): ReventlessCore.Auth_Adapter.requestContext => {
|
|
23
|
+
headers: Dict.fromArray(headers),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let resetAll = () => {
|
|
27
|
+
LocalAuth.resetUsers()
|
|
28
|
+
LocalAuth.Login.resetStore()
|
|
29
|
+
LocalAuth.Login.setTokenSecret("test-secret-deterministic")
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
testPromise("issue returns Ok(token) for valid credentials", async () => {
|
|
33
|
+
resetAll()
|
|
34
|
+
LocalAuth.Login.setCredentials(~username="alice", ~password="alice-pw", ~identity=alice)
|
|
35
|
+
let result = await LocalAuth.Login.issue(~username="alice", ~password="alice-pw")
|
|
36
|
+
switch result {
|
|
37
|
+
| Ok(token) =>
|
|
38
|
+
// Token shape: <base64url-payload>.<base64url-sig>
|
|
39
|
+
let parts = token->String.split(".")
|
|
40
|
+
expect(parts->Array.length)->toEqual(2)
|
|
41
|
+
| Error(msg) => JsError.throwWithMessage("unexpected Error: " ++ msg)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
testPromise("issue returns Error for unknown username", async () => {
|
|
46
|
+
resetAll()
|
|
47
|
+
let result = await LocalAuth.Login.issue(~username="ghost", ~password="x")
|
|
48
|
+
switch result {
|
|
49
|
+
| Error(_) => ()
|
|
50
|
+
| Ok(_) => JsError.throwWithMessage("expected Error")
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
testPromise("issue returns Error for wrong password", async () => {
|
|
55
|
+
resetAll()
|
|
56
|
+
LocalAuth.Login.setCredentials(~username="alice", ~password="alice-pw", ~identity=alice)
|
|
57
|
+
let result = await LocalAuth.Login.issue(~username="alice", ~password="WRONG")
|
|
58
|
+
switch result {
|
|
59
|
+
| Error(_) => ()
|
|
60
|
+
| Ok(_) => JsError.throwWithMessage("expected Error")
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
testPromise("verifyAndDecode round-trips the identity for a fresh token", async () => {
|
|
65
|
+
resetAll()
|
|
66
|
+
LocalAuth.Login.setCredentials(~username="alice", ~password="alice-pw", ~identity=alice)
|
|
67
|
+
let token = switch await LocalAuth.Login.issue(~username="alice", ~password="alice-pw") {
|
|
68
|
+
| Ok(t) => t
|
|
69
|
+
| Error(msg) => JsError.throwWithMessage("issue failed: " ++ msg)
|
|
70
|
+
}
|
|
71
|
+
switch LocalAuth.Login.verifyAndDecode(token) {
|
|
72
|
+
| Some(identity) =>
|
|
73
|
+
expect(identity.username)->toEqual("alice")
|
|
74
|
+
expect(identity.groups)->toEqual(["Editor"])
|
|
75
|
+
expect(identity.userId)->toEqual("u-alice")
|
|
76
|
+
| None => JsError.throwWithMessage("verifyAndDecode returned None")
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
testPromise("verifyAndDecode rejects a tampered payload", async () => {
|
|
81
|
+
resetAll()
|
|
82
|
+
LocalAuth.Login.setCredentials(~username="alice", ~password="alice-pw", ~identity=alice)
|
|
83
|
+
let token = switch await LocalAuth.Login.issue(~username="alice", ~password="alice-pw") {
|
|
84
|
+
| Ok(t) => t
|
|
85
|
+
| Error(msg) => JsError.throwWithMessage("issue failed: " ++ msg)
|
|
86
|
+
}
|
|
87
|
+
// Flip the first character of the payload — signature should no longer verify.
|
|
88
|
+
let parts = token->String.split(".")
|
|
89
|
+
let payload = parts->Array.getUnsafe(0)
|
|
90
|
+
let sig = parts->Array.getUnsafe(1)
|
|
91
|
+
let firstChar = payload->String.charAt(0)
|
|
92
|
+
let replacement = firstChar == "A" ? "B" : "A"
|
|
93
|
+
let tamperedPayload = replacement ++ payload->String.slice(~start=1, ~end=payload->String.length)
|
|
94
|
+
let tampered = `${tamperedPayload}.${sig}`
|
|
95
|
+
switch LocalAuth.Login.verifyAndDecode(tampered) {
|
|
96
|
+
| None => ()
|
|
97
|
+
| Some(_) => JsError.throwWithMessage("expected verifyAndDecode to reject tampered payload")
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
testPromise("verifyAndDecode rejects a tampered signature", async () => {
|
|
102
|
+
resetAll()
|
|
103
|
+
LocalAuth.Login.setCredentials(~username="alice", ~password="alice-pw", ~identity=alice)
|
|
104
|
+
let token = switch await LocalAuth.Login.issue(~username="alice", ~password="alice-pw") {
|
|
105
|
+
| Ok(t) => t
|
|
106
|
+
| Error(msg) => JsError.throwWithMessage("issue failed: " ++ msg)
|
|
107
|
+
}
|
|
108
|
+
// Append a junk char to the signature.
|
|
109
|
+
switch LocalAuth.Login.verifyAndDecode(token ++ "x") {
|
|
110
|
+
| None => ()
|
|
111
|
+
| Some(_) => JsError.throwWithMessage("expected verifyAndDecode to reject tampered signature")
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
testPromise("verifyAndDecode rejects a malformed token (no dot)", async () => {
|
|
116
|
+
resetAll()
|
|
117
|
+
switch LocalAuth.Login.verifyAndDecode("not-a-real-token") {
|
|
118
|
+
| None => ()
|
|
119
|
+
| Some(_) => JsError.throwWithMessage("expected None for malformed token")
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
testPromise("authenticate accepts a valid Bearer token", async () => {
|
|
124
|
+
resetAll()
|
|
125
|
+
LocalAuth.Login.setCredentials(~username="bob", ~password="bob-pw", ~identity=bob)
|
|
126
|
+
let token = switch await LocalAuth.Login.issue(~username="bob", ~password="bob-pw") {
|
|
127
|
+
| Ok(t) => t
|
|
128
|
+
| Error(msg) => JsError.throwWithMessage("issue failed: " ++ msg)
|
|
129
|
+
}
|
|
130
|
+
let result = await LocalAuth.authenticate(
|
|
131
|
+
buildContext([("authorization", "Bearer " ++ token)]),
|
|
132
|
+
)
|
|
133
|
+
switch result {
|
|
134
|
+
| Authenticated(identity) =>
|
|
135
|
+
expect(identity.username)->toEqual("bob")
|
|
136
|
+
expect(identity.groups)->toEqual(["Viewer"])
|
|
137
|
+
| _ => JsError.throwWithMessage("expected Authenticated(bob)")
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
testPromise("authenticate rejects invalid Bearer even when X-User is present", async () => {
|
|
142
|
+
resetAll()
|
|
143
|
+
// A present-but-unverifiable Bearer must reject with AuthError so the
|
|
144
|
+
// host-shell `on401` logout path fires. X-User is ignored.
|
|
145
|
+
let result = await LocalAuth.authenticate(
|
|
146
|
+
buildContext([("authorization", "Bearer not-a-token"), ("x-user", "admin")]),
|
|
147
|
+
)
|
|
148
|
+
switch result {
|
|
149
|
+
| AuthError(_) => ()
|
|
150
|
+
| _ => JsError.throwWithMessage("expected AuthError for invalid Bearer")
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
testPromise("Bearer outranks X-User when both present and Bearer is valid", async () => {
|
|
155
|
+
resetAll()
|
|
156
|
+
LocalAuth.Login.setCredentials(~username="bob", ~password="bob-pw", ~identity=bob)
|
|
157
|
+
let token = switch await LocalAuth.Login.issue(~username="bob", ~password="bob-pw") {
|
|
158
|
+
| Ok(t) => t
|
|
159
|
+
| Error(msg) => JsError.throwWithMessage("issue failed: " ++ msg)
|
|
160
|
+
}
|
|
161
|
+
let result = await LocalAuth.authenticate(
|
|
162
|
+
buildContext([("authorization", "Bearer " ++ token), ("x-user", "admin")]),
|
|
163
|
+
)
|
|
164
|
+
switch result {
|
|
165
|
+
| Authenticated(identity) => expect(identity.username)->toEqual("bob")
|
|
166
|
+
| _ => JsError.throwWithMessage("expected Bearer to outrank X-User")
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
testPromise("setCredentials mirrors identity into the X-User registry", async () => {
|
|
171
|
+
resetAll()
|
|
172
|
+
LocalAuth.Login.setCredentials(~username="alice", ~password="alice-pw", ~identity=alice)
|
|
173
|
+
let result = await LocalAuth.authenticate(buildContext([("x-user", "alice")]))
|
|
174
|
+
switch result {
|
|
175
|
+
| Authenticated(identity) =>
|
|
176
|
+
expect(identity.userId)->toEqual("u-alice")
|
|
177
|
+
expect(identity.groups)->toEqual(["Editor"])
|
|
178
|
+
| _ => JsError.throwWithMessage("expected Authenticated(alice) via X-User")
|
|
179
|
+
}
|
|
180
|
+
})
|