@reventlessdev/reventless-gwt 1.0.0-alpha.238 → 1.0.0-alpha.240
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 +21 -0
- package/package.json +6 -6
- package/src/Automation_GWT.res +130 -0
- package/src/Automation_GWT.res.mjs +193 -0
- package/src/Behavior_GWT.res +6 -6
- package/src/Behavior_GWT.res.mjs +5 -5
- package/src/Capabilities_Fake.res +157 -0
- package/src/Capabilities_Fake.res.mjs +254 -0
- package/src/Flow_GWT.res +112 -1
- package/src/Flow_GWT.res.mjs +165 -11
- package/src/InboundTranslation_GWT.res +112 -0
- package/src/InboundTranslation_GWT.res.mjs +190 -1
- package/src/OutboundTranslation_GWT.res +207 -4
- package/src/OutboundTranslation_GWT.res.mjs +257 -1
- package/src/StateChange/ExternalAddCategorySlice.res +1 -1
- package/src/StateChange/ExternalAddCategorySlice.res.mjs +2 -2
- package/src/StateChange/WithFixtures.res +1 -1
- package/src/StateChange/WithFixtures.res.mjs +2 -2
- package/src/StateChange/WithManualOpen.res +1 -1
- package/src/StateChange/WithManualOpen.res.mjs +2 -2
- package/tests/AutomationFromSliceGwtTest.res +145 -0
- package/tests/AutomationFromSliceGwtTest.res.mjs +353 -0
- package/tests/AutomationGwtTest.res.mjs +2 -2
- package/tests/DelegateGwtTest.res +5 -5
- package/tests/DelegateGwtTest.res.mjs +27 -27
- package/tests/FlowAggregateGwtTest.res.mjs +4 -4
- package/tests/FlowCrossPluginGwtTest.res +4 -4
- package/tests/FlowCrossPluginGwtTest.res.mjs +22 -22
- package/tests/FlowGwtTest.res.mjs +6 -6
- package/tests/InboundFromSliceGwtTest.res +63 -0
- package/tests/InboundFromSliceGwtTest.res.mjs +164 -0
- package/tests/InboundTranslationGwtTest.res.mjs +2 -7
- package/tests/MappingGwtTest.res.mjs +12 -12
- package/tests/OutboundFromSliceGwtTest.res +124 -0
- package/tests/OutboundFromSliceGwtTest.res.mjs +268 -0
- package/tests/StateChangeSliceAuthorizeGwtTest.res.mjs +20 -16
- package/tests/StateChangeSliceGwtTest.res.mjs +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,27 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 1.0.0-alpha.240 (2026-10-10)
|
|
7
|
+
|
|
8
|
+
* feat!: a command's rule is looked up by its name, without a cast ([5f02c40](https://github.com/ReventlessDev/reventless-core/commit/5f02c4081d077bf4a6951d8c9b2b16553a8ca87b))
|
|
9
|
+
|
|
10
|
+
### BREAKING CHANGES
|
|
11
|
+
|
|
12
|
+
* a spec that writes `commandAuthorization` by hand writes
|
|
13
|
+
`authorizationOf = (name: string) => …` instead; the PPX refuses the old name.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# 1.0.0-alpha.239 (2026-10-10)
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* an inbound translation is gated alike on every door, and encodes its commands by schema ([6ec3ce6](https://github.com/ReventlessDev/reventless-core/commit/6ec3ce672e53e2c162c64a90d103be691391be5e))
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* **gwt:** automation and translation tests take the slice as written, and the sidecars read them ([93a0919](https://github.com/ReventlessDev/reventless-core/commit/93a0919a0d66730cf9bf89144ad8fba3cb025332))
|
|
25
|
+
|
|
26
|
+
|
|
6
27
|
# 1.0.0-alpha.238 (2026-10-08)
|
|
7
28
|
|
|
8
29
|
**Note:** Version bump only for package @reventlessdev/reventless-gwt
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-gwt",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.240",
|
|
4
4
|
"description": "Given-When-Then DSLs and test harness for Reventless slice testing",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -14,17 +14,17 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"sury": "11.0.0",
|
|
17
|
-
"@reventlessdev/rescript-effect": "0.1.0-alpha.34",
|
|
18
17
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.11",
|
|
19
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.188",
|
|
20
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.291",
|
|
21
18
|
"@reventlessdev/rescript-node": "2.0.0-alpha.22",
|
|
22
|
-
"@reventlessdev/
|
|
19
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.34",
|
|
20
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.190",
|
|
21
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.293",
|
|
22
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.162"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"rescript": "12.3.1",
|
|
26
26
|
"sury-ppx": "11.0.0",
|
|
27
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
27
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.99"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"rescript": "^12.3.1"
|
package/src/Automation_GWT.res
CHANGED
|
@@ -56,6 +56,9 @@ module type T = {
|
|
|
56
56
|
let thenScenarioTodos: (scenario, array<(string, Spec.todoItem)>) => Outcome.outcome
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/** Deprecated: takes an adapter module that flattens one mapping. Use
|
|
60
|
+
`FromSlice`, which takes the slice as written; this goes one release after
|
|
61
|
+
the examples have moved. */
|
|
59
62
|
module Make = (Spec: SliceSpec): (T with module Spec = Spec) => {
|
|
60
63
|
module Spec = Spec
|
|
61
64
|
|
|
@@ -194,3 +197,130 @@ module Make = (Spec: SliceSpec): (T with module Spec = Spec) => {
|
|
|
194
197
|
|
|
195
198
|
let thenScenarioTodos = (scenario, expected) => thenTodos(scenario.todos, expected)
|
|
196
199
|
}
|
|
200
|
+
|
|
201
|
+
// The slice as written: its `Spec` and its `_Automation` body, with every
|
|
202
|
+
// mapping, `process` and `onExhausted`. Per-source verbs come from
|
|
203
|
+
// `Mapping(M)`, because a mapping's event type is hidden inside `mappings`.
|
|
204
|
+
module FromSlice = (
|
|
205
|
+
Spec: Reventless.AutomationSlice.Spec,
|
|
206
|
+
Automation: Reventless.AutomationSlice.Automation with module Spec := Spec,
|
|
207
|
+
) => {
|
|
208
|
+
module Route = AutomationSlice_Route.Make(Spec, Automation)
|
|
209
|
+
|
|
210
|
+
let describe = JestBind.describe
|
|
211
|
+
let test = (name, body) => JestBind.test(~slice=Spec.name, name, body)
|
|
212
|
+
|
|
213
|
+
let testContext: Reventless.AutomationSlice.context = {
|
|
214
|
+
environment: "test",
|
|
215
|
+
platformName: "test",
|
|
216
|
+
pluginName: "test",
|
|
217
|
+
sliceName: Spec.name,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** A source event as the runtime receives it: encoded, and addressed by its
|
|
221
|
+
source's name. Built with `Mapping(M).event`. */
|
|
222
|
+
type sourced = {sourceName: string, sourceId: string, payload: JSON.t}
|
|
223
|
+
|
|
224
|
+
type scenario = {
|
|
225
|
+
todos: array<(string, Spec.todoItem)>,
|
|
226
|
+
commands: array<(string, Spec.command)>,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let encTodos = (arr: array<(string, Spec.todoItem)>) =>
|
|
230
|
+
arr->Array.map(((id, t)) => (id, t->Message.encode(Spec.todoItemSchema)))
|
|
231
|
+
let encPair = ((id, cmd): (string, Spec.command)) => {
|
|
232
|
+
let d = Dict.make()
|
|
233
|
+
d->Dict.set("id", JSON.Encode.string(id))
|
|
234
|
+
d->Dict.set("command", cmd->Message.encode(Spec.commandSchema))
|
|
235
|
+
JSON.Encode.object(d)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let thenTodos = (actual, expected) =>
|
|
239
|
+
actual == expected
|
|
240
|
+
? Outcome.pass
|
|
241
|
+
: Outcome.fail(TodoMismatch({expected: encTodos(expected), actual: encTodos(actual)}))
|
|
242
|
+
|
|
243
|
+
let thenResolved = (actual: option<string>, expected: option<string>) => {
|
|
244
|
+
let asPair = opt => opt->Option.mapOr([], id => [(id, JSON.Encode.null)])
|
|
245
|
+
actual == expected
|
|
246
|
+
? Outcome.pass
|
|
247
|
+
: Outcome.fail(TodoMismatch({expected: asPair(expected), actual: asPair(actual)}))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
module Mapping = (M: Automation.Mapping) => {
|
|
251
|
+
let event = (e: M.sourceEvent, ~sourceId="") => {
|
|
252
|
+
sourceName: M.sourceName,
|
|
253
|
+
sourceId,
|
|
254
|
+
payload: e->Message.encode(M.sourceEventSchema),
|
|
255
|
+
}
|
|
256
|
+
let givenEvent = (e: M.sourceEvent) => e
|
|
257
|
+
let whenCollect = (e, ~sourceId="", ~context=testContext) => M.collect(e, ~sourceId, context)
|
|
258
|
+
let whenResolve = M.resolve
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Unit: process and abandonment
|
|
262
|
+
let givenTodo = (id: string, item: Spec.todoItem) => (id, item)
|
|
263
|
+
let whenProcess = ((id, item)) => Automation.process(id, item)
|
|
264
|
+
let whenExhausted = ((id, item)) => Automation.onExhausted(id, item)
|
|
265
|
+
let thenCommand = (actual, expectedId, expectedCmd) =>
|
|
266
|
+
switch actual {
|
|
267
|
+
| Some((id, cmd)) if id == expectedId && cmd == expectedCmd => Outcome.pass
|
|
268
|
+
| actual =>
|
|
269
|
+
Outcome.fail(
|
|
270
|
+
EventsMismatch({
|
|
271
|
+
expected: [encPair((expectedId, expectedCmd))],
|
|
272
|
+
actual: actual->Option.mapOr([], p => [encPair(p)]),
|
|
273
|
+
}),
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
let thenNoCommand = actual =>
|
|
277
|
+
switch actual {
|
|
278
|
+
| None => Outcome.pass
|
|
279
|
+
| Some(p) => Outcome.fail(NoEventExpected({actual: [encPair(p)]}))
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Scenario: a sweep routes each event in turn, as phase 1 does, so to-dos keep
|
|
283
|
+
// the order the events produced them.
|
|
284
|
+
let givenEvents = (es: array<sourced>) => es
|
|
285
|
+
let routeAll = (events: array<sourced>, context) =>
|
|
286
|
+
events->Array.flatMap(e =>
|
|
287
|
+
Route.route(e.payload, ~sourceName=e.sourceName, ~sourceId=e.sourceId, context)
|
|
288
|
+
)
|
|
289
|
+
/** Applies routed events as the runtime's to-do list does: the first writer of
|
|
290
|
+
an id wins, and a resolve completes only a row that already exists. */
|
|
291
|
+
let sweep = (routed: array<AutomationSlice_Route.routed<Spec.todoItem>>) => {
|
|
292
|
+
let rows: array<(string, Spec.todoItem, ref<bool>)> = []
|
|
293
|
+
routed->Array.forEach(r => {
|
|
294
|
+
r.collected->Array.forEach(((id, item)) =>
|
|
295
|
+
if !(rows->Array.some(((seen, _, _)) => seen == id)) {
|
|
296
|
+
rows->Array.push((id, item, ref(false)))
|
|
297
|
+
}
|
|
298
|
+
)
|
|
299
|
+
r.resolved->Option.forEach(id =>
|
|
300
|
+
rows->Array.forEach(
|
|
301
|
+
((seen, _, done)) =>
|
|
302
|
+
if seen == id {
|
|
303
|
+
done := true
|
|
304
|
+
},
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
})
|
|
308
|
+
let todos = rows->Array.filterMap(((id, item, done)) => done.contents ? None : Some((id, item)))
|
|
309
|
+
{todos, commands: todos->Array.filterMap(((id, item)) => Automation.process(id, item))}
|
|
310
|
+
}
|
|
311
|
+
let whenSweep = (events, ~context=testContext) => sweep(routeAll(events, context))
|
|
312
|
+
let thenCommands = (scenario, expected) =>
|
|
313
|
+
scenario.commands == expected
|
|
314
|
+
? Outcome.pass
|
|
315
|
+
: Outcome.fail(
|
|
316
|
+
EventsMismatch({
|
|
317
|
+
expected: expected->Array.map(encPair),
|
|
318
|
+
actual: scenario.commands->Array.map(encPair),
|
|
319
|
+
}),
|
|
320
|
+
)
|
|
321
|
+
let andThenEvents = (scenario, events, ~context=testContext) => {
|
|
322
|
+
let resolved = routeAll(events, context)->Array.filterMap(r => r.resolved)
|
|
323
|
+
{...scenario, todos: scenario.todos->Array.filter(((id, _)) => !(resolved->Array.includes(id)))}
|
|
324
|
+
}
|
|
325
|
+
let thenScenarioTodos = (scenario, expected) => thenTodos(scenario.todos, expected)
|
|
326
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
5
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
5
6
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
6
7
|
import * as Outcome$ReventlessGwt from "./Outcome.res.mjs";
|
|
7
8
|
import * as JestBind$ReventlessGwt from "./JestBind.res.mjs";
|
|
8
9
|
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
10
|
+
import * as AutomationSlice_Route$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Route.res.mjs";
|
|
9
11
|
|
|
10
12
|
function Make(Spec) {
|
|
11
13
|
let test = (name, body) => JestBind$ReventlessGwt.test(Spec.name, name, body);
|
|
@@ -152,7 +154,198 @@ function Make(Spec) {
|
|
|
152
154
|
};
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
function FromSlice(Spec) {
|
|
158
|
+
return Automation => {
|
|
159
|
+
let Route = AutomationSlice_Route$ReventlessCore.Make(Spec)(Automation);
|
|
160
|
+
let test = (name, body) => JestBind$ReventlessGwt.test(Spec.name, name, body);
|
|
161
|
+
let testContext_sliceName = Spec.name;
|
|
162
|
+
let testContext = {
|
|
163
|
+
environment: "test",
|
|
164
|
+
platformName: "test",
|
|
165
|
+
pluginName: "test",
|
|
166
|
+
sliceName: testContext_sliceName
|
|
167
|
+
};
|
|
168
|
+
let encTodos = arr => arr.map(param => [
|
|
169
|
+
param[0],
|
|
170
|
+
Message$ReventlessCore.encode(param[1], Spec.todoItemSchema)
|
|
171
|
+
]);
|
|
172
|
+
let encPair = param => {
|
|
173
|
+
let d = {};
|
|
174
|
+
d["id"] = param[0];
|
|
175
|
+
d["command"] = Message$ReventlessCore.encode(param[1], Spec.commandSchema);
|
|
176
|
+
return d;
|
|
177
|
+
};
|
|
178
|
+
let thenTodos = (actual, expected) => {
|
|
179
|
+
if (Primitive_object.equal(actual, expected)) {
|
|
180
|
+
return Outcome$ReventlessGwt.pass;
|
|
181
|
+
} else {
|
|
182
|
+
return Outcome$ReventlessGwt.fail({
|
|
183
|
+
TAG: "TodoMismatch",
|
|
184
|
+
expected: encTodos(expected),
|
|
185
|
+
actual: encTodos(actual)
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
let thenResolved = (actual, expected) => {
|
|
190
|
+
let asPair = opt => Stdlib_Option.mapOr(opt, [], id => [[
|
|
191
|
+
id,
|
|
192
|
+
null
|
|
193
|
+
]]);
|
|
194
|
+
if (Primitive_object.equal(actual, expected)) {
|
|
195
|
+
return Outcome$ReventlessGwt.pass;
|
|
196
|
+
} else {
|
|
197
|
+
return Outcome$ReventlessGwt.fail({
|
|
198
|
+
TAG: "TodoMismatch",
|
|
199
|
+
expected: asPair(expected),
|
|
200
|
+
actual: asPair(actual)
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
let Mapping = M => {
|
|
205
|
+
let event = (e, sourceIdOpt) => {
|
|
206
|
+
let sourceId = sourceIdOpt !== undefined ? sourceIdOpt : "";
|
|
207
|
+
return {
|
|
208
|
+
sourceName: M.sourceName,
|
|
209
|
+
sourceId: sourceId,
|
|
210
|
+
payload: Message$ReventlessCore.encode(e, M.sourceEventSchema)
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
let givenEvent = e => e;
|
|
214
|
+
let whenCollect = (e, sourceIdOpt, contextOpt) => {
|
|
215
|
+
let sourceId = sourceIdOpt !== undefined ? sourceIdOpt : "";
|
|
216
|
+
let context = contextOpt !== undefined ? contextOpt : testContext;
|
|
217
|
+
return M.collect(e, sourceId, context);
|
|
218
|
+
};
|
|
219
|
+
return {
|
|
220
|
+
event: event,
|
|
221
|
+
givenEvent: givenEvent,
|
|
222
|
+
whenCollect: whenCollect,
|
|
223
|
+
whenResolve: M.resolve
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
let givenTodo = (id, item) => [
|
|
227
|
+
id,
|
|
228
|
+
item
|
|
229
|
+
];
|
|
230
|
+
let whenProcess = param => Automation.process(param[0], param[1]);
|
|
231
|
+
let whenExhausted = param => Automation.onExhausted(param[0], param[1]);
|
|
232
|
+
let thenCommand = (actual, expectedId, expectedCmd) => {
|
|
233
|
+
if (actual !== undefined && actual[0] === expectedId && Primitive_object.equal(actual[1], expectedCmd)) {
|
|
234
|
+
return Outcome$ReventlessGwt.pass;
|
|
235
|
+
}
|
|
236
|
+
return Outcome$ReventlessGwt.fail({
|
|
237
|
+
TAG: "EventsMismatch",
|
|
238
|
+
expected: [encPair([
|
|
239
|
+
expectedId,
|
|
240
|
+
expectedCmd
|
|
241
|
+
])],
|
|
242
|
+
actual: Stdlib_Option.mapOr(actual, [], p => [encPair(p)])
|
|
243
|
+
});
|
|
244
|
+
};
|
|
245
|
+
let thenNoCommand = actual => {
|
|
246
|
+
if (actual !== undefined) {
|
|
247
|
+
return Outcome$ReventlessGwt.fail({
|
|
248
|
+
TAG: "NoEventExpected",
|
|
249
|
+
actual: [encPair(actual)]
|
|
250
|
+
});
|
|
251
|
+
} else {
|
|
252
|
+
return Outcome$ReventlessGwt.pass;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
let givenEvents = es => es;
|
|
256
|
+
let routeAll = (events, context) => events.flatMap(e => Route.route(e.payload, e.sourceName, e.sourceId, context));
|
|
257
|
+
let sweep = routed => {
|
|
258
|
+
let rows = [];
|
|
259
|
+
routed.forEach(r => {
|
|
260
|
+
r.collected.forEach(param => {
|
|
261
|
+
let id = param[0];
|
|
262
|
+
if (!rows.some(param => param[0] === id)) {
|
|
263
|
+
rows.push([
|
|
264
|
+
id,
|
|
265
|
+
param[1],
|
|
266
|
+
{
|
|
267
|
+
contents: false
|
|
268
|
+
}
|
|
269
|
+
]);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
Stdlib_Option.forEach(r.resolved, id => {
|
|
274
|
+
rows.forEach(param => {
|
|
275
|
+
if (param[0] === id) {
|
|
276
|
+
param[2].contents = true;
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
let todos = Stdlib_Array.filterMap(rows, param => {
|
|
283
|
+
if (param[2].contents) {
|
|
284
|
+
return;
|
|
285
|
+
} else {
|
|
286
|
+
return [
|
|
287
|
+
param[0],
|
|
288
|
+
param[1]
|
|
289
|
+
];
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
return {
|
|
293
|
+
todos: todos,
|
|
294
|
+
commands: Stdlib_Array.filterMap(todos, param => Automation.process(param[0], param[1]))
|
|
295
|
+
};
|
|
296
|
+
};
|
|
297
|
+
let whenSweep = (events, contextOpt) => {
|
|
298
|
+
let context = contextOpt !== undefined ? contextOpt : testContext;
|
|
299
|
+
return sweep(routeAll(events, context));
|
|
300
|
+
};
|
|
301
|
+
let thenCommands = (scenario, expected) => {
|
|
302
|
+
if (Primitive_object.equal(scenario.commands, expected)) {
|
|
303
|
+
return Outcome$ReventlessGwt.pass;
|
|
304
|
+
} else {
|
|
305
|
+
return Outcome$ReventlessGwt.fail({
|
|
306
|
+
TAG: "EventsMismatch",
|
|
307
|
+
expected: expected.map(encPair),
|
|
308
|
+
actual: scenario.commands.map(encPair)
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
let andThenEvents = (scenario, events, contextOpt) => {
|
|
313
|
+
let context = contextOpt !== undefined ? contextOpt : testContext;
|
|
314
|
+
let resolved = Stdlib_Array.filterMap(routeAll(events, context), r => r.resolved);
|
|
315
|
+
return {
|
|
316
|
+
todos: scenario.todos.filter(param => !resolved.includes(param[0])),
|
|
317
|
+
commands: scenario.commands
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
let thenScenarioTodos = (scenario, expected) => thenTodos(scenario.todos, expected);
|
|
321
|
+
return {
|
|
322
|
+
Route: Route,
|
|
323
|
+
describe: JestBind$ReventlessGwt.describe,
|
|
324
|
+
test: test,
|
|
325
|
+
testContext: testContext,
|
|
326
|
+
encTodos: encTodos,
|
|
327
|
+
encPair: encPair,
|
|
328
|
+
thenTodos: thenTodos,
|
|
329
|
+
thenResolved: thenResolved,
|
|
330
|
+
Mapping: Mapping,
|
|
331
|
+
givenTodo: givenTodo,
|
|
332
|
+
whenProcess: whenProcess,
|
|
333
|
+
whenExhausted: whenExhausted,
|
|
334
|
+
thenCommand: thenCommand,
|
|
335
|
+
thenNoCommand: thenNoCommand,
|
|
336
|
+
givenEvents: givenEvents,
|
|
337
|
+
routeAll: routeAll,
|
|
338
|
+
sweep: sweep,
|
|
339
|
+
whenSweep: whenSweep,
|
|
340
|
+
thenCommands: thenCommands,
|
|
341
|
+
andThenEvents: andThenEvents,
|
|
342
|
+
thenScenarioTodos: thenScenarioTodos
|
|
343
|
+
};
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
155
347
|
export {
|
|
156
348
|
Make,
|
|
349
|
+
FromSlice,
|
|
157
350
|
}
|
|
158
351
|
/* Message-ReventlessCore Not a pure module */
|
package/src/Behavior_GWT.res
CHANGED
|
@@ -119,7 +119,7 @@ module type ActingSpec = {
|
|
|
119
119
|
@schema
|
|
120
120
|
type command
|
|
121
121
|
type role
|
|
122
|
-
let
|
|
122
|
+
let authorizationOf: string => Reventless.Authorization.rule<role>
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
module Acting = (Spec: ActingSpec) => {
|
|
@@ -142,7 +142,7 @@ module Acting = (Spec: ActingSpec) => {
|
|
|
142
142
|
// a role maps to is a deployment's fact, not the scenario's.
|
|
143
143
|
let refusedByRule = (who: Caller.t, command: Spec.command): bool =>
|
|
144
144
|
if (
|
|
145
|
-
Spec.
|
|
145
|
+
Spec.authorizationOf(commandName(command))
|
|
146
146
|
->Reventless.Authorization.named
|
|
147
147
|
->Reventless.Authorization.admits(~signedIn=who.signedIn, ~holds=role =>
|
|
148
148
|
who.roles->Array.includes(role)
|
|
@@ -240,7 +240,7 @@ module type BehaviorSpec = {
|
|
|
240
240
|
type event
|
|
241
241
|
|
|
242
242
|
type role
|
|
243
|
-
let
|
|
243
|
+
let authorizationOf: string => Reventless.Authorization.rule<role>
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
module type Behavior = {
|
|
@@ -460,7 +460,7 @@ module Make = (Spec: BehaviorSpec, Behavior: Behavior with module Spec := Spec):
|
|
|
460
460
|
type command = Spec.command
|
|
461
461
|
let commandSchema = Spec.commandSchema
|
|
462
462
|
type role = Spec.role
|
|
463
|
-
let
|
|
463
|
+
let authorizationOf = Spec.authorizationOf
|
|
464
464
|
})
|
|
465
465
|
let asCaller = Guard.asCaller
|
|
466
466
|
|
|
@@ -729,7 +729,7 @@ module type AggregateSpec = {
|
|
|
729
729
|
@schema
|
|
730
730
|
type error
|
|
731
731
|
type role
|
|
732
|
-
let
|
|
732
|
+
let authorizationOf: string => Reventless.Authorization.rule<role>
|
|
733
733
|
}
|
|
734
734
|
|
|
735
735
|
module type AggregateT = {
|
|
@@ -790,7 +790,7 @@ module MakeFromAggregate = (Spec: AggregateSpec, Behavior: Behavior.T with modul
|
|
|
790
790
|
type command = Spec.command
|
|
791
791
|
let commandSchema = Spec.commandSchema
|
|
792
792
|
type role = Spec.role
|
|
793
|
-
let
|
|
793
|
+
let authorizationOf = Spec.authorizationOf
|
|
794
794
|
})
|
|
795
795
|
let asCaller = Guard.asCaller
|
|
796
796
|
|
package/src/Behavior_GWT.res.mjs
CHANGED
|
@@ -106,7 +106,7 @@ function Acting(Spec) {
|
|
|
106
106
|
};
|
|
107
107
|
let commandName = command => Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(command, Spec.commandSchema));
|
|
108
108
|
let refusedByRule = (who, command) => {
|
|
109
|
-
if (Authorization$Reventless.admits(Authorization$Reventless.named(Spec.
|
|
109
|
+
if (Authorization$Reventless.admits(Authorization$Reventless.named(Spec.authorizationOf(Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(command, Spec.commandSchema)))), who.signedIn, role => who.roles.includes(role))) {
|
|
110
110
|
return false;
|
|
111
111
|
} else {
|
|
112
112
|
refusal.contents = {
|
|
@@ -376,7 +376,7 @@ function Make(Spec) {
|
|
|
376
376
|
let name = Spec.name;
|
|
377
377
|
let historySchema = Spec.consumedEventSchema;
|
|
378
378
|
let commandSchema = Spec.commandSchema;
|
|
379
|
-
let
|
|
379
|
+
let authorizationOf = Spec.authorizationOf;
|
|
380
380
|
let caller = {
|
|
381
381
|
contents: undefined
|
|
382
382
|
};
|
|
@@ -394,7 +394,7 @@ function Make(Spec) {
|
|
|
394
394
|
refusal.contents = undefined;
|
|
395
395
|
let claim = Stdlib_Option.map(who, w => w.claim);
|
|
396
396
|
if (Stdlib_Option.mapOr(who, false, __x => {
|
|
397
|
-
if (Authorization$Reventless.admits(Authorization$Reventless.named(
|
|
397
|
+
if (Authorization$Reventless.admits(Authorization$Reventless.named(authorizationOf(Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(command, commandSchema)))), __x.signedIn, role => __x.roles.includes(role))) {
|
|
398
398
|
return false;
|
|
399
399
|
} else {
|
|
400
400
|
refusal.contents = {
|
|
@@ -747,7 +747,7 @@ function MakeFromAggregate(Spec) {
|
|
|
747
747
|
let name = Spec.name;
|
|
748
748
|
let historySchema = Spec.eventSchema;
|
|
749
749
|
let commandSchema = Spec.commandSchema;
|
|
750
|
-
let
|
|
750
|
+
let authorizationOf = Spec.authorizationOf;
|
|
751
751
|
let caller = {
|
|
752
752
|
contents: undefined
|
|
753
753
|
};
|
|
@@ -765,7 +765,7 @@ function MakeFromAggregate(Spec) {
|
|
|
765
765
|
refusal.contents = undefined;
|
|
766
766
|
let claim = Stdlib_Option.map(who, w => w.claim);
|
|
767
767
|
if (Stdlib_Option.mapOr(who, false, __x => {
|
|
768
|
-
if (Authorization$Reventless.admits(Authorization$Reventless.named(
|
|
768
|
+
if (Authorization$Reventless.admits(Authorization$Reventless.named(authorizationOf(Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(command, commandSchema)))), __x.signedIn, role => __x.roles.includes(role))) {
|
|
769
769
|
return false;
|
|
770
770
|
} else {
|
|
771
771
|
refusal.contents = {
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Recording fakes for `Reventless.Capabilities.t`: every call a `translate` makes
|
|
2
|
+
// is kept, in order, and each answer can be scripted. What an outbound test
|
|
3
|
+
// asserts with `thenSent`.
|
|
4
|
+
|
|
5
|
+
module Capabilities = Reventless.Capabilities
|
|
6
|
+
module Geocoding = Reventless.Geocoding
|
|
7
|
+
module Messaging = Reventless.Messaging
|
|
8
|
+
module IdentityProvider = Reventless.IdentityProvider
|
|
9
|
+
module Secrets = Reventless.Secrets
|
|
10
|
+
|
|
11
|
+
/** One call a slice made through its capabilities. */
|
|
12
|
+
type call =
|
|
13
|
+
| Geocoded({text: string})
|
|
14
|
+
| Sent({recipient: Messaging.recipient, message: Messaging.message})
|
|
15
|
+
| PrincipalCreated({
|
|
16
|
+
contact: Messaging.recipient,
|
|
17
|
+
credential: IdentityProvider.credential,
|
|
18
|
+
groups: array<string>,
|
|
19
|
+
})
|
|
20
|
+
| AddedToGroup({principal: IdentityProvider.principal, group: string})
|
|
21
|
+
| RemovedFromGroup({principal: IdentityProvider.principal, group: string})
|
|
22
|
+
| PrincipalDeleted({principal: IdentityProvider.principal})
|
|
23
|
+
| TokenDrawn({length: int, alphabet: Secrets.alphabet})
|
|
24
|
+
| Hashed({input: string})
|
|
25
|
+
|
|
26
|
+
type t = {
|
|
27
|
+
capabilities: Capabilities.t,
|
|
28
|
+
/** The calls made so far, oldest first. */
|
|
29
|
+
calls: unit => array<call>,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
Fakes whose answers are given per capability. An answer left out is a plain
|
|
34
|
+
success: a geocoder with no candidates, a send with a numbered receipt, a
|
|
35
|
+
principal with a numbered id, `token` (or `t` repeated to length), and a hash
|
|
36
|
+
that prefixes `"hash:"`. `~secrets` replaces the secret source whole, e.g. with
|
|
37
|
+
`Secrets.unavailable`; its calls are still recorded.
|
|
38
|
+
*/
|
|
39
|
+
let make = (
|
|
40
|
+
~geocode: option<Geocoding.search>=?,
|
|
41
|
+
~send: option<Messaging.send>=?,
|
|
42
|
+
~channels: array<Messaging.channel>=[Email, Sms],
|
|
43
|
+
~pushServices: array<Messaging.pushService>=[],
|
|
44
|
+
~createPrincipal: option<
|
|
45
|
+
(
|
|
46
|
+
~contact: Messaging.recipient,
|
|
47
|
+
~credential: IdentityProvider.credential,
|
|
48
|
+
~groups: array<string>,
|
|
49
|
+
) => promise<result<IdentityProvider.principal, IdentityProvider.failure>>,
|
|
50
|
+
>=?,
|
|
51
|
+
~token: option<string>=?,
|
|
52
|
+
~hash: option<string => string>=?,
|
|
53
|
+
~secrets: option<Secrets.t>=?,
|
|
54
|
+
(),
|
|
55
|
+
): t => {
|
|
56
|
+
let log: array<call> = []
|
|
57
|
+
let record = c => log->Array.push(c)
|
|
58
|
+
let geocode = async (~text) => {
|
|
59
|
+
record(Geocoded({text: text}))
|
|
60
|
+
switch geocode {
|
|
61
|
+
| Some(f) => await f(~text)
|
|
62
|
+
| None => Ok([])
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
let send = async (~recipient, ~message) => {
|
|
66
|
+
record(Sent({recipient, message}))
|
|
67
|
+
switch send {
|
|
68
|
+
| Some(f) => await f(~recipient, ~message)
|
|
69
|
+
| None => Ok({Messaging.ref: `receipt-${Int.toString(log->Array.length)}`})
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
let createPrincipal = async (~contact, ~credential, ~groups) => {
|
|
73
|
+
record(PrincipalCreated({contact, credential, groups}))
|
|
74
|
+
switch createPrincipal {
|
|
75
|
+
| Some(f) => await f(~contact, ~credential, ~groups)
|
|
76
|
+
| None => Ok({IdentityProvider.providerId: `principal-${Int.toString(log->Array.length)}`})
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
let capabilities: Capabilities.t = {
|
|
80
|
+
geocode,
|
|
81
|
+
messaging: Messaging.makeProvider(~emailAndSms=channels, ~pushServices, ~send),
|
|
82
|
+
identityProvider: IdentityProvider.make(
|
|
83
|
+
~createPrincipal,
|
|
84
|
+
~addToGroup=async (~principal, ~group) => {
|
|
85
|
+
record(AddedToGroup({principal, group}))
|
|
86
|
+
Ok()
|
|
87
|
+
},
|
|
88
|
+
~removeFromGroup=async (~principal, ~group) => {
|
|
89
|
+
record(RemovedFromGroup({principal, group}))
|
|
90
|
+
Ok()
|
|
91
|
+
},
|
|
92
|
+
~deletePrincipal=async (~principal) => {
|
|
93
|
+
record(PrincipalDeleted({principal: principal}))
|
|
94
|
+
Ok()
|
|
95
|
+
},
|
|
96
|
+
~operations=[CreatePrincipal, AddToGroup, RemoveFromGroup, DeletePrincipal],
|
|
97
|
+
),
|
|
98
|
+
secrets: {
|
|
99
|
+
randomToken: (~length, ~alphabet) => {
|
|
100
|
+
record(TokenDrawn({length, alphabet}))
|
|
101
|
+
switch secrets {
|
|
102
|
+
| Some(s) => s.randomToken(~length, ~alphabet)
|
|
103
|
+
| None => Ok(token->Option.getOr("t"->String.repeat(length)))
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
hash: input => {
|
|
107
|
+
record(Hashed({input: input}))
|
|
108
|
+
switch secrets {
|
|
109
|
+
| Some(s) => s.hash(input)
|
|
110
|
+
| None => Ok(hash->Option.mapOr(`hash:${input}`, f => f(input)))
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
{capabilities, calls: () => log->Array.copy}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** A readable rendering of a call, for a mismatch. */
|
|
119
|
+
let toJson = (c: call): JSON.t => {
|
|
120
|
+
let recipientText = (r: Messaging.recipient) =>
|
|
121
|
+
switch r {
|
|
122
|
+
| ToEmail(email) => `email:${email}`
|
|
123
|
+
| ToSms(phone) => `sms:${phone}`
|
|
124
|
+
| ToPush(address) => `push:${address->Messaging.serviceOf->Messaging.pushServiceToString}`
|
|
125
|
+
}
|
|
126
|
+
let obj = (tag, fields) =>
|
|
127
|
+
JSON.Encode.object(Dict.fromArray([("TAG", JSON.Encode.string(tag)), ...fields]))
|
|
128
|
+
let str = JSON.Encode.string
|
|
129
|
+
switch c {
|
|
130
|
+
| Geocoded({text}) => obj("Geocoded", [("text", str(text))])
|
|
131
|
+
| Sent({recipient, message}) =>
|
|
132
|
+
obj(
|
|
133
|
+
"Sent",
|
|
134
|
+
[
|
|
135
|
+
("recipient", str(recipientText(recipient))),
|
|
136
|
+
("subject", message.subject->Option.mapOr(JSON.Encode.null, str)),
|
|
137
|
+
("body", str(message.body)),
|
|
138
|
+
],
|
|
139
|
+
)
|
|
140
|
+
| PrincipalCreated({contact, groups, _}) =>
|
|
141
|
+
obj(
|
|
142
|
+
"PrincipalCreated",
|
|
143
|
+
[
|
|
144
|
+
("contact", str(recipientText(contact))),
|
|
145
|
+
("groups", groups->Array.map(str)->JSON.Encode.array),
|
|
146
|
+
],
|
|
147
|
+
)
|
|
148
|
+
| AddedToGroup({principal, group}) =>
|
|
149
|
+
obj("AddedToGroup", [("principal", str(principal.providerId)), ("group", str(group))])
|
|
150
|
+
| RemovedFromGroup({principal, group}) =>
|
|
151
|
+
obj("RemovedFromGroup", [("principal", str(principal.providerId)), ("group", str(group))])
|
|
152
|
+
| PrincipalDeleted({principal}) =>
|
|
153
|
+
obj("PrincipalDeleted", [("principal", str(principal.providerId))])
|
|
154
|
+
| TokenDrawn({length, _}) => obj("TokenDrawn", [("length", JSON.Encode.int(length))])
|
|
155
|
+
| Hashed({input}) => obj("Hashed", [("input", str(input))])
|
|
156
|
+
}
|
|
157
|
+
}
|