@reventlessdev/reventless-gwt 1.0.0-alpha.237 → 1.0.0-alpha.239
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 +18 -0
- package/package.json +5 -5
- package/src/Automation_GWT.res +130 -0
- package/src/Automation_GWT.res.mjs +193 -0
- 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/tests/AutomationFromSliceGwtTest.res +145 -0
- package/tests/AutomationFromSliceGwtTest.res.mjs +353 -0
- package/tests/InboundFromSliceGwtTest.res +63 -0
- package/tests/InboundFromSliceGwtTest.res.mjs +164 -0
- package/tests/InboundTranslationGwtTest.res.mjs +0 -5
- package/tests/OutboundFromSliceGwtTest.res +124 -0
- package/tests/OutboundFromSliceGwtTest.res.mjs +268 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
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.239 (2026-10-10)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* an inbound translation is gated alike on every door, and encodes its commands by schema ([6ec3ce6](https://github.com/ReventlessDev/reventless-core/commit/6ec3ce672e53e2c162c64a90d103be691391be5e))
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **gwt:** automation and translation tests take the slice as written, and the sidecars read them ([93a0919](https://github.com/ReventlessDev/reventless-core/commit/93a0919a0d66730cf9bf89144ad8fba3cb025332))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# 1.0.0-alpha.238 (2026-10-08)
|
|
17
|
+
|
|
18
|
+
**Note:** Version bump only for package @reventlessdev/reventless-gwt
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
6
24
|
# 1.0.0-alpha.237 (2026-10-08)
|
|
7
25
|
|
|
8
26
|
**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.239",
|
|
4
4
|
"description": "Given-When-Then DSLs and test harness for Reventless slice testing",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"sury": "11.0.0",
|
|
17
17
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.34",
|
|
18
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.292",
|
|
18
19
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.11",
|
|
19
|
-
"@reventlessdev/
|
|
20
|
-
"@reventlessdev/
|
|
21
|
-
"@reventlessdev/reventless-
|
|
22
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.159"
|
|
20
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.22",
|
|
21
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.189",
|
|
22
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.161"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"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 */
|
|
@@ -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
|
+
}
|