@reventlessdev/online-shop-hybrid-ordering 1.0.0-alpha.268 → 1.0.0-alpha.270
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 +15 -0
- package/package.json +9 -9
- package/tests/Customer/OutboundTranslation/GeocodeCustomerAddress_GWT.res +41 -27
- package/tests/Customer/OutboundTranslation/GeocodeCustomerAddress_GWT.res.mjs +82 -37
- package/tests/Customer/OutboundTranslation/VerifyCustomerEmail_GWT.res +49 -73
- package/tests/Customer/OutboundTranslation/VerifyCustomerEmail_GWT.res.mjs +117 -95
- package/tests/Flow/OrderingFlow_GWT.res +2 -37
- package/tests/Flow/OrderingFlow_GWT.res.mjs +20 -84
- package/tests/Notification/Automation/NotificationIntake_GWT.res +2 -18
- package/tests/Notification/Automation/NotificationIntake_GWT.res.mjs +48 -52
- package/tests/Notification/OutboundTranslation/AnnounceRecipientContact_GWT.res +37 -0
- package/tests/Notification/OutboundTranslation/AnnounceRecipientContact_GWT.res.mjs +137 -0
- package/tests/Notification/OutboundTranslation/SendNotification_GWT.res +17 -25
- package/tests/Notification/OutboundTranslation/SendNotification_GWT.res.mjs +61 -46
- package/tests/Order/Automation/AutoShipOrder_GWT.res +17 -22
- package/tests/Order/Automation/AutoShipOrder_GWT.res.mjs +78 -57
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
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.270 (2026-10-10)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **gwt:** automation and translation tests take the slice as written, and the sidecars read them ([93a0919](https://github.com/ReventlessDev/reventless-core/commit/93a0919a0d66730cf9bf89144ad8fba3cb025332))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 1.0.0-alpha.269 (2026-10-08)
|
|
14
|
+
|
|
15
|
+
**Note:** Version bump only for package @reventlessdev/online-shop-hybrid-ordering
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
6
21
|
# 1.0.0-alpha.268 (2026-10-08)
|
|
7
22
|
|
|
8
23
|
**Note:** Version bump only for package @reventlessdev/online-shop-hybrid-ordering
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/online-shop-hybrid-ordering",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.270",
|
|
4
4
|
"description": "Online Shop Hybrid example — Ordering plugin (Customer aggregate + Order/CatalogProduct DCB)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -21,15 +21,15 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"sury": "11.0.0",
|
|
24
|
-
"@reventlessdev/online-shop-hybrid-catalog-spec": "1.0.0-alpha.
|
|
25
|
-
"@reventlessdev/online-shop-hybrid-ordering-spec": "1.0.0-alpha.
|
|
26
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.187",
|
|
24
|
+
"@reventlessdev/online-shop-hybrid-catalog-spec": "1.0.0-alpha.158",
|
|
25
|
+
"@reventlessdev/online-shop-hybrid-ordering-spec": "1.0.0-alpha.157",
|
|
27
26
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.23",
|
|
28
|
-
"@reventlessdev/reventless-gwt": "1.0.0-alpha.
|
|
29
|
-
"@reventlessdev/reventless-
|
|
30
|
-
"@reventlessdev/
|
|
31
|
-
"@reventlessdev/
|
|
32
|
-
"@reventlessdev/
|
|
27
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.239",
|
|
28
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.189",
|
|
29
|
+
"@reventlessdev/reventless-local": "3.0.0-alpha.282",
|
|
30
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.161",
|
|
31
|
+
"@reventlessdev/trait-address-geocoding": "1.0.0-alpha.41",
|
|
32
|
+
"@reventlessdev/trait-notification": "1.0.0-alpha.41"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"rescript": "12.3.1",
|
|
@@ -2,15 +2,9 @@
|
|
|
2
2
|
// log, which is what makes `~sourceId` load-bearing: an aggregate's event payload
|
|
3
3
|
// does not name its own subject.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// stays here is the host-specific wording of the answers.
|
|
9
|
-
|
|
10
|
-
module GeocodeCustomerAddressSlice = {
|
|
11
|
-
include GeocodeCustomerAddress
|
|
12
|
-
let collect = GeocodeCustomerAddress_Translation.collect
|
|
13
|
-
}
|
|
5
|
+
// The graft rules — keying, outage-vs-verdict — are asserted by the trait's suite
|
|
6
|
+
// in `AddressGeocodingConformance_GWT.res`; what stays here is the host-specific
|
|
7
|
+
// wording of the answers.
|
|
14
8
|
|
|
15
9
|
@@reventless.gwt
|
|
16
10
|
|
|
@@ -18,20 +12,8 @@ open Ordering_Examples
|
|
|
18
12
|
|
|
19
13
|
let vienna: Reventless.GeoPoint.t = {lat: 48.2082, lng: 16.3738}
|
|
20
14
|
|
|
21
|
-
// The real `translate`,
|
|
22
|
-
|
|
23
|
-
let withGeocoder = (
|
|
24
|
-
answer: result<array<Reventless.Geocoding.candidate>, Reventless.Geocoding.failure>,
|
|
25
|
-
) => {
|
|
26
|
-
// Spread `none` and override the one capability under test: a literal record
|
|
27
|
-
// would have to name every other capability the framework grows, and this test
|
|
28
|
-
// has nothing to say about them.
|
|
29
|
-
let capabilities: Reventless.Capabilities.t = {
|
|
30
|
-
...Reventless.Capabilities.none,
|
|
31
|
-
geocode: (~text as _) => Promise.resolve(answer),
|
|
32
|
-
}
|
|
33
|
-
(id, item) => GeocodeCustomerAddress_Translation.translate(id, item, ~capabilities)
|
|
34
|
-
}
|
|
15
|
+
// The real `translate`, against a geocoder that answers `answer`.
|
|
16
|
+
let geocoder = answer => fakes(~geocode=async (~text as _) => answer, ())
|
|
35
17
|
|
|
36
18
|
describe("GeocodeCustomerAddress OutboundTranslationSlice", () => {
|
|
37
19
|
// scenario-id: 8b319e75-7826-4d3d-bd88-a43a197fb014
|
|
@@ -63,8 +45,8 @@ describe("GeocodeCustomerAddress OutboundTranslationSlice", () => {
|
|
|
63
45
|
address: viennaAddress,
|
|
64
46
|
},
|
|
65
47
|
)
|
|
66
|
-
->
|
|
67
|
-
|
|
48
|
+
->givenCapabilities(
|
|
49
|
+
geocoder(
|
|
68
50
|
Ok([
|
|
69
51
|
{
|
|
70
52
|
label: "Stephansplatz 1, Vienna",
|
|
@@ -74,6 +56,7 @@ describe("GeocodeCustomerAddress OutboundTranslationSlice", () => {
|
|
|
74
56
|
]),
|
|
75
57
|
),
|
|
76
58
|
)
|
|
59
|
+
->whenTranslated
|
|
77
60
|
->thenCommand("cust-1", SetLocation({location: vienna, resolvedFrom: viennaAddress}))
|
|
78
61
|
)
|
|
79
62
|
|
|
@@ -82,8 +65,8 @@ describe("GeocodeCustomerAddress OutboundTranslationSlice", () => {
|
|
|
82
65
|
// scenario-id: 93103dd6-1304-42b1-83f9-fd4ac4a90160
|
|
83
66
|
test("translate: an ambiguous answer produces a reason naming the candidates", () =>
|
|
84
67
|
givenTodo("cust-1:Springfield", {customerId: cust1, address: "Springfield"})
|
|
85
|
-
->
|
|
86
|
-
|
|
68
|
+
->givenCapabilities(
|
|
69
|
+
geocoder(
|
|
87
70
|
Ok([
|
|
88
71
|
{
|
|
89
72
|
label: "Springfield, IL",
|
|
@@ -98,6 +81,7 @@ describe("GeocodeCustomerAddress OutboundTranslationSlice", () => {
|
|
|
98
81
|
]),
|
|
99
82
|
),
|
|
100
83
|
)
|
|
84
|
+
->whenTranslated
|
|
101
85
|
->thenCommand(
|
|
102
86
|
"cust-1",
|
|
103
87
|
MarkAddressUnresolvable({
|
|
@@ -106,4 +90,34 @@ describe("GeocodeCustomerAddress OutboundTranslationSlice", () => {
|
|
|
106
90
|
}),
|
|
107
91
|
)
|
|
108
92
|
)
|
|
93
|
+
|
|
94
|
+
// The address is what is asked, as written.
|
|
95
|
+
test("translate: the geocoder is asked for the address as written", () =>
|
|
96
|
+
givenTodo("cust-1:Stephansplatz 1, Vienna", {customerId: cust1, address: viennaAddress})
|
|
97
|
+
->givenCapabilities(geocoder(Ok([])))
|
|
98
|
+
->whenTranslated
|
|
99
|
+
->thenSent([Geocoded({text: viennaAddress})])
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
// An outage is not a verdict: the row stays open for the next sweep.
|
|
103
|
+
test("translate: a geocoder that is down leaves the row to be retried", () =>
|
|
104
|
+
givenTodo("cust-1:Stephansplatz 1, Vienna", {customerId: cust1, address: viennaAddress})
|
|
105
|
+
->givenCapabilities(geocoder(Error(Unavailable("timeout"))))
|
|
106
|
+
->whenTranslated
|
|
107
|
+
->thenTodoStatus("cust-1:Stephansplatz 1, Vienna", #Failed)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
// The budget ran out with the geocoder never answering: the verdict is recorded
|
|
111
|
+
// rather than leaving the row pending forever.
|
|
112
|
+
test("exhausted: the address is recorded as unresolvable", () =>
|
|
113
|
+
givenTodo("cust-1:Stephansplatz 1, Vienna", {customerId: cust1, address: viennaAddress})
|
|
114
|
+
->whenExhausted(~lastError="timeout")
|
|
115
|
+
->thenCommand(
|
|
116
|
+
"cust-1",
|
|
117
|
+
MarkAddressUnresolvable({
|
|
118
|
+
address: viennaAddress,
|
|
119
|
+
reason: GeocodeCustomerAddress_Translation.Geocode.exhaustedReason(Some("timeout")),
|
|
120
|
+
}),
|
|
121
|
+
)
|
|
122
|
+
)
|
|
109
123
|
})
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
-
import * as Capabilities$Reventless from "@reventlessdev/reventless-spec/src/semantic/Capabilities.res.mjs";
|
|
4
3
|
import * as Ordering_Examples$OrderingPlugin from "../../Ordering_Examples.res.mjs";
|
|
5
4
|
import * as GeocodeCustomerAddress$OrderingPlugin from "../../../src/Customer/OutboundTranslation/GeocodeCustomerAddress.res.mjs";
|
|
6
5
|
import * as OutboundTranslation_GWT$ReventlessGwt from "@reventlessdev/reventless-gwt/src/OutboundTranslation_GWT.res.mjs";
|
|
6
|
+
import * as AddressGeocoding_Translate$TraitAddressGeocoding from "@reventlessdev/trait-address-geocoding/src/AddressGeocoding_Translate.res.mjs";
|
|
7
7
|
import * as GeocodeCustomerAddress_Translation$OrderingPlugin from "../../../src/Customer/OutboundTranslation/GeocodeCustomerAddress_Translation.res.mjs";
|
|
8
8
|
|
|
9
|
-
let
|
|
9
|
+
let include = OutboundTranslation_GWT$ReventlessGwt.FromSlice({
|
|
10
10
|
name: GeocodeCustomerAddress$OrderingPlugin.name,
|
|
11
|
-
|
|
11
|
+
moduleUrl: GeocodeCustomerAddress$OrderingPlugin.moduleUrl,
|
|
12
12
|
consumedEventSchema: GeocodeCustomerAddress$OrderingPlugin.consumedEventSchema,
|
|
13
13
|
outboundItemSchema: GeocodeCustomerAddress$OrderingPlugin.outboundItemSchema,
|
|
14
14
|
inboundCommandSchema: GeocodeCustomerAddress$OrderingPlugin.inboundCommandSchema,
|
|
@@ -18,17 +18,12 @@ let GeocodeCustomerAddressSlice = {
|
|
|
18
18
|
sourceNames: GeocodeCustomerAddress$OrderingPlugin.sourceNames,
|
|
19
19
|
externalSystem: GeocodeCustomerAddress$OrderingPlugin.externalSystem,
|
|
20
20
|
capabilityNeeds: GeocodeCustomerAddress$OrderingPlugin.capabilityNeeds,
|
|
21
|
-
traits: GeocodeCustomerAddress$OrderingPlugin.traits
|
|
22
|
-
|
|
23
|
-
collect: GeocodeCustomerAddress_Translation$OrderingPlugin.collect
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
name: GeocodeCustomerAddress$OrderingPlugin.name,
|
|
28
|
-
consumedEventSchema: GeocodeCustomerAddress$OrderingPlugin.consumedEventSchema,
|
|
29
|
-
outboundItemSchema: GeocodeCustomerAddress$OrderingPlugin.outboundItemSchema,
|
|
30
|
-
inboundCommandSchema: GeocodeCustomerAddress$OrderingPlugin.inboundCommandSchema,
|
|
31
|
-
collect: GeocodeCustomerAddress_Translation$OrderingPlugin.collect
|
|
21
|
+
traits: GeocodeCustomerAddress$OrderingPlugin.traits
|
|
22
|
+
})({
|
|
23
|
+
collect: GeocodeCustomerAddress_Translation$OrderingPlugin.collect,
|
|
24
|
+
translate: GeocodeCustomerAddress_Translation$OrderingPlugin.translate,
|
|
25
|
+
onExhausted: GeocodeCustomerAddress_Translation$OrderingPlugin.onExhausted,
|
|
26
|
+
moduleUrl: GeocodeCustomerAddress_Translation$OrderingPlugin.moduleUrl
|
|
32
27
|
});
|
|
33
28
|
|
|
34
29
|
let describe = include.describe;
|
|
@@ -37,10 +32,20 @@ let test = include.test;
|
|
|
37
32
|
|
|
38
33
|
let givenTodo = include.givenTodo;
|
|
39
34
|
|
|
35
|
+
let fakes = include.fakes;
|
|
36
|
+
|
|
37
|
+
let givenCapabilities = include.givenCapabilities;
|
|
38
|
+
|
|
39
|
+
let whenTranslated = include.whenTranslated;
|
|
40
|
+
|
|
40
41
|
let whenTranslateMocked = include.whenTranslateMocked;
|
|
41
42
|
|
|
43
|
+
let whenExhausted = include.whenExhausted;
|
|
44
|
+
|
|
42
45
|
let thenCommand = include.thenCommand;
|
|
43
46
|
|
|
47
|
+
let thenSent = include.thenSent;
|
|
48
|
+
|
|
44
49
|
let thenTodoStatus = include.thenTodoStatus;
|
|
45
50
|
|
|
46
51
|
let vienna = {
|
|
@@ -48,18 +53,8 @@ let vienna = {
|
|
|
48
53
|
lng: 16.3738
|
|
49
54
|
};
|
|
50
55
|
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
let capabilities_messaging = Capabilities$Reventless.none.messaging;
|
|
54
|
-
let capabilities_identityProvider = Capabilities$Reventless.none.identityProvider;
|
|
55
|
-
let capabilities_secrets = Capabilities$Reventless.none.secrets;
|
|
56
|
-
let capabilities = {
|
|
57
|
-
geocode: capabilities_geocode,
|
|
58
|
-
messaging: capabilities_messaging,
|
|
59
|
-
identityProvider: capabilities_identityProvider,
|
|
60
|
-
secrets: capabilities_secrets
|
|
61
|
-
};
|
|
62
|
-
return (id, item) => GeocodeCustomerAddress_Translation$OrderingPlugin.translate(id, item, capabilities);
|
|
56
|
+
function geocoder(answer) {
|
|
57
|
+
return fakes(async param => answer, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
63
58
|
}
|
|
64
59
|
|
|
65
60
|
describe("GeocodeCustomerAddress OutboundTranslationSlice", () => {
|
|
@@ -77,25 +72,25 @@ describe("GeocodeCustomerAddress OutboundTranslationSlice", () => {
|
|
|
77
72
|
}
|
|
78
73
|
]
|
|
79
74
|
})), "cust-1:Stephansplatz 1, Vienna", "Completed"));
|
|
80
|
-
test("translate: a confident answer produces SetLocation", undefined, () => thenCommand(
|
|
75
|
+
test("translate: a confident answer produces SetLocation", undefined, () => thenCommand(whenTranslated(givenCapabilities(givenTodo("cust-1:Stephansplatz 1, Vienna", {
|
|
81
76
|
customerId: Ordering_Examples$OrderingPlugin.cust1,
|
|
82
77
|
address: Ordering_Examples$OrderingPlugin.viennaAddress
|
|
83
|
-
}),
|
|
78
|
+
}), geocoder({
|
|
84
79
|
TAG: "Ok",
|
|
85
80
|
_0: [{
|
|
86
81
|
label: "Stephansplatz 1, Vienna",
|
|
87
82
|
point: vienna,
|
|
88
83
|
relevance: 0.995
|
|
89
84
|
}]
|
|
90
|
-
})), "cust-1", {
|
|
85
|
+
}))), "cust-1", {
|
|
91
86
|
TAG: "SetLocation",
|
|
92
87
|
location: vienna,
|
|
93
88
|
resolvedFrom: Ordering_Examples$OrderingPlugin.viennaAddress
|
|
94
89
|
}));
|
|
95
|
-
test("translate: an ambiguous answer produces a reason naming the candidates", undefined, () => thenCommand(
|
|
90
|
+
test("translate: an ambiguous answer produces a reason naming the candidates", undefined, () => thenCommand(whenTranslated(givenCapabilities(givenTodo("cust-1:Springfield", {
|
|
96
91
|
customerId: Ordering_Examples$OrderingPlugin.cust1,
|
|
97
92
|
address: "Springfield"
|
|
98
|
-
}),
|
|
93
|
+
}), geocoder({
|
|
99
94
|
TAG: "Ok",
|
|
100
95
|
_0: [
|
|
101
96
|
{
|
|
@@ -109,46 +104,96 @@ describe("GeocodeCustomerAddress OutboundTranslationSlice", () => {
|
|
|
109
104
|
relevance: 0.985
|
|
110
105
|
}
|
|
111
106
|
]
|
|
112
|
-
})), "cust-1", {
|
|
107
|
+
}))), "cust-1", {
|
|
113
108
|
TAG: "MarkAddressUnresolvable",
|
|
114
109
|
address: "Springfield",
|
|
115
110
|
reason: `"Springfield" matched "Springfield, IL" and "Springfield, MA" about equally well`
|
|
116
111
|
}));
|
|
112
|
+
test("translate: the geocoder is asked for the address as written", undefined, () => thenSent(whenTranslated(givenCapabilities(givenTodo("cust-1:Stephansplatz 1, Vienna", {
|
|
113
|
+
customerId: Ordering_Examples$OrderingPlugin.cust1,
|
|
114
|
+
address: Ordering_Examples$OrderingPlugin.viennaAddress
|
|
115
|
+
}), geocoder({
|
|
116
|
+
TAG: "Ok",
|
|
117
|
+
_0: []
|
|
118
|
+
}))), [{
|
|
119
|
+
TAG: "Geocoded",
|
|
120
|
+
text: Ordering_Examples$OrderingPlugin.viennaAddress
|
|
121
|
+
}]));
|
|
122
|
+
test("translate: a geocoder that is down leaves the row to be retried", undefined, () => thenTodoStatus(whenTranslated(givenCapabilities(givenTodo("cust-1:Stephansplatz 1, Vienna", {
|
|
123
|
+
customerId: Ordering_Examples$OrderingPlugin.cust1,
|
|
124
|
+
address: Ordering_Examples$OrderingPlugin.viennaAddress
|
|
125
|
+
}), geocoder({
|
|
126
|
+
TAG: "Error",
|
|
127
|
+
_0: {
|
|
128
|
+
TAG: "Unavailable",
|
|
129
|
+
_0: "timeout"
|
|
130
|
+
}
|
|
131
|
+
}))), "cust-1:Stephansplatz 1, Vienna", "Failed"));
|
|
132
|
+
test("exhausted: the address is recorded as unresolvable", undefined, () => thenCommand(whenExhausted(givenTodo("cust-1:Stephansplatz 1, Vienna", {
|
|
133
|
+
customerId: Ordering_Examples$OrderingPlugin.cust1,
|
|
134
|
+
address: Ordering_Examples$OrderingPlugin.viennaAddress
|
|
135
|
+
}), "timeout"), "cust-1", {
|
|
136
|
+
TAG: "MarkAddressUnresolvable",
|
|
137
|
+
address: Ordering_Examples$OrderingPlugin.viennaAddress,
|
|
138
|
+
reason: AddressGeocoding_Translate$TraitAddressGeocoding.exhaustedReason("timeout")
|
|
139
|
+
}));
|
|
117
140
|
});
|
|
118
141
|
|
|
119
|
-
let Spec = include.Spec;
|
|
120
|
-
|
|
121
142
|
let testSync = include.testSync;
|
|
122
143
|
|
|
144
|
+
let encItems = include.encItems;
|
|
145
|
+
|
|
146
|
+
let commandPairJson = include.commandPairJson;
|
|
147
|
+
|
|
148
|
+
let errorOf = include.errorOf;
|
|
149
|
+
|
|
123
150
|
let givenEvent = include.givenEvent;
|
|
124
151
|
|
|
125
152
|
let whenCollect = include.whenCollect;
|
|
126
153
|
|
|
127
154
|
let thenTodos = include.thenTodos;
|
|
128
155
|
|
|
156
|
+
let attemptWith = include.attemptWith;
|
|
157
|
+
|
|
158
|
+
let attempts = include.attempts;
|
|
159
|
+
|
|
129
160
|
let whenTranslateRetrying = include.whenTranslateRetrying;
|
|
130
161
|
|
|
131
162
|
let thenNoCommand = include.thenNoCommand;
|
|
132
163
|
|
|
164
|
+
let thenNothingSent = include.thenNothingSent;
|
|
165
|
+
|
|
133
166
|
let thenRetryRecorded = include.thenRetryRecorded;
|
|
134
167
|
|
|
168
|
+
let statusName = include.statusName;
|
|
169
|
+
|
|
135
170
|
export {
|
|
136
|
-
GeocodeCustomerAddressSlice,
|
|
137
|
-
Spec,
|
|
138
171
|
describe,
|
|
139
172
|
test,
|
|
140
173
|
testSync,
|
|
174
|
+
encItems,
|
|
175
|
+
commandPairJson,
|
|
176
|
+
errorOf,
|
|
141
177
|
givenEvent,
|
|
142
178
|
whenCollect,
|
|
143
179
|
thenTodos,
|
|
144
180
|
givenTodo,
|
|
181
|
+
fakes,
|
|
182
|
+
givenCapabilities,
|
|
183
|
+
attemptWith,
|
|
184
|
+
attempts,
|
|
185
|
+
whenTranslated,
|
|
145
186
|
whenTranslateMocked,
|
|
146
187
|
whenTranslateRetrying,
|
|
188
|
+
whenExhausted,
|
|
147
189
|
thenCommand,
|
|
148
190
|
thenNoCommand,
|
|
191
|
+
thenSent,
|
|
192
|
+
thenNothingSent,
|
|
149
193
|
thenRetryRecorded,
|
|
194
|
+
statusName,
|
|
150
195
|
thenTodoStatus,
|
|
151
196
|
vienna,
|
|
152
|
-
|
|
197
|
+
geocoder,
|
|
153
198
|
}
|
|
154
199
|
/* include Not a pure module */
|
|
@@ -6,66 +6,41 @@
|
|
|
6
6
|
// predict, and this file would otherwise be able to check only that *something*
|
|
7
7
|
// was sent.
|
|
8
8
|
|
|
9
|
-
module VerifyCustomerEmailSlice = {
|
|
10
|
-
include VerifyCustomerEmail
|
|
11
|
-
let collect = VerifyCustomerEmail_Translation.collect
|
|
12
|
-
}
|
|
13
|
-
|
|
14
9
|
@@reventless.gwt
|
|
15
10
|
|
|
16
11
|
module Outcome = ReventlessGwt.Outcome
|
|
17
12
|
|
|
18
13
|
let item: VerifyCustomerEmail.outboundItem = {customerId: "c1", email: "alice@x.y"}
|
|
19
14
|
|
|
20
|
-
// What the run put on the wire, in both directions: the bodies that went to the
|
|
21
|
-
// address, and the commands that reached the ledger.
|
|
22
|
-
let bodies: ref<array<string>> = ref([])
|
|
23
|
-
let issued: ref<array<string>> = ref([])
|
|
24
|
-
|
|
25
15
|
// A known secret and a hash obviously derived from it, so an assertion can name
|
|
26
16
|
// both. `hashed:` is not what the real backend computes — what matters is that
|
|
27
17
|
// whatever the source produced is what the ledger was told about.
|
|
28
|
-
let withSecrets = (~send)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
let
|
|
37
|
-
...Reventless.Capabilities.none,
|
|
38
|
-
messaging: Reventless.Messaging.makeProvider(~emailAndSms=[Email], ~pushServices=[], ~send),
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let accepts = (~recipient as _, ~message: Reventless.Messaging.message) => {
|
|
42
|
-
bodies := bodies.contents->Array.concat([message.body])
|
|
43
|
-
Promise.resolve(Ok({Reventless.Messaging.ref: "ses-1"}))
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
let refuses = failure => (~recipient as _, ~message as _) => Promise.resolve(Error(failure))
|
|
47
|
-
|
|
48
|
-
// Records what reached the ledger, because the command carries an instant read
|
|
49
|
-
// from the clock and so cannot be compared whole.
|
|
50
|
-
let run = capabilities => {
|
|
51
|
-
bodies := []
|
|
52
|
-
issued := []
|
|
53
|
-
async (id, item) => {
|
|
54
|
-
let attempt = await VerifyCustomerEmail_Translation.translate(id, item, ~capabilities)
|
|
55
|
-
switch attempt {
|
|
56
|
-
| Ok(Some((_, IssueEmailChallenge({proofHash, email, purpose, _})))) =>
|
|
57
|
-
issued := issued.contents->Array.concat([`${email}|${purpose}|${proofHash}`])
|
|
58
|
-
| _ => ()
|
|
59
|
-
}
|
|
60
|
-
attempt
|
|
61
|
-
}
|
|
62
|
-
}
|
|
18
|
+
let withSecrets = (~send=?, ()) =>
|
|
19
|
+
fakes(~token="known-secret", ~hash=t => `hashed:${t}`, ~channels=[Email], ~send?, ())
|
|
20
|
+
|
|
21
|
+
// No secret source at all, and messaging wired so the refusal cannot be
|
|
22
|
+
// mistaken for a mail problem.
|
|
23
|
+
let withoutSecrets = () =>
|
|
24
|
+
fakes(~secrets=Reventless.Secrets.unavailable(~reason="none provisioned"), ~channels=[Email], ())
|
|
25
|
+
|
|
26
|
+
let refuses = failure => async (~recipient as _, ~message as _) => Error(failure)
|
|
63
27
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
28
|
+
// The challenge mail, as the slice words it for this secret.
|
|
29
|
+
let challengeMail: ReventlessGwt.Capabilities_Fake.call = Sent({
|
|
30
|
+
recipient: ContactVerification.ByEmail("alice@x.y")->ContactVerification.toRecipient,
|
|
31
|
+
message: {subject: "Confirm your email address", body: body(~token="known-secret")},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// What reached the ledger. The command carries an instant read from the clock,
|
|
35
|
+
// so it cannot be compared whole.
|
|
36
|
+
let thenLedgerTold = async (pending, expected) =>
|
|
37
|
+
switch (await pending).result {
|
|
38
|
+
| Ok(Some((_, IssueEmailChallenge({proofHash, email, purpose, _})))) =>
|
|
39
|
+
let actual = `${email}|${purpose}|${proofHash}`
|
|
40
|
+
actual == expected
|
|
41
|
+
? Outcome.pass
|
|
42
|
+
: Outcome.fail(TranslateError({expected, actual: Some(actual)}))
|
|
43
|
+
| _ => Outcome.fail(TranslateError({expected, actual: None}))
|
|
69
44
|
}
|
|
70
45
|
|
|
71
46
|
describe("VerifyCustomerEmail OutboundTranslationSlice", () => {
|
|
@@ -74,23 +49,21 @@ describe("VerifyCustomerEmail OutboundTranslationSlice", () => {
|
|
|
74
49
|
// scenario-id: 866cd8c0-c30b-4049-85e1-98359a252c49
|
|
75
50
|
test("a sent challenge is opened against the hash, never the secret", () =>
|
|
76
51
|
givenTodo("c1:alice@x.y", item)
|
|
77
|
-
->
|
|
78
|
-
->
|
|
79
|
-
->
|
|
80
|
-
~expected="the ledger is told alice@x.y|ContactChange|hashed:known-secret",
|
|
81
|
-
() => issued.contents == ["alice@x.y|ContactChange|hashed:known-secret"],
|
|
82
|
-
)
|
|
52
|
+
->givenCapabilities(withSecrets())
|
|
53
|
+
->whenTranslated
|
|
54
|
+
->thenLedgerTold("alice@x.y|ContactChange|hashed:known-secret")
|
|
83
55
|
)
|
|
84
56
|
|
|
85
57
|
// scenario-id: dea4775c-9c1f-41e2-bd54-751b308c65cd
|
|
86
58
|
test("the secret itself is what goes to the address", () =>
|
|
87
59
|
givenTodo("c1:alice@x.y", item)
|
|
88
|
-
->
|
|
89
|
-
->
|
|
90
|
-
->
|
|
91
|
-
|
|
92
|
-
(
|
|
93
|
-
|
|
60
|
+
->givenCapabilities(withSecrets())
|
|
61
|
+
->whenTranslated
|
|
62
|
+
->thenSent([
|
|
63
|
+
TokenDrawn({length: policy.proofLength, alphabet: policy.proofAlphabet}),
|
|
64
|
+
Hashed({input: "known-secret"}),
|
|
65
|
+
challengeMail,
|
|
66
|
+
])
|
|
94
67
|
)
|
|
95
68
|
|
|
96
69
|
// 🚨 Send first, then record. A challenge opened against a message that never
|
|
@@ -99,19 +72,21 @@ describe("VerifyCustomerEmail OutboundTranslationSlice", () => {
|
|
|
99
72
|
// scenario-id: 7de712d8-9930-41a2-977d-137639e38812
|
|
100
73
|
test("a refused send opens no challenge", () =>
|
|
101
74
|
givenTodo("c1:alice@x.y", item)
|
|
102
|
-
->
|
|
103
|
-
|
|
75
|
+
->givenCapabilities(
|
|
76
|
+
withSecrets(~send=refuses(Reventless.Messaging.Refused("no such mailbox")), ()),
|
|
104
77
|
)
|
|
105
|
-
->
|
|
78
|
+
->whenTranslated
|
|
79
|
+
->thenTodoStatus("c1:alice@x.y", #Failed)
|
|
106
80
|
)
|
|
107
81
|
|
|
108
82
|
// scenario-id: a04b17e9-d691-40d3-a431-894a1c75e9f2
|
|
109
83
|
test("an unreachable provider opens no challenge either", () =>
|
|
110
84
|
givenTodo("c1:alice@x.y", item)
|
|
111
|
-
->
|
|
112
|
-
|
|
85
|
+
->givenCapabilities(
|
|
86
|
+
withSecrets(~send=refuses(Reventless.Messaging.Unavailable("smtp timeout")), ()),
|
|
113
87
|
)
|
|
114
|
-
->
|
|
88
|
+
->whenTranslated
|
|
89
|
+
->thenTodoStatus("c1:alice@x.y", #Failed)
|
|
115
90
|
)
|
|
116
91
|
|
|
117
92
|
// A deployment with no secret source cannot prove anything. That is a gap in
|
|
@@ -120,16 +95,17 @@ describe("VerifyCustomerEmail OutboundTranslationSlice", () => {
|
|
|
120
95
|
// scenario-id: 70d4d062-1ba5-4d77-8e27-8af0f98d4bd8
|
|
121
96
|
test("no secret source opens no challenge", () =>
|
|
122
97
|
givenTodo("c1:alice@x.y", item)
|
|
123
|
-
->
|
|
124
|
-
->
|
|
98
|
+
->givenCapabilities(withoutSecrets())
|
|
99
|
+
->whenTranslated
|
|
100
|
+
->thenTodoStatus("c1:alice@x.y", #Failed)
|
|
125
101
|
)
|
|
126
102
|
|
|
127
103
|
// scenario-id: 29f4868d-b099-401f-9609-913f6c55bddf
|
|
128
104
|
test("no secret source sends nothing either", () =>
|
|
129
105
|
givenTodo("c1:alice@x.y", item)
|
|
130
|
-
->
|
|
131
|
-
->
|
|
132
|
-
->
|
|
106
|
+
->givenCapabilities(withoutSecrets())
|
|
107
|
+
->whenTranslated
|
|
108
|
+
->thenSent([TokenDrawn({length: policy.proofLength, alphabet: policy.proofAlphabet})])
|
|
133
109
|
)
|
|
134
110
|
})
|
|
135
111
|
|