@reventlessdev/reventless-gwt 1.0.0-alpha.254 → 1.0.0-alpha.256
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 +26 -0
- package/package.json +5 -5
- package/src/Capabilities_Fake.res +34 -1
- package/src/Capabilities_Fake.res.mjs +59 -2
- package/src/Flow_GWT.res +2 -2
- package/src/Flow_GWT.res.mjs +2 -2
- package/src/InboundTranslation_GWT.res +69 -21
- package/src/InboundTranslation_GWT.res.mjs +101 -46
- package/src/OutboundTranslation_GWT.res.mjs +1 -1
- package/src/ScenarioRecorder.res +10 -1
- package/src/ScenarioRecorder.res.mjs +17 -1
- package/src/StubRuntime.res +0 -2
- package/src/StubRuntime.res.mjs +0 -2
- package/tests/InboundFromSliceGwtTest.res +83 -2
- package/tests/InboundFromSliceGwtTest.res.mjs +149 -24
- package/tests/OutboundFromSliceGwtTest.res.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,32 @@
|
|
|
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.256 (2026-10-25)
|
|
7
|
+
|
|
8
|
+
* refactor(spec)!: the plugin structure's optional parts are optional fields ([130bad5](https://github.com/ReventlessDev/reventless-core/commit/130bad581caf725a9d61b5a3df3d012e4f8ba402))
|
|
9
|
+
* feat!: an inbound translation is async and handed the platform's capabilities, and its audit row withholds sensitive fields ([dae6049](https://github.com/ReventlessDev/reventless-core/commit/dae6049e084f81408a494a2cd2011ec564dda65c))
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* a stash capability holds a value for minutes outside the log and hands it over once ([627f285](https://github.com/ReventlessDev/reventless-core/commit/627f28529ead2d2d31bdd091c9f42e7faca40f6f))
|
|
13
|
+
|
|
14
|
+
### BREAKING CHANGES
|
|
15
|
+
|
|
16
|
+
* code that builds these records drops `name: None`, writes
|
|
17
|
+
`name: x` for `name: Some(x)`, and `name: ?x` for a value already of type
|
|
18
|
+
option.
|
|
19
|
+
* an inbound translation's translate takes ~capabilities and
|
|
20
|
+
returns a promise, and its spec must declare capabilityNeeds ([] for one that
|
|
21
|
+
only maps input). FromSlice steps return promises.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# 1.0.0-alpha.255 (2026-10-23)
|
|
26
|
+
|
|
27
|
+
### Bug Fixes
|
|
28
|
+
|
|
29
|
+
* **gwt:** a recorded sidecar is written only when it says something new ([a8f3cb9](https://github.com/ReventlessDev/reventless-core/commit/a8f3cb926d779f6a0c763196f73da5df102ca31e))
|
|
30
|
+
|
|
31
|
+
|
|
6
32
|
# 1.0.0-alpha.254 (2026-10-22)
|
|
7
33
|
|
|
8
34
|
**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.256",
|
|
4
4
|
"description": "Given-When-Then DSLs and test harness for Reventless slice testing",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"sury": "11.0.0",
|
|
17
|
+
"@reventlessdev/rescript-effect": "1.0.0-alpha.1",
|
|
17
18
|
"@reventlessdev/rescript-node": "2.0.0-alpha.23",
|
|
18
19
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.13",
|
|
19
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.
|
|
20
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.
|
|
21
|
-
"@reventlessdev/
|
|
22
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.175"
|
|
20
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.308",
|
|
21
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.205",
|
|
22
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.176"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"rescript": "12.3.1",
|
|
@@ -7,6 +7,7 @@ module Geocoding = Reventless.Geocoding
|
|
|
7
7
|
module Messaging = Reventless.Messaging
|
|
8
8
|
module IdentityProvider = Reventless.IdentityProvider
|
|
9
9
|
module Secrets = Reventless.Secrets
|
|
10
|
+
module Stash = Reventless.Stash
|
|
10
11
|
|
|
11
12
|
/** One call a slice made through its capabilities. */
|
|
12
13
|
type call =
|
|
@@ -22,6 +23,10 @@ type call =
|
|
|
22
23
|
| PrincipalDeleted({principal: IdentityProvider.principal})
|
|
23
24
|
| TokenDrawn({length: int, alphabet: Secrets.alphabet})
|
|
24
25
|
| Hashed({input: string})
|
|
26
|
+
/** A value put in the stash. The value itself is recorded, so a test can
|
|
27
|
+
assert which secret was handed over — a fake has no log to keep it out of. */
|
|
28
|
+
| Stashed({key: string, value: string, ttl: Reventless.Duration.t})
|
|
29
|
+
| Taken({key: string})
|
|
25
30
|
|
|
26
31
|
type t = {
|
|
27
32
|
capabilities: Capabilities.t,
|
|
@@ -34,7 +39,9 @@ Fakes whose answers are given per capability. An answer left out is a plain
|
|
|
34
39
|
success: a geocoder with no candidates, a send with a numbered receipt, a
|
|
35
40
|
principal with a numbered id, `token` (or `t` repeated to length), and a hash
|
|
36
41
|
that prefixes `"hash:"`. `~secrets` replaces the secret source whole, e.g. with
|
|
37
|
-
`Secrets.unavailable`; its calls are still recorded.
|
|
42
|
+
`Secrets.unavailable`; its calls are still recorded. The stash holds what is put
|
|
43
|
+
in it and hands each value over once, starting from `~stashed`; `~stash`
|
|
44
|
+
replaces it whole, e.g. with `Stash.unavailable`.
|
|
38
45
|
*/
|
|
39
46
|
let make = (
|
|
40
47
|
~geocode: option<Geocoding.search>=?,
|
|
@@ -51,6 +58,8 @@ let make = (
|
|
|
51
58
|
~token: option<string>=?,
|
|
52
59
|
~hash: option<string => string>=?,
|
|
53
60
|
~secrets: option<Secrets.t>=?,
|
|
61
|
+
~stashed: array<(string, string)>=[],
|
|
62
|
+
~stash: option<Stash.t>=?,
|
|
54
63
|
(),
|
|
55
64
|
): t => {
|
|
56
65
|
let log: array<call> = []
|
|
@@ -76,6 +85,7 @@ let make = (
|
|
|
76
85
|
| None => Ok({IdentityProvider.providerId: `principal-${Int.toString(log->Array.length)}`})
|
|
77
86
|
}
|
|
78
87
|
}
|
|
88
|
+
let held = Dict.fromArray(stashed)
|
|
79
89
|
let capabilities: Capabilities.t = {
|
|
80
90
|
geocode,
|
|
81
91
|
messaging: Messaging.makeProvider(~emailAndSms=channels, ~pushServices, ~send),
|
|
@@ -111,6 +121,27 @@ let make = (
|
|
|
111
121
|
}
|
|
112
122
|
},
|
|
113
123
|
},
|
|
124
|
+
stash: {
|
|
125
|
+
put: async (~key, ~value, ~ttl) => {
|
|
126
|
+
record(Stashed({key, value, ttl}))
|
|
127
|
+
switch stash {
|
|
128
|
+
| Some(s) => await s.put(~key, ~value, ~ttl)
|
|
129
|
+
| None =>
|
|
130
|
+
held->Dict.set(key, value)
|
|
131
|
+
Ok()
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
take: async (~key) => {
|
|
135
|
+
record(Taken({key: key}))
|
|
136
|
+
switch stash {
|
|
137
|
+
| Some(s) => await s.take(~key)
|
|
138
|
+
| None =>
|
|
139
|
+
let found = held->Dict.get(key)
|
|
140
|
+
held->Dict.delete(key)
|
|
141
|
+
Ok(found)
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
},
|
|
114
145
|
}
|
|
115
146
|
{capabilities, calls: () => log->Array.copy}
|
|
116
147
|
}
|
|
@@ -153,5 +184,7 @@ let toJson = (c: call): JSON.t => {
|
|
|
153
184
|
obj("PrincipalDeleted", [("principal", str(principal.providerId))])
|
|
154
185
|
| TokenDrawn({length, _}) => obj("TokenDrawn", [("length", JSON.Encode.int(length))])
|
|
155
186
|
| Hashed({input}) => obj("Hashed", [("input", str(input))])
|
|
187
|
+
| Stashed({key, ttl, _}) => obj("Stashed", [("key", str(key)), ("ttl", JSON.Encode.int(ttl))])
|
|
188
|
+
| Taken({key}) => obj("Taken", [("key", str(key))])
|
|
156
189
|
}
|
|
157
190
|
}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Belt_Array from "@rescript/runtime/lib/es6/Belt_Array.js";
|
|
4
|
+
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
4
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
6
|
import * as Messaging$Reventless from "@reventlessdev/reventless-spec/src/semantic/Messaging.res.mjs";
|
|
6
7
|
import * as IdentityProvider$Reventless from "@reventlessdev/reventless-spec/src/semantic/IdentityProvider.res.mjs";
|
|
7
8
|
|
|
8
|
-
function make(geocode, send, channelsOpt, pushServicesOpt, createPrincipal, token, hash, secrets, param) {
|
|
9
|
+
function make(geocode, send, channelsOpt, pushServicesOpt, createPrincipal, token, hash, secrets, stashedOpt, stash, param) {
|
|
9
10
|
let channels = channelsOpt !== undefined ? channelsOpt : [
|
|
10
11
|
"Email",
|
|
11
12
|
"Sms"
|
|
12
13
|
];
|
|
13
14
|
let pushServices = pushServicesOpt !== undefined ? pushServicesOpt : [];
|
|
15
|
+
let stashed = stashedOpt !== undefined ? stashedOpt : [];
|
|
14
16
|
let log = [];
|
|
15
17
|
let geocode$1 = async text => {
|
|
16
18
|
log.push({
|
|
@@ -61,6 +63,7 @@ function make(geocode, send, channelsOpt, pushServicesOpt, createPrincipal, toke
|
|
|
61
63
|
};
|
|
62
64
|
}
|
|
63
65
|
};
|
|
66
|
+
let held = Object.fromEntries(stashed);
|
|
64
67
|
let capabilities_messaging = Messaging$Reventless.makeProvider(channels, pushServices, send$1);
|
|
65
68
|
let capabilities_identityProvider = IdentityProvider$Reventless.make(createPrincipal$1, async (principal, group) => {
|
|
66
69
|
log.push({
|
|
@@ -128,11 +131,46 @@ function make(geocode, send, channelsOpt, pushServicesOpt, createPrincipal, toke
|
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
133
|
};
|
|
134
|
+
let capabilities_stash = {
|
|
135
|
+
put: async (key, value, ttl) => {
|
|
136
|
+
log.push({
|
|
137
|
+
TAG: "Stashed",
|
|
138
|
+
key: key,
|
|
139
|
+
value: value,
|
|
140
|
+
ttl: ttl
|
|
141
|
+
});
|
|
142
|
+
if (stash !== undefined) {
|
|
143
|
+
return await stash.put(key, value, ttl);
|
|
144
|
+
} else {
|
|
145
|
+
held[key] = value;
|
|
146
|
+
return {
|
|
147
|
+
TAG: "Ok",
|
|
148
|
+
_0: undefined
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
take: async key => {
|
|
153
|
+
log.push({
|
|
154
|
+
TAG: "Taken",
|
|
155
|
+
key: key
|
|
156
|
+
});
|
|
157
|
+
if (stash !== undefined) {
|
|
158
|
+
return await stash.take(key);
|
|
159
|
+
}
|
|
160
|
+
let found = held[key];
|
|
161
|
+
Stdlib_Dict.$$delete(held, key);
|
|
162
|
+
return {
|
|
163
|
+
TAG: "Ok",
|
|
164
|
+
_0: found
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
};
|
|
131
168
|
let capabilities = {
|
|
132
169
|
geocode: geocode$1,
|
|
133
170
|
messaging: capabilities_messaging,
|
|
134
171
|
identityProvider: capabilities_identityProvider,
|
|
135
|
-
secrets: capabilities_secrets
|
|
172
|
+
secrets: capabilities_secrets,
|
|
173
|
+
stash: capabilities_stash
|
|
136
174
|
};
|
|
137
175
|
return {
|
|
138
176
|
capabilities: capabilities,
|
|
@@ -229,6 +267,22 @@ function toJson(c) {
|
|
|
229
267
|
"input",
|
|
230
268
|
c.input
|
|
231
269
|
]]);
|
|
270
|
+
case "Stashed" :
|
|
271
|
+
return obj("Stashed", [
|
|
272
|
+
[
|
|
273
|
+
"key",
|
|
274
|
+
c.key
|
|
275
|
+
],
|
|
276
|
+
[
|
|
277
|
+
"ttl",
|
|
278
|
+
c.ttl
|
|
279
|
+
]
|
|
280
|
+
]);
|
|
281
|
+
case "Taken" :
|
|
282
|
+
return obj("Taken", [[
|
|
283
|
+
"key",
|
|
284
|
+
c.key
|
|
285
|
+
]]);
|
|
232
286
|
}
|
|
233
287
|
}
|
|
234
288
|
|
|
@@ -242,12 +296,15 @@ let IdentityProvider;
|
|
|
242
296
|
|
|
243
297
|
let Secrets;
|
|
244
298
|
|
|
299
|
+
let Stash;
|
|
300
|
+
|
|
245
301
|
export {
|
|
246
302
|
Capabilities,
|
|
247
303
|
Geocoding,
|
|
248
304
|
Messaging,
|
|
249
305
|
IdentityProvider,
|
|
250
306
|
Secrets,
|
|
307
|
+
Stash,
|
|
251
308
|
make,
|
|
252
309
|
toJson,
|
|
253
310
|
}
|
package/src/Flow_GWT.res
CHANGED
|
@@ -680,9 +680,9 @@ module InboundSlice = (
|
|
|
680
680
|
module I = InboundTranslation_GWT.FromSlice(Spec, Translation)
|
|
681
681
|
|
|
682
682
|
// Received input becomes the flow's next commands, as an automation's do.
|
|
683
|
-
let whenReceived = async (flowP: flow, json: JSON.t) => {
|
|
683
|
+
let whenReceived = async (flowP: flow, json: JSON.t, ~fakes=?) => {
|
|
684
684
|
let s = await flowP
|
|
685
|
-
switch I.whenReceived(json) {
|
|
685
|
+
switch (await I.whenReceived(json, ~fakes?)).outcome {
|
|
686
686
|
| Translated(pairs) => {
|
|
687
687
|
...s,
|
|
688
688
|
lastCommands: pairs->Array.map(((_id, cmd)) => cmd->Message.encode(Spec.commandSchema)),
|
package/src/Flow_GWT.res.mjs
CHANGED
|
@@ -582,9 +582,9 @@ function OutboundSlice(Spec) {
|
|
|
582
582
|
function InboundSlice(Spec) {
|
|
583
583
|
return Translation => {
|
|
584
584
|
let I = InboundTranslation_GWT$ReventlessGwt.FromSlice(Spec)(Translation);
|
|
585
|
-
let whenReceived = async (flowP, json) => {
|
|
585
|
+
let whenReceived = async (flowP, json, fakes) => {
|
|
586
586
|
let s = await flowP;
|
|
587
|
-
let pairs = I.whenReceived(json);
|
|
587
|
+
let pairs = (await I.whenReceived(json, fakes, undefined)).outcome;
|
|
588
588
|
switch (pairs.TAG) {
|
|
589
589
|
case "Translated" :
|
|
590
590
|
return {
|
|
@@ -77,21 +77,29 @@ module Make = (Spec: SliceSpec): (T with module Spec = Spec) => {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
// The slice as written: its `Spec` and its `_Translation` body
|
|
80
|
+
// The slice as written: its `Spec` and its `_Translation` body, run against
|
|
81
|
+
// recording capability fakes as the outbound DSL's `FromSlice` is.
|
|
81
82
|
module FromSlice = (
|
|
82
83
|
Spec: Reventless.InboundTranslationSlice.Spec,
|
|
83
84
|
Translation: Reventless.InboundTranslationSlice.Translation with module Spec := Spec,
|
|
84
85
|
) => {
|
|
85
86
|
let describe = JestBind.describe
|
|
86
|
-
let test = (name, body) =>
|
|
87
|
+
let test = (name, ~timeout=?, body) =>
|
|
88
|
+
JestBind.testPromise(~slice=Spec.name, name, ~timeout?, body)
|
|
87
89
|
|
|
88
90
|
/** What became of one input. `InputRefused` is input that did not decode, so
|
|
89
91
|
`translate` never saw it; `NotUnderstood` is input it declined. */
|
|
90
|
-
type
|
|
92
|
+
type outcome =
|
|
91
93
|
| Translated(array<(string, Spec.command)>)
|
|
92
94
|
| NotUnderstood(string)
|
|
93
95
|
| InputRefused(string)
|
|
94
96
|
|
|
97
|
+
/** An outcome, and the capability calls `translate` made reaching it. */
|
|
98
|
+
type received = {outcome: outcome, sent: array<Capabilities_Fake.call>}
|
|
99
|
+
|
|
100
|
+
/** Scripted capabilities for `whenInput(~fakes)`; see `Capabilities_Fake.make`. */
|
|
101
|
+
let fakes = Capabilities_Fake.make
|
|
102
|
+
|
|
95
103
|
let encPairs = (pairs: array<(string, Spec.command)>) =>
|
|
96
104
|
pairs->Array.map(((id, cmd)) => {
|
|
97
105
|
let d = Dict.make()
|
|
@@ -100,23 +108,49 @@ module FromSlice = (
|
|
|
100
108
|
JSON.Encode.object(d)
|
|
101
109
|
})
|
|
102
110
|
|
|
103
|
-
let
|
|
104
|
-
switch
|
|
111
|
+
let errorOf = exn =>
|
|
112
|
+
switch exn {
|
|
113
|
+
| S.Exn(e) => e.message
|
|
114
|
+
| JsExn(e) => JsExn.message(e)->Option.getOr("unknown error")
|
|
115
|
+
| _ => "unknown error"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** One input through the slice's own `translate`. A throw is input the slice
|
|
119
|
+
declined, as the runtime records it. `~caller` is who called the door, as
|
|
120
|
+
the transport would hand it on; left out, the platform is acting for
|
|
121
|
+
itself. */
|
|
122
|
+
let whenInput = async (
|
|
123
|
+
input: Spec.externalInput,
|
|
124
|
+
~fakes=Capabilities_Fake.make(),
|
|
125
|
+
~caller: option<Reventless.Identity.t>=?,
|
|
126
|
+
) => {
|
|
127
|
+
let outcome = switch await Translation.translate(
|
|
128
|
+
input,
|
|
129
|
+
~capabilities=fakes.capabilities,
|
|
130
|
+
~caller,
|
|
131
|
+
) {
|
|
105
132
|
| Ok(pairs) => Translated(pairs)
|
|
106
133
|
| Error(msg) => NotUnderstood(msg)
|
|
134
|
+
| exception exn => NotUnderstood(errorOf(exn))
|
|
107
135
|
}
|
|
136
|
+
{outcome, sent: fakes.calls()}
|
|
137
|
+
}
|
|
108
138
|
|
|
109
139
|
/** Input as it arrives: decoded through `externalInputSchema` first. */
|
|
110
|
-
let whenReceived = (
|
|
140
|
+
let whenReceived = async (
|
|
141
|
+
json: JSON.t,
|
|
142
|
+
~fakes=Capabilities_Fake.make(),
|
|
143
|
+
~caller: option<Reventless.Identity.t>=?,
|
|
144
|
+
) =>
|
|
111
145
|
switch json->Reventless.Util_Sury.fromJson(Spec.externalInputSchema) {
|
|
112
|
-
| input => whenInput(input)
|
|
146
|
+
| input => await whenInput(input, ~fakes, ~caller?)
|
|
113
147
|
// Sury's message names the field that failed.
|
|
114
|
-
| exception S.Exn(e) => InputRefused(e.message)
|
|
115
|
-
| exception _ => InputRefused("invalid input")
|
|
148
|
+
| exception S.Exn(e) => {outcome: InputRefused(e.message), sent: fakes.calls()}
|
|
149
|
+
| exception _ => {outcome: InputRefused("invalid input"), sent: fakes.calls()}
|
|
116
150
|
}
|
|
117
151
|
|
|
118
|
-
let describeOther =
|
|
119
|
-
switch
|
|
152
|
+
let describeOther = outcome =>
|
|
153
|
+
switch outcome {
|
|
120
154
|
| Translated(_) => None
|
|
121
155
|
| NotUnderstood(msg) => Some(`not understood: ${msg}`)
|
|
122
156
|
| InputRefused(msg) => Some(`input refused: ${msg}`)
|
|
@@ -131,8 +165,8 @@ module FromSlice = (
|
|
|
131
165
|
| exception _ => false
|
|
132
166
|
}
|
|
133
167
|
|
|
134
|
-
let thenCommands = (received
|
|
135
|
-
switch
|
|
168
|
+
let thenCommands = async (pending: promise<received>, expected) =>
|
|
169
|
+
switch (await pending).outcome {
|
|
136
170
|
| Translated(actual) if actual == expected =>
|
|
137
171
|
switch actual->Array.find(((_, cmd)) => !roundTrips(cmd)) {
|
|
138
172
|
| None => Outcome.pass
|
|
@@ -149,19 +183,19 @@ module FromSlice = (
|
|
|
149
183
|
| other => Outcome.fail(TranslateError({expected: "(commands)", actual: describeOther(other)}))
|
|
150
184
|
}
|
|
151
185
|
|
|
152
|
-
let thenCommand = (
|
|
153
|
-
thenCommands(
|
|
186
|
+
let thenCommand = (pending, expectedId, expectedCmd) =>
|
|
187
|
+
thenCommands(pending, [(expectedId, expectedCmd)])
|
|
154
188
|
|
|
155
|
-
let thenNoCommand = received =>
|
|
156
|
-
switch
|
|
189
|
+
let thenNoCommand = async (pending: promise<received>) =>
|
|
190
|
+
switch (await pending).outcome {
|
|
157
191
|
| Translated([]) => Outcome.pass
|
|
158
192
|
| Translated(actual) => Outcome.fail(NoEventExpected({actual: encPairs(actual)}))
|
|
159
193
|
| other =>
|
|
160
194
|
Outcome.fail(TranslateError({expected: "(no commands)", actual: describeOther(other)}))
|
|
161
195
|
}
|
|
162
196
|
|
|
163
|
-
let thenNotUnderstood = (received
|
|
164
|
-
switch
|
|
197
|
+
let thenNotUnderstood = async (pending: promise<received>, expectedMsg) =>
|
|
198
|
+
switch (await pending).outcome {
|
|
165
199
|
| NotUnderstood(actual) if actual == expectedMsg => Outcome.pass
|
|
166
200
|
| NotUnderstood(actual) =>
|
|
167
201
|
Outcome.fail(TranslateError({expected: expectedMsg, actual: Some(actual)}))
|
|
@@ -171,8 +205,8 @@ module FromSlice = (
|
|
|
171
205
|
|
|
172
206
|
/** Passes when the input was refused and the reason contains `reason`: the
|
|
173
207
|
decoder words its own messages, so a test pins the part that matters. */
|
|
174
|
-
let thenRefusedInput = (received
|
|
175
|
-
switch
|
|
208
|
+
let thenRefusedInput = async (pending: promise<received>, reason) =>
|
|
209
|
+
switch (await pending).outcome {
|
|
176
210
|
| InputRefused(actual) if actual->String.includes(reason) => Outcome.pass
|
|
177
211
|
| other =>
|
|
178
212
|
Outcome.fail(
|
|
@@ -185,4 +219,18 @@ module FromSlice = (
|
|
|
185
219
|
}),
|
|
186
220
|
)
|
|
187
221
|
}
|
|
222
|
+
|
|
223
|
+
/** The capability calls `translate` made, in order. */
|
|
224
|
+
let thenSent = async (pending: promise<received>, expected: array<Capabilities_Fake.call>) => {
|
|
225
|
+
let {sent, _} = await pending
|
|
226
|
+
sent == expected
|
|
227
|
+
? Outcome.pass
|
|
228
|
+
: Outcome.fail(
|
|
229
|
+
EventsMismatch({
|
|
230
|
+
expected: expected->Array.map(Capabilities_Fake.toJson),
|
|
231
|
+
actual: sent->Array.map(Capabilities_Fake.toJson),
|
|
232
|
+
}),
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
let thenNothingSent = pending => thenSent(pending, [])
|
|
188
236
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
4
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
6
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
6
7
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
@@ -8,6 +9,7 @@ import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/U
|
|
|
8
9
|
import * as Outcome$ReventlessGwt from "./Outcome.res.mjs";
|
|
9
10
|
import * as JestBind$ReventlessGwt from "./JestBind.res.mjs";
|
|
10
11
|
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
12
|
+
import * as Capabilities_Fake$ReventlessGwt from "./Capabilities_Fake.res.mjs";
|
|
11
13
|
|
|
12
14
|
function Make(Spec) {
|
|
13
15
|
let test = (name, body) => JestBind$ReventlessGwt.test(Spec.name, name, body);
|
|
@@ -92,28 +94,53 @@ function Make(Spec) {
|
|
|
92
94
|
|
|
93
95
|
function FromSlice(Spec) {
|
|
94
96
|
return Translation => {
|
|
95
|
-
let test = (name, body) => JestBind$ReventlessGwt.
|
|
97
|
+
let test = (name, timeout, body) => JestBind$ReventlessGwt.testPromise(Spec.name, name, timeout, body);
|
|
96
98
|
let encPairs = pairs => pairs.map(param => {
|
|
97
99
|
let d = {};
|
|
98
100
|
d["id"] = param[0];
|
|
99
101
|
d["command"] = Message$ReventlessCore.encode(param[1], Spec.commandSchema);
|
|
100
102
|
return d;
|
|
101
103
|
});
|
|
102
|
-
let
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
_0: pairs._0
|
|
108
|
-
};
|
|
104
|
+
let errorOf = exn => {
|
|
105
|
+
if (exn.RE_EXN_ID === S.Exn) {
|
|
106
|
+
return exn._1.message;
|
|
107
|
+
} else if (exn.RE_EXN_ID === "JsExn") {
|
|
108
|
+
return Stdlib_Option.getOr(Stdlib_JsExn.message(exn._1), "unknown error");
|
|
109
109
|
} else {
|
|
110
|
-
return
|
|
110
|
+
return "unknown error";
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
let whenInput = async (input, fakesOpt, caller) => {
|
|
114
|
+
let fakes = fakesOpt !== undefined ? fakesOpt : Capabilities_Fake$ReventlessGwt.make(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
115
|
+
let outcome;
|
|
116
|
+
let exit = 0;
|
|
117
|
+
let pairs;
|
|
118
|
+
try {
|
|
119
|
+
pairs = await Translation.translate(input, fakes.capabilities, caller);
|
|
120
|
+
exit = 1;
|
|
121
|
+
} catch (raw_exn) {
|
|
122
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
123
|
+
outcome = {
|
|
111
124
|
TAG: "NotUnderstood",
|
|
112
|
-
_0:
|
|
125
|
+
_0: errorOf(exn)
|
|
113
126
|
};
|
|
114
127
|
}
|
|
128
|
+
if (exit === 1) {
|
|
129
|
+
outcome = pairs.TAG === "Ok" ? ({
|
|
130
|
+
TAG: "Translated",
|
|
131
|
+
_0: pairs._0
|
|
132
|
+
}) : ({
|
|
133
|
+
TAG: "NotUnderstood",
|
|
134
|
+
_0: pairs._0
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
outcome: outcome,
|
|
139
|
+
sent: fakes.calls()
|
|
140
|
+
};
|
|
115
141
|
};
|
|
116
|
-
let whenReceived = json => {
|
|
142
|
+
let whenReceived = async (json, fakesOpt, caller) => {
|
|
143
|
+
let fakes = fakesOpt !== undefined ? fakesOpt : Capabilities_Fake$ReventlessGwt.make(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
117
144
|
let input;
|
|
118
145
|
try {
|
|
119
146
|
input = Util_Sury$Reventless.fromJson(json, Spec.externalInputSchema);
|
|
@@ -121,26 +148,32 @@ function FromSlice(Spec) {
|
|
|
121
148
|
let e = Primitive_exceptions.internalToException(raw_e);
|
|
122
149
|
if (e.RE_EXN_ID === S.Exn) {
|
|
123
150
|
return {
|
|
124
|
-
|
|
125
|
-
|
|
151
|
+
outcome: {
|
|
152
|
+
TAG: "InputRefused",
|
|
153
|
+
_0: e._1.message
|
|
154
|
+
},
|
|
155
|
+
sent: fakes.calls()
|
|
126
156
|
};
|
|
127
157
|
} else {
|
|
128
158
|
return {
|
|
129
|
-
|
|
130
|
-
|
|
159
|
+
outcome: {
|
|
160
|
+
TAG: "InputRefused",
|
|
161
|
+
_0: "invalid input"
|
|
162
|
+
},
|
|
163
|
+
sent: fakes.calls()
|
|
131
164
|
};
|
|
132
165
|
}
|
|
133
166
|
}
|
|
134
|
-
return whenInput(input);
|
|
167
|
+
return await whenInput(input, fakes, caller);
|
|
135
168
|
};
|
|
136
|
-
let describeOther =
|
|
137
|
-
switch (
|
|
169
|
+
let describeOther = outcome => {
|
|
170
|
+
switch (outcome.TAG) {
|
|
138
171
|
case "Translated" :
|
|
139
172
|
return;
|
|
140
173
|
case "NotUnderstood" :
|
|
141
|
-
return `not understood: ` +
|
|
174
|
+
return `not understood: ` + outcome._0;
|
|
142
175
|
case "InputRefused" :
|
|
143
|
-
return `input refused: ` +
|
|
176
|
+
return `input refused: ` + outcome._0;
|
|
144
177
|
}
|
|
145
178
|
};
|
|
146
179
|
let roundTrips = cmd => {
|
|
@@ -152,18 +185,19 @@ function FromSlice(Spec) {
|
|
|
152
185
|
}
|
|
153
186
|
return Primitive_object.equal(back, cmd);
|
|
154
187
|
};
|
|
155
|
-
let thenCommands = (
|
|
156
|
-
|
|
188
|
+
let thenCommands = async (pending, expected) => {
|
|
189
|
+
let actual = (await pending).outcome;
|
|
190
|
+
switch (actual.TAG) {
|
|
157
191
|
case "Translated" :
|
|
158
|
-
let actual =
|
|
159
|
-
if (!Primitive_object.equal(actual, expected)) {
|
|
192
|
+
let actual$1 = actual._0;
|
|
193
|
+
if (!Primitive_object.equal(actual$1, expected)) {
|
|
160
194
|
return Outcome$ReventlessGwt.fail({
|
|
161
195
|
TAG: "EventsMismatch",
|
|
162
196
|
expected: encPairs(expected),
|
|
163
|
-
actual: encPairs(actual)
|
|
197
|
+
actual: encPairs(actual$1)
|
|
164
198
|
});
|
|
165
199
|
}
|
|
166
|
-
let pair = actual.find(param => !roundTrips(param[1]));
|
|
200
|
+
let pair = actual$1.find(param => !roundTrips(param[1]));
|
|
167
201
|
if (pair !== undefined) {
|
|
168
202
|
return Outcome$ReventlessGwt.fail({
|
|
169
203
|
TAG: "TranslateError",
|
|
@@ -178,22 +212,23 @@ function FromSlice(Spec) {
|
|
|
178
212
|
return Outcome$ReventlessGwt.fail({
|
|
179
213
|
TAG: "TranslateError",
|
|
180
214
|
expected: "(commands)",
|
|
181
|
-
actual: describeOther(
|
|
215
|
+
actual: describeOther(actual)
|
|
182
216
|
});
|
|
183
217
|
}
|
|
184
218
|
};
|
|
185
|
-
let thenCommand = (
|
|
219
|
+
let thenCommand = (pending, expectedId, expectedCmd) => thenCommands(pending, [[
|
|
186
220
|
expectedId,
|
|
187
221
|
expectedCmd
|
|
188
222
|
]]);
|
|
189
|
-
let thenNoCommand =
|
|
190
|
-
|
|
223
|
+
let thenNoCommand = async pending => {
|
|
224
|
+
let actual = (await pending).outcome;
|
|
225
|
+
switch (actual.TAG) {
|
|
191
226
|
case "Translated" :
|
|
192
|
-
let actual =
|
|
193
|
-
if (actual.length !== 0) {
|
|
227
|
+
let actual$1 = actual._0;
|
|
228
|
+
if (actual$1.length !== 0) {
|
|
194
229
|
return Outcome$ReventlessGwt.fail({
|
|
195
230
|
TAG: "NoEventExpected",
|
|
196
|
-
actual: encPairs(actual)
|
|
231
|
+
actual: encPairs(actual$1)
|
|
197
232
|
});
|
|
198
233
|
} else {
|
|
199
234
|
return Outcome$ReventlessGwt.pass;
|
|
@@ -203,21 +238,22 @@ function FromSlice(Spec) {
|
|
|
203
238
|
return Outcome$ReventlessGwt.fail({
|
|
204
239
|
TAG: "TranslateError",
|
|
205
240
|
expected: "(no commands)",
|
|
206
|
-
actual: describeOther(
|
|
241
|
+
actual: describeOther(actual)
|
|
207
242
|
});
|
|
208
243
|
}
|
|
209
244
|
};
|
|
210
|
-
let thenNotUnderstood = (
|
|
211
|
-
|
|
245
|
+
let thenNotUnderstood = async (pending, expectedMsg) => {
|
|
246
|
+
let actual = (await pending).outcome;
|
|
247
|
+
switch (actual.TAG) {
|
|
212
248
|
case "NotUnderstood" :
|
|
213
|
-
let actual =
|
|
214
|
-
if (actual === expectedMsg) {
|
|
249
|
+
let actual$1 = actual._0;
|
|
250
|
+
if (actual$1 === expectedMsg) {
|
|
215
251
|
return Outcome$ReventlessGwt.pass;
|
|
216
252
|
} else {
|
|
217
253
|
return Outcome$ReventlessGwt.fail({
|
|
218
254
|
TAG: "TranslateError",
|
|
219
255
|
expected: expectedMsg,
|
|
220
|
-
actual: actual
|
|
256
|
+
actual: actual$1
|
|
221
257
|
});
|
|
222
258
|
}
|
|
223
259
|
case "Translated" :
|
|
@@ -225,29 +261,30 @@ function FromSlice(Spec) {
|
|
|
225
261
|
return Outcome$ReventlessGwt.fail({
|
|
226
262
|
TAG: "TranslateError",
|
|
227
263
|
expected: expectedMsg,
|
|
228
|
-
actual: describeOther(
|
|
264
|
+
actual: describeOther(actual)
|
|
229
265
|
});
|
|
230
266
|
}
|
|
231
267
|
};
|
|
232
|
-
let thenRefusedInput = (
|
|
233
|
-
|
|
268
|
+
let thenRefusedInput = async (pending, reason) => {
|
|
269
|
+
let actual = (await pending).outcome;
|
|
270
|
+
switch (actual.TAG) {
|
|
234
271
|
case "Translated" :
|
|
235
272
|
case "NotUnderstood" :
|
|
236
273
|
break;
|
|
237
274
|
case "InputRefused" :
|
|
238
|
-
if (
|
|
275
|
+
if (actual._0.includes(reason)) {
|
|
239
276
|
return Outcome$ReventlessGwt.pass;
|
|
240
277
|
}
|
|
241
278
|
break;
|
|
242
279
|
}
|
|
243
280
|
let tmp;
|
|
244
|
-
switch (
|
|
281
|
+
switch (actual.TAG) {
|
|
245
282
|
case "Translated" :
|
|
246
283
|
tmp = "translated";
|
|
247
284
|
break;
|
|
248
285
|
case "NotUnderstood" :
|
|
249
286
|
case "InputRefused" :
|
|
250
|
-
tmp = describeOther(
|
|
287
|
+
tmp = describeOther(actual);
|
|
251
288
|
break;
|
|
252
289
|
}
|
|
253
290
|
return Outcome$ReventlessGwt.fail({
|
|
@@ -256,10 +293,26 @@ function FromSlice(Spec) {
|
|
|
256
293
|
actual: tmp
|
|
257
294
|
});
|
|
258
295
|
};
|
|
296
|
+
let thenSent = async (pending, expected) => {
|
|
297
|
+
let match = await pending;
|
|
298
|
+
let sent = match.sent;
|
|
299
|
+
if (Primitive_object.equal(sent, expected)) {
|
|
300
|
+
return Outcome$ReventlessGwt.pass;
|
|
301
|
+
} else {
|
|
302
|
+
return Outcome$ReventlessGwt.fail({
|
|
303
|
+
TAG: "EventsMismatch",
|
|
304
|
+
expected: expected.map(Capabilities_Fake$ReventlessGwt.toJson),
|
|
305
|
+
actual: sent.map(Capabilities_Fake$ReventlessGwt.toJson)
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
let thenNothingSent = pending => thenSent(pending, []);
|
|
259
310
|
return {
|
|
260
311
|
describe: JestBind$ReventlessGwt.describe,
|
|
261
312
|
test: test,
|
|
313
|
+
fakes: Capabilities_Fake$ReventlessGwt.make,
|
|
262
314
|
encPairs: encPairs,
|
|
315
|
+
errorOf: errorOf,
|
|
263
316
|
whenInput: whenInput,
|
|
264
317
|
whenReceived: whenReceived,
|
|
265
318
|
describeOther: describeOther,
|
|
@@ -269,7 +322,9 @@ function FromSlice(Spec) {
|
|
|
269
322
|
thenNoCommand: thenNoCommand,
|
|
270
323
|
thenNotUnderstood: thenNotUnderstood,
|
|
271
324
|
thenTranslateError: thenNotUnderstood,
|
|
272
|
-
thenRefusedInput: thenRefusedInput
|
|
325
|
+
thenRefusedInput: thenRefusedInput,
|
|
326
|
+
thenSent: thenSent,
|
|
327
|
+
thenNothingSent: thenNothingSent
|
|
273
328
|
};
|
|
274
329
|
};
|
|
275
330
|
}
|
|
@@ -229,7 +229,7 @@ function FromSlice(Spec) {
|
|
|
229
229
|
let givenTodo = (id, item) => ({
|
|
230
230
|
id: id,
|
|
231
231
|
item: item,
|
|
232
|
-
fakes: Capabilities_Fake$ReventlessGwt.make(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined)
|
|
232
|
+
fakes: Capabilities_Fake$ReventlessGwt.make(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined)
|
|
233
233
|
});
|
|
234
234
|
let givenCapabilities = (todo, fakes) => ({
|
|
235
235
|
id: todo.id,
|
package/src/ScenarioRecorder.res
CHANGED
|
@@ -120,7 +120,16 @@ let write = (~path: string, ~file: string, ~spec: string, scenarios: array<scena
|
|
|
120
120
|
),
|
|
121
121
|
]),
|
|
122
122
|
)
|
|
123
|
-
|
|
123
|
+
let text = JSON.stringifyAny(json, ~space=2)->Option.getOr("{}") ++ "\n"
|
|
124
|
+
// Written only when it says something new: a watch reacts to the write, and the same run
|
|
125
|
+
// records the same scenarios each time.
|
|
126
|
+
let same = switch NodeFs.readFileSync(path) {
|
|
127
|
+
| old => old == text
|
|
128
|
+
| exception _ => false
|
|
129
|
+
}
|
|
130
|
+
if !same {
|
|
131
|
+
NodeFs.writeFileSync(path, text)
|
|
132
|
+
}
|
|
124
133
|
}
|
|
125
134
|
|
|
126
135
|
/** Runs one test's `body`, recording its steps when recording is on, and adds the scenario to
|
|
@@ -161,7 +161,23 @@ function write(path, file, spec, scenarios) {
|
|
|
161
161
|
])))
|
|
162
162
|
]
|
|
163
163
|
]);
|
|
164
|
-
|
|
164
|
+
let text = Stdlib_Option.getOr(JSON.stringify(json, undefined, 2), "{}") + "\n";
|
|
165
|
+
let same;
|
|
166
|
+
let exit = 0;
|
|
167
|
+
let old;
|
|
168
|
+
try {
|
|
169
|
+
old = Nodefs.readFileSync(path, "utf8");
|
|
170
|
+
exit = 1;
|
|
171
|
+
} catch (exn) {
|
|
172
|
+
same = false;
|
|
173
|
+
}
|
|
174
|
+
if (exit === 1) {
|
|
175
|
+
same = old === text;
|
|
176
|
+
}
|
|
177
|
+
if (!same) {
|
|
178
|
+
Nodefs.writeFileSync(path, text, "utf8");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
165
181
|
}
|
|
166
182
|
|
|
167
183
|
function around(spec, title, body) {
|
package/src/StubRuntime.res
CHANGED
package/src/StubRuntime.res.mjs
CHANGED
|
@@ -16,14 +16,37 @@ module PaymentHook = {
|
|
|
16
16
|
let endpoint = None
|
|
17
17
|
type role = Reventless.Role.name
|
|
18
18
|
let authorizationOf = _ => Reventless.Authorization.AllowAuthenticated
|
|
19
|
+
let capabilityNeeds = [Reventless.CapabilityNeed.Messaging]
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
// Confirms a paid order and, through the capability `translate` is handed, tells
|
|
23
|
+
// the buyer — which is what makes the fakes worth asserting on. A payment marked
|
|
24
|
+
// paid by hand is confirmed only for a caller in Finance, which is what makes the
|
|
25
|
+
// caller worth handing in.
|
|
21
26
|
module PaymentHook_Translation = {
|
|
22
27
|
let moduleUrl = ""
|
|
23
|
-
let translate = (
|
|
28
|
+
let translate = async (
|
|
29
|
+
input: PaymentHook.externalInput,
|
|
30
|
+
~capabilities: Reventless.Capabilities.t,
|
|
31
|
+
~caller: option<Reventless.Identity.t>,
|
|
32
|
+
) =>
|
|
24
33
|
switch input.status {
|
|
25
|
-
| "
|
|
34
|
+
| "paidByHand" =>
|
|
35
|
+
switch caller {
|
|
36
|
+
| Some(identity) if identity->Reventless.Identity.hasGroup("Finance") =>
|
|
37
|
+
Ok([(input.orderId, PaymentHook.ConfirmPayment({orderId: input.orderId}))])
|
|
38
|
+
| _ => Error("only Finance marks a payment paid by hand")
|
|
39
|
+
}
|
|
40
|
+
| "paid" =>
|
|
41
|
+
switch await capabilities.messaging.send(
|
|
42
|
+
~recipient=ToEmail(Reventless.Email.unsafe("buyer@example.com")),
|
|
43
|
+
~message={subject: "Paid", body: input.orderId},
|
|
44
|
+
) {
|
|
45
|
+
| Ok(_) => Ok([(input.orderId, PaymentHook.ConfirmPayment({orderId: input.orderId}))])
|
|
46
|
+
| Error(failure) => Error(Reventless.Messaging.failureReason(failure))
|
|
47
|
+
}
|
|
26
48
|
| "pending" => Ok([])
|
|
49
|
+
| "broken" => JsError.throwWithMessage("the translation threw")
|
|
27
50
|
| other => Error(`Unknown status: ${other}`)
|
|
28
51
|
}
|
|
29
52
|
}
|
|
@@ -34,7 +57,41 @@ include InboundTranslation_GWT.FromSlice(PaymentHook, PaymentHook_Translation)
|
|
|
34
57
|
let json = (pairs: array<(string, string)>) =>
|
|
35
58
|
pairs->Array.map(((k, v)) => (k, JSON.Encode.string(v)))->Dict.fromArray->JSON.Encode.object
|
|
36
59
|
|
|
60
|
+
let financeClerk: Reventless.Identity.t = {
|
|
61
|
+
...Reventless.Identity.anonymous,
|
|
62
|
+
userId: "u_clerk",
|
|
63
|
+
groups: ["Finance"],
|
|
64
|
+
}
|
|
65
|
+
let visitor = Reventless.Identity.anonymous
|
|
66
|
+
|
|
37
67
|
describe("InboundTranslation_GWT.FromSlice", () => {
|
|
68
|
+
test("a door that reads its caller translates for the caller it admits", () =>
|
|
69
|
+
whenInput({orderId: "o1", status: "paidByHand"}, ~caller=financeClerk)->thenCommand(
|
|
70
|
+
"o1",
|
|
71
|
+
ConfirmPayment({orderId: "o1"}),
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
test("…and declines a caller it does not", () =>
|
|
76
|
+
whenInput({orderId: "o1", status: "paidByHand"}, ~caller=visitor)->thenNotUnderstood(
|
|
77
|
+
"only Finance marks a payment paid by hand",
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
// No caller is the platform acting for itself, which this door does not admit.
|
|
82
|
+
test("…and an input with no caller at all", () =>
|
|
83
|
+
whenInput({orderId: "o1", status: "paidByHand"})->thenNotUnderstood(
|
|
84
|
+
"only Finance marks a payment paid by hand",
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
test("received JSON carries its caller through the decode", () =>
|
|
89
|
+
whenReceived(
|
|
90
|
+
json([("orderId", "o1"), ("status", "paidByHand")]),
|
|
91
|
+
~caller=financeClerk,
|
|
92
|
+
)->thenCommand("o1", ConfirmPayment({orderId: "o1"}))
|
|
93
|
+
)
|
|
94
|
+
|
|
38
95
|
test("input translates to a command", () =>
|
|
39
96
|
whenInput({orderId: "o1", status: "paid"})->thenCommand("o1", ConfirmPayment({orderId: "o1"}))
|
|
40
97
|
)
|
|
@@ -61,4 +118,28 @@ describe("InboundTranslation_GWT.FromSlice", () => {
|
|
|
61
118
|
test("no commands is an answer", () =>
|
|
62
119
|
whenInput({orderId: "o1", status: "pending"})->thenNoCommand
|
|
63
120
|
)
|
|
121
|
+
|
|
122
|
+
test("translate reaches the capabilities it is handed", () =>
|
|
123
|
+
whenInput({orderId: "o1", status: "paid"})->thenSent([
|
|
124
|
+
Sent({
|
|
125
|
+
recipient: ToEmail(Reventless.Email.unsafe("buyer@example.com")),
|
|
126
|
+
message: {subject: "Paid", body: "o1"},
|
|
127
|
+
}),
|
|
128
|
+
])
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
test("a capability failure is the translation declining the input", () =>
|
|
132
|
+
whenInput(
|
|
133
|
+
{orderId: "o1", status: "paid"},
|
|
134
|
+
~fakes=fakes(~send=async (~recipient as _, ~message as _) => Error(Unavailable("down")), ()),
|
|
135
|
+
)->thenNotUnderstood("down")
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
test("a translation that throws declined the input", () =>
|
|
139
|
+
whenInput({orderId: "o1", status: "broken"})->thenNotUnderstood("the translation threw")
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
test("input that never translated sent nothing", () =>
|
|
143
|
+
whenReceived(json([("status", "paid")]))->thenNothingSent
|
|
144
|
+
)
|
|
64
145
|
})
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Sury from "sury";
|
|
4
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
5
|
+
import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
|
|
6
|
+
import * as Messaging$Reventless from "@reventlessdev/reventless-spec/src/semantic/Messaging.res.mjs";
|
|
4
7
|
import * as InboundTranslation_GWT$ReventlessGwt from "../src/InboundTranslation_GWT.res.mjs";
|
|
5
8
|
|
|
6
9
|
let name = "PaymentHook";
|
|
@@ -25,6 +28,8 @@ function authorizationOf(param) {
|
|
|
25
28
|
return "AllowAuthenticated";
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
let capabilityNeeds = ["Messaging"];
|
|
32
|
+
|
|
28
33
|
let traits = [];
|
|
29
34
|
|
|
30
35
|
let PaymentHook = {
|
|
@@ -36,25 +41,60 @@ let PaymentHook = {
|
|
|
36
41
|
externalSystem: externalSystem,
|
|
37
42
|
endpoint: undefined,
|
|
38
43
|
authorizationOf: authorizationOf,
|
|
44
|
+
capabilityNeeds: capabilityNeeds,
|
|
39
45
|
traits: traits
|
|
40
46
|
};
|
|
41
47
|
|
|
42
48
|
let moduleUrl$1 = "";
|
|
43
49
|
|
|
44
|
-
function translate(input) {
|
|
50
|
+
async function translate(input, capabilities, caller) {
|
|
45
51
|
let other = input.status;
|
|
46
52
|
switch (other) {
|
|
53
|
+
case "broken" :
|
|
54
|
+
return Stdlib_JsError.throwWithMessage("the translation threw");
|
|
47
55
|
case "paid" :
|
|
48
|
-
|
|
49
|
-
TAG: "
|
|
50
|
-
_0:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
let failure = await capabilities.messaging.send({
|
|
57
|
+
TAG: "ToEmail",
|
|
58
|
+
_0: "buyer@example.com"
|
|
59
|
+
}, {
|
|
60
|
+
subject: "Paid",
|
|
61
|
+
body: input.orderId
|
|
62
|
+
});
|
|
63
|
+
if (failure.TAG === "Ok") {
|
|
64
|
+
return {
|
|
65
|
+
TAG: "Ok",
|
|
66
|
+
_0: [[
|
|
67
|
+
input.orderId,
|
|
68
|
+
{
|
|
69
|
+
TAG: "ConfirmPayment",
|
|
70
|
+
orderId: input.orderId
|
|
71
|
+
}
|
|
72
|
+
]]
|
|
73
|
+
};
|
|
74
|
+
} else {
|
|
75
|
+
return {
|
|
76
|
+
TAG: "Error",
|
|
77
|
+
_0: Messaging$Reventless.failureReason(failure._0)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
case "paidByHand" :
|
|
81
|
+
if (caller !== undefined && Identity$Reventless.hasGroup(caller, "Finance")) {
|
|
82
|
+
return {
|
|
83
|
+
TAG: "Ok",
|
|
84
|
+
_0: [[
|
|
85
|
+
input.orderId,
|
|
86
|
+
{
|
|
87
|
+
TAG: "ConfirmPayment",
|
|
88
|
+
orderId: input.orderId
|
|
89
|
+
}
|
|
90
|
+
]]
|
|
91
|
+
};
|
|
92
|
+
} else {
|
|
93
|
+
return {
|
|
94
|
+
TAG: "Error",
|
|
95
|
+
_0: "only Finance marks a payment paid by hand"
|
|
96
|
+
};
|
|
97
|
+
}
|
|
58
98
|
case "pending" :
|
|
59
99
|
return {
|
|
60
100
|
TAG: "Ok",
|
|
@@ -81,7 +121,8 @@ let include = InboundTranslation_GWT$ReventlessGwt.FromSlice({
|
|
|
81
121
|
targetName: targetName,
|
|
82
122
|
externalSystem: externalSystem,
|
|
83
123
|
authorizationOf: authorizationOf,
|
|
84
|
-
endpoint: undefined
|
|
124
|
+
endpoint: undefined,
|
|
125
|
+
capabilityNeeds: capabilityNeeds
|
|
85
126
|
})({
|
|
86
127
|
translate: translate,
|
|
87
128
|
moduleUrl: moduleUrl$1
|
|
@@ -91,6 +132,8 @@ let describe = include.describe;
|
|
|
91
132
|
|
|
92
133
|
let test = include.test;
|
|
93
134
|
|
|
135
|
+
let fakes = include.fakes;
|
|
136
|
+
|
|
94
137
|
let whenInput = include.whenInput;
|
|
95
138
|
|
|
96
139
|
let whenReceived = include.whenReceived;
|
|
@@ -105,6 +148,10 @@ let thenTranslateError = include.thenTranslateError;
|
|
|
105
148
|
|
|
106
149
|
let thenRefusedInput = include.thenRefusedInput;
|
|
107
150
|
|
|
151
|
+
let thenSent = include.thenSent;
|
|
152
|
+
|
|
153
|
+
let thenNothingSent = include.thenNothingSent;
|
|
154
|
+
|
|
108
155
|
function json(pairs) {
|
|
109
156
|
return Object.fromEntries(pairs.map(param => [
|
|
110
157
|
param[0],
|
|
@@ -112,15 +159,49 @@ function json(pairs) {
|
|
|
112
159
|
]));
|
|
113
160
|
}
|
|
114
161
|
|
|
162
|
+
let newrecord = {...Identity$Reventless.anonymous};
|
|
163
|
+
|
|
164
|
+
newrecord.groups = ["Finance"];
|
|
165
|
+
|
|
166
|
+
newrecord.userId = "u_clerk";
|
|
167
|
+
|
|
115
168
|
describe("InboundTranslation_GWT.FromSlice", () => {
|
|
116
|
-
test("
|
|
169
|
+
test("a door that reads its caller translates for the caller it admits", undefined, () => thenCommand(whenInput({
|
|
170
|
+
orderId: "o1",
|
|
171
|
+
status: "paidByHand"
|
|
172
|
+
}, undefined, newrecord), "o1", {
|
|
173
|
+
TAG: "ConfirmPayment",
|
|
174
|
+
orderId: "o1"
|
|
175
|
+
}));
|
|
176
|
+
test("…and declines a caller it does not", undefined, () => thenNotUnderstood(whenInput({
|
|
177
|
+
orderId: "o1",
|
|
178
|
+
status: "paidByHand"
|
|
179
|
+
}, undefined, Identity$Reventless.anonymous), "only Finance marks a payment paid by hand"));
|
|
180
|
+
test("…and an input with no caller at all", undefined, () => thenNotUnderstood(whenInput({
|
|
181
|
+
orderId: "o1",
|
|
182
|
+
status: "paidByHand"
|
|
183
|
+
}, undefined, undefined), "only Finance marks a payment paid by hand"));
|
|
184
|
+
test("received JSON carries its caller through the decode", undefined, () => thenCommand(whenReceived(json([
|
|
185
|
+
[
|
|
186
|
+
"orderId",
|
|
187
|
+
"o1"
|
|
188
|
+
],
|
|
189
|
+
[
|
|
190
|
+
"status",
|
|
191
|
+
"paidByHand"
|
|
192
|
+
]
|
|
193
|
+
]), undefined, newrecord), "o1", {
|
|
194
|
+
TAG: "ConfirmPayment",
|
|
195
|
+
orderId: "o1"
|
|
196
|
+
}));
|
|
197
|
+
test("input translates to a command", undefined, () => thenCommand(whenInput({
|
|
117
198
|
orderId: "o1",
|
|
118
199
|
status: "paid"
|
|
119
|
-
}), "o1", {
|
|
200
|
+
}, undefined, undefined), "o1", {
|
|
120
201
|
TAG: "ConfirmPayment",
|
|
121
202
|
orderId: "o1"
|
|
122
203
|
}));
|
|
123
|
-
test("received JSON decodes before it translates", () => thenCommand(whenReceived(json([
|
|
204
|
+
test("received JSON decodes before it translates", undefined, () => thenCommand(whenReceived(json([
|
|
124
205
|
[
|
|
125
206
|
"orderId",
|
|
126
207
|
"o1"
|
|
@@ -129,42 +210,82 @@ describe("InboundTranslation_GWT.FromSlice", () => {
|
|
|
129
210
|
"status",
|
|
130
211
|
"paid"
|
|
131
212
|
]
|
|
132
|
-
])), "o1", {
|
|
213
|
+
]), undefined, undefined), "o1", {
|
|
133
214
|
TAG: "ConfirmPayment",
|
|
134
215
|
orderId: "o1"
|
|
135
216
|
}));
|
|
136
|
-
test("input that does not decode is refused before translate", () => thenRefusedInput(whenReceived(json([[
|
|
217
|
+
test("input that does not decode is refused before translate", undefined, () => thenRefusedInput(whenReceived(json([[
|
|
137
218
|
"status",
|
|
138
219
|
"paid"
|
|
139
|
-
]])), "orderId"));
|
|
140
|
-
test("input translate declines is not understood", () => thenNotUnderstood(whenInput({
|
|
220
|
+
]]), undefined, undefined), "orderId"));
|
|
221
|
+
test("input translate declines is not understood", undefined, () => thenNotUnderstood(whenInput({
|
|
141
222
|
orderId: "o1",
|
|
142
223
|
status: "lost"
|
|
143
|
-
}), "Unknown status: lost"));
|
|
144
|
-
test("thenTranslateError is the older name", () => thenTranslateError(whenInput({
|
|
224
|
+
}, undefined, undefined), "Unknown status: lost"));
|
|
225
|
+
test("thenTranslateError is the older name", undefined, () => thenTranslateError(whenInput({
|
|
145
226
|
orderId: "o1",
|
|
146
227
|
status: "lost"
|
|
147
|
-
}), "Unknown status: lost"));
|
|
148
|
-
test("no commands is an answer", () => thenNoCommand(whenInput({
|
|
228
|
+
}, undefined, undefined), "Unknown status: lost"));
|
|
229
|
+
test("no commands is an answer", undefined, () => thenNoCommand(whenInput({
|
|
149
230
|
orderId: "o1",
|
|
150
231
|
status: "pending"
|
|
151
|
-
})));
|
|
232
|
+
}, undefined, undefined)));
|
|
233
|
+
test("translate reaches the capabilities it is handed", undefined, () => thenSent(whenInput({
|
|
234
|
+
orderId: "o1",
|
|
235
|
+
status: "paid"
|
|
236
|
+
}, undefined, undefined), [{
|
|
237
|
+
TAG: "Sent",
|
|
238
|
+
recipient: {
|
|
239
|
+
TAG: "ToEmail",
|
|
240
|
+
_0: "buyer@example.com"
|
|
241
|
+
},
|
|
242
|
+
message: {
|
|
243
|
+
subject: "Paid",
|
|
244
|
+
body: "o1"
|
|
245
|
+
}
|
|
246
|
+
}]));
|
|
247
|
+
test("a capability failure is the translation declining the input", undefined, () => thenNotUnderstood(whenInput({
|
|
248
|
+
orderId: "o1",
|
|
249
|
+
status: "paid"
|
|
250
|
+
}, fakes(undefined, async (param, param$1) => ({
|
|
251
|
+
TAG: "Error",
|
|
252
|
+
_0: {
|
|
253
|
+
TAG: "Unavailable",
|
|
254
|
+
_0: "down"
|
|
255
|
+
}
|
|
256
|
+
}), undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined), "down"));
|
|
257
|
+
test("a translation that throws declined the input", undefined, () => thenNotUnderstood(whenInput({
|
|
258
|
+
orderId: "o1",
|
|
259
|
+
status: "broken"
|
|
260
|
+
}, undefined, undefined), "the translation threw"));
|
|
261
|
+
test("input that never translated sent nothing", undefined, () => thenNothingSent(whenReceived(json([[
|
|
262
|
+
"status",
|
|
263
|
+
"paid"
|
|
264
|
+
]]), undefined, undefined)));
|
|
152
265
|
});
|
|
153
266
|
|
|
154
267
|
let encPairs = include.encPairs;
|
|
155
268
|
|
|
269
|
+
let errorOf = include.errorOf;
|
|
270
|
+
|
|
156
271
|
let describeOther = include.describeOther;
|
|
157
272
|
|
|
158
273
|
let roundTrips = include.roundTrips;
|
|
159
274
|
|
|
160
275
|
let thenCommands = include.thenCommands;
|
|
161
276
|
|
|
277
|
+
let financeClerk = newrecord;
|
|
278
|
+
|
|
279
|
+
let visitor = Identity$Reventless.anonymous;
|
|
280
|
+
|
|
162
281
|
export {
|
|
163
282
|
PaymentHook,
|
|
164
283
|
PaymentHook_Translation,
|
|
165
284
|
describe,
|
|
166
285
|
test,
|
|
286
|
+
fakes,
|
|
167
287
|
encPairs,
|
|
288
|
+
errorOf,
|
|
168
289
|
whenInput,
|
|
169
290
|
whenReceived,
|
|
170
291
|
describeOther,
|
|
@@ -175,6 +296,10 @@ export {
|
|
|
175
296
|
thenNotUnderstood,
|
|
176
297
|
thenTranslateError,
|
|
177
298
|
thenRefusedInput,
|
|
299
|
+
thenSent,
|
|
300
|
+
thenNothingSent,
|
|
178
301
|
json,
|
|
302
|
+
financeClerk,
|
|
303
|
+
visitor,
|
|
179
304
|
}
|
|
180
305
|
/* externalInputSchema Not a pure module */
|
|
@@ -151,7 +151,7 @@ let down = Capabilities_Fake$ReventlessGwt.make(undefined, async (param, param$1
|
|
|
151
151
|
TAG: "Unavailable",
|
|
152
152
|
_0: "smtp down"
|
|
153
153
|
}
|
|
154
|
-
}), undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
154
|
+
}), undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
155
155
|
|
|
156
156
|
let welcome = {
|
|
157
157
|
TAG: "Sent",
|