@reventlessdev/reventless-gwt 1.0.0-alpha.231 → 1.0.0-alpha.233
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 +22 -0
- package/package.json +6 -6
- package/src/Behavior_GWT.res +294 -19
- package/src/Behavior_GWT.res.mjs +429 -41
- package/src/StateChange/ExternalAddCategorySlice.res +5 -0
- package/src/StateChange/ExternalAddCategorySlice.res.mjs +5 -0
- package/src/StateChange/WithFixtures.res +5 -0
- package/src/StateChange/WithFixtures.res.mjs +5 -0
- package/src/StateChange/WithManualOpen.res +5 -0
- package/src/StateChange/WithManualOpen.res.mjs +5 -0
- package/tests/CallerTest.res +63 -0
- package/tests/CallerTest.res.mjs +70 -0
- package/tests/DelegateGwtTest.res +10 -5
- package/tests/FlowCrossPluginGwtTest.res +8 -4
- package/tests/FlowCrossPluginGwtTest.res.mjs +4 -2
- package/tests/StateChange/ExternalAddCategorySlice_GWT.res.mjs +9 -0
- package/tests/StateChange/WithFixtures_GWT.res.mjs +9 -0
- package/tests/StateChange/WithManualOpen_GWT.res.mjs +9 -0
- package/tests/StateChangeSliceAuthorizeGwtTest.res +174 -0
- package/tests/StateChangeSliceAuthorizeGwtTest.res.mjs +326 -0
- package/tests/StateChangeSliceGwtTest.res.mjs +9 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// `Caller.owner` takes an id as a scenario holds it, typed or not, so a test
|
|
4
|
+
// never converts one by hand; roles arrive as the plugin's own `Roles.t`.
|
|
5
|
+
|
|
6
|
+
module CustomerId = Reventless.Id.Make({
|
|
7
|
+
let key = "customerId"
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
module Roles = {
|
|
11
|
+
type t = Merchandiser
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("Caller.owner", () => {
|
|
15
|
+
testSync("takes a typed id as the user id it stands for", () =>
|
|
16
|
+
expect(Behavior_GWT.Caller.owner(CustomerId.makeFromString("c1")).claim)->toEqual(
|
|
17
|
+
Reventless.Message.CallerClaim.Owned({userId: "c1"}),
|
|
18
|
+
)
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
testSync("takes a plain string too", () =>
|
|
22
|
+
expect(Behavior_GWT.Caller.owner("c1").claim)->toEqual(
|
|
23
|
+
Reventless.Message.CallerClaim.Owned({userId: "c1"}),
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
testSync("holds no role unless given some", () =>
|
|
28
|
+
expect(Behavior_GWT.Caller.owner("c1").roles)->toEqual([])
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
testSync("holds the roles it is given, by name", () =>
|
|
32
|
+
expect(Behavior_GWT.Caller.owner("c1", ~roles=[Roles.Merchandiser]).roles)->toEqual([
|
|
33
|
+
"Merchandiser",
|
|
34
|
+
])
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
// Anything that is not a string at runtime is not an id, and comparing it
|
|
38
|
+
// with a recorded owner would only ever refuse — a silent wrong answer.
|
|
39
|
+
testSync("refuses a value that is not an id", () =>
|
|
40
|
+
expect(
|
|
41
|
+
switch Behavior_GWT.Caller.owner(42) {
|
|
42
|
+
| _ => false
|
|
43
|
+
| exception _ => true
|
|
44
|
+
},
|
|
45
|
+
)->toBe(true)
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe("Caller.inRoles", () => {
|
|
50
|
+
testSync("is signed in, holds the roles, and owns nothing in particular", () => {
|
|
51
|
+
let caller = Behavior_GWT.Caller.inRoles([Roles.Merchandiser])
|
|
52
|
+
expect((caller.signedIn, caller.roles))->toEqual((true, ["Merchandiser"]))
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
testSync("refuses a role that is not a payload-less case", () =>
|
|
56
|
+
expect(
|
|
57
|
+
switch Behavior_GWT.Caller.inRoles([42]) {
|
|
58
|
+
| _ => false
|
|
59
|
+
| exception _ => true
|
|
60
|
+
},
|
|
61
|
+
)->toBe(true)
|
|
62
|
+
)
|
|
63
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
|
|
4
|
+
import * as Behavior_GWT$ReventlessGwt from "../src/Behavior_GWT.res.mjs";
|
|
5
|
+
|
|
6
|
+
let CustomerId = Id$Reventless.Make({
|
|
7
|
+
key: "customerId"
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
let Roles = {};
|
|
11
|
+
|
|
12
|
+
globalThis.describe("Caller.owner", () => {
|
|
13
|
+
globalThis.test("takes a typed id as the user id it stands for", () => {
|
|
14
|
+
globalThis.expect(Behavior_GWT$ReventlessGwt.Caller.owner(CustomerId.makeFromString("c1"), undefined).claim).toEqual({
|
|
15
|
+
TAG: "Owned",
|
|
16
|
+
userId: "c1"
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
globalThis.test("takes a plain string too", () => {
|
|
20
|
+
globalThis.expect(Behavior_GWT$ReventlessGwt.Caller.owner("c1", undefined).claim).toEqual({
|
|
21
|
+
TAG: "Owned",
|
|
22
|
+
userId: "c1"
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
globalThis.test("holds no role unless given some", () => {
|
|
26
|
+
globalThis.expect(Behavior_GWT$ReventlessGwt.Caller.owner("c1", undefined).roles).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
globalThis.test("holds the roles it is given, by name", () => {
|
|
29
|
+
globalThis.expect(Behavior_GWT$ReventlessGwt.Caller.owner("c1", ["Merchandiser"]).roles).toEqual(["Merchandiser"]);
|
|
30
|
+
});
|
|
31
|
+
globalThis.test("refuses a value that is not an id", () => {
|
|
32
|
+
let tmp;
|
|
33
|
+
try {
|
|
34
|
+
Behavior_GWT$ReventlessGwt.Caller.owner(42, undefined);
|
|
35
|
+
tmp = false;
|
|
36
|
+
} catch (exn) {
|
|
37
|
+
tmp = true;
|
|
38
|
+
}
|
|
39
|
+
globalThis.expect(tmp).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
globalThis.describe("Caller.inRoles", () => {
|
|
44
|
+
globalThis.test("is signed in, holds the roles, and owns nothing in particular", () => {
|
|
45
|
+
let caller = Behavior_GWT$ReventlessGwt.Caller.inRoles(["Merchandiser"]);
|
|
46
|
+
globalThis.expect([
|
|
47
|
+
caller.signedIn,
|
|
48
|
+
caller.roles
|
|
49
|
+
]).toEqual([
|
|
50
|
+
true,
|
|
51
|
+
["Merchandiser"]
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
globalThis.test("refuses a role that is not a payload-less case", () => {
|
|
55
|
+
let tmp;
|
|
56
|
+
try {
|
|
57
|
+
Behavior_GWT$ReventlessGwt.Caller.inRoles([42]);
|
|
58
|
+
tmp = false;
|
|
59
|
+
} catch (exn) {
|
|
60
|
+
tmp = true;
|
|
61
|
+
}
|
|
62
|
+
globalThis.expect(tmp).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export {
|
|
67
|
+
CustomerId,
|
|
68
|
+
Roles,
|
|
69
|
+
}
|
|
70
|
+
/* CustomerId Not a pure module */
|
|
@@ -45,7 +45,8 @@ module ProductDelegate = {
|
|
|
45
45
|
type error = NoError
|
|
46
46
|
|
|
47
47
|
let moduleUrl = ""
|
|
48
|
-
|
|
48
|
+
type role = Reventless.Role.name
|
|
49
|
+
let commandAuthorization = (_: command): Reventless.Authorization.rule<role> => AllowAuthenticated
|
|
49
50
|
type lifecycleState = unit
|
|
50
51
|
let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
|
|
51
52
|
}
|
|
@@ -132,7 +133,8 @@ module OrderDelegate = {
|
|
|
132
133
|
type error = NoError
|
|
133
134
|
|
|
134
135
|
let moduleUrl = ""
|
|
135
|
-
|
|
136
|
+
type role = Reventless.Role.name
|
|
137
|
+
let commandAuthorization = (_: command): Reventless.Authorization.rule<role> => AllowAuthenticated
|
|
136
138
|
type lifecycleState = unit
|
|
137
139
|
let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
|
|
138
140
|
}
|
|
@@ -201,7 +203,8 @@ module SyncDelegate = {
|
|
|
201
203
|
type error = NoError
|
|
202
204
|
|
|
203
205
|
let moduleUrl = ""
|
|
204
|
-
|
|
206
|
+
type role = Reventless.Role.name
|
|
207
|
+
let commandAuthorization = (_: command): Reventless.Authorization.rule<role> => AllowAuthenticated
|
|
205
208
|
type lifecycleState = unit
|
|
206
209
|
let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
|
|
207
210
|
}
|
|
@@ -279,7 +282,8 @@ module StockDelegate = {
|
|
|
279
282
|
type error = NoError
|
|
280
283
|
|
|
281
284
|
let moduleUrl = ""
|
|
282
|
-
|
|
285
|
+
type role = Reventless.Role.name
|
|
286
|
+
let commandAuthorization = (_: command): Reventless.Authorization.rule<role> => AllowAuthenticated
|
|
283
287
|
type lifecycleState = unit
|
|
284
288
|
let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
|
|
285
289
|
}
|
|
@@ -341,7 +345,8 @@ module SyncedDelegate = {
|
|
|
341
345
|
type error = NoError
|
|
342
346
|
|
|
343
347
|
let moduleUrl = ""
|
|
344
|
-
|
|
348
|
+
type role = Reventless.Role.name
|
|
349
|
+
let commandAuthorization = (_: command): Reventless.Authorization.rule<role> => AllowAuthenticated
|
|
345
350
|
type lifecycleState = unit
|
|
346
351
|
let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
|
|
347
352
|
}
|
|
@@ -80,7 +80,8 @@ module ProductDelegate = {
|
|
|
80
80
|
@schema type event = ProductAdded({productId: string, name: string, price: float})
|
|
81
81
|
@schema type error = NoError
|
|
82
82
|
let moduleUrl = ""
|
|
83
|
-
|
|
83
|
+
type role = Reventless.Role.name
|
|
84
|
+
let commandAuthorization = (_: command): Reventless.Authorization.rule<role> => AllowAuthenticated
|
|
84
85
|
type lifecycleState = unit
|
|
85
86
|
let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
|
|
86
87
|
}
|
|
@@ -119,7 +120,8 @@ module OrderDelegate = {
|
|
|
119
120
|
@schema type event = OrderPlaced({orderId: string, productIds: array<string>})
|
|
120
121
|
@schema type error = NoError
|
|
121
122
|
let moduleUrl = ""
|
|
122
|
-
|
|
123
|
+
type role = Reventless.Role.name
|
|
124
|
+
let commandAuthorization = (_: command): Reventless.Authorization.rule<role> => AllowAuthenticated
|
|
123
125
|
type lifecycleState = unit
|
|
124
126
|
let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
|
|
125
127
|
}
|
|
@@ -156,7 +158,8 @@ module RecordDemandSlice = {
|
|
|
156
158
|
type event =
|
|
157
159
|
DemandRecorded({productId: @s.matches(Reventless.DcbTag.string) string, orderId: string})
|
|
158
160
|
let moduleUrl = ""
|
|
159
|
-
|
|
161
|
+
type role = Reventless.Role.name
|
|
162
|
+
let commandAuthorization = (_: command): Reventless.Authorization.rule<role> => AllowAuthenticated
|
|
160
163
|
type lifecycleState = unit
|
|
161
164
|
let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
|
|
162
165
|
}
|
|
@@ -216,7 +219,8 @@ module SyncProductSlice = {
|
|
|
216
219
|
price: float,
|
|
217
220
|
})
|
|
218
221
|
let moduleUrl = ""
|
|
219
|
-
|
|
222
|
+
type role = Reventless.Role.name
|
|
223
|
+
let commandAuthorization = (_: command): Reventless.Authorization.rule<role> => AllowAuthenticated
|
|
220
224
|
type lifecycleState = unit
|
|
221
225
|
let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
|
|
222
226
|
}
|
|
@@ -691,7 +691,8 @@ let Sync = Flow_GWT$ReventlessGwt.CommandStep({
|
|
|
691
691
|
consumedEventSchema: consumedEventSchema$2,
|
|
692
692
|
commandSchema: commandSchema$6,
|
|
693
693
|
errorSchema: errorSchema$4,
|
|
694
|
-
eventSchema: eventSchema$6
|
|
694
|
+
eventSchema: eventSchema$6,
|
|
695
|
+
commandAuthorization: commandAuthorization$6
|
|
695
696
|
})({
|
|
696
697
|
initialState: initialState$2,
|
|
697
698
|
evolve: evolve$2,
|
|
@@ -762,7 +763,8 @@ let Demand = Flow_GWT$ReventlessGwt.CommandStep({
|
|
|
762
763
|
consumedEventSchema: consumedEventSchema$1,
|
|
763
764
|
commandSchema: commandSchema$5,
|
|
764
765
|
errorSchema: errorSchema$3,
|
|
765
|
-
eventSchema: eventSchema$5
|
|
766
|
+
eventSchema: eventSchema$5,
|
|
767
|
+
commandAuthorization: commandAuthorization$5
|
|
766
768
|
})({
|
|
767
769
|
initialState: initialState$1,
|
|
768
770
|
evolve: evolve$1,
|
|
@@ -39,6 +39,10 @@ let Spec = include.Spec;
|
|
|
39
39
|
|
|
40
40
|
let todo = include.todo;
|
|
41
41
|
|
|
42
|
+
let Caller = include.Caller;
|
|
43
|
+
|
|
44
|
+
let asCaller = include.asCaller;
|
|
45
|
+
|
|
42
46
|
let thenEvents = include.thenEvents;
|
|
43
47
|
|
|
44
48
|
let thenNoEvent = include.thenNoEvent;
|
|
@@ -47,6 +51,8 @@ let thenEventWithError = include.thenEventWithError;
|
|
|
47
51
|
|
|
48
52
|
let thenEventsWithError = include.thenEventsWithError;
|
|
49
53
|
|
|
54
|
+
let thenRefused = include.thenRefused;
|
|
55
|
+
|
|
50
56
|
let thenAppendsConditionedOn = include.thenAppendsConditionedOn;
|
|
51
57
|
|
|
52
58
|
let thenAppendsConditionedOnExactly = include.thenAppendsConditionedOnExactly;
|
|
@@ -57,6 +63,8 @@ export {
|
|
|
57
63
|
todo,
|
|
58
64
|
test,
|
|
59
65
|
givenEvents,
|
|
66
|
+
Caller,
|
|
67
|
+
asCaller,
|
|
60
68
|
whenCmd,
|
|
61
69
|
thenEvent,
|
|
62
70
|
thenEvents,
|
|
@@ -64,6 +72,7 @@ export {
|
|
|
64
72
|
thenEventWithError,
|
|
65
73
|
thenEventsWithError,
|
|
66
74
|
thenError,
|
|
75
|
+
thenRefused,
|
|
67
76
|
thenAppendsConditionedOn,
|
|
68
77
|
thenAppendsConditionedOnExactly,
|
|
69
78
|
}
|
|
@@ -23,6 +23,10 @@ let Spec = include.Spec;
|
|
|
23
23
|
|
|
24
24
|
let todo = include.todo;
|
|
25
25
|
|
|
26
|
+
let Caller = include.Caller;
|
|
27
|
+
|
|
28
|
+
let asCaller = include.asCaller;
|
|
29
|
+
|
|
26
30
|
let thenEvents = include.thenEvents;
|
|
27
31
|
|
|
28
32
|
let thenNoEvent = include.thenNoEvent;
|
|
@@ -33,6 +37,8 @@ let thenEventsWithError = include.thenEventsWithError;
|
|
|
33
37
|
|
|
34
38
|
let thenError = include.thenError;
|
|
35
39
|
|
|
40
|
+
let thenRefused = include.thenRefused;
|
|
41
|
+
|
|
36
42
|
let thenAppendsConditionedOn = include.thenAppendsConditionedOn;
|
|
37
43
|
|
|
38
44
|
let thenAppendsConditionedOnExactly = include.thenAppendsConditionedOnExactly;
|
|
@@ -43,6 +49,8 @@ export {
|
|
|
43
49
|
todo,
|
|
44
50
|
test,
|
|
45
51
|
givenEvents,
|
|
52
|
+
Caller,
|
|
53
|
+
asCaller,
|
|
46
54
|
whenCmd,
|
|
47
55
|
thenEvent,
|
|
48
56
|
thenEvents,
|
|
@@ -50,6 +58,7 @@ export {
|
|
|
50
58
|
thenEventWithError,
|
|
51
59
|
thenEventsWithError,
|
|
52
60
|
thenError,
|
|
61
|
+
thenRefused,
|
|
53
62
|
thenAppendsConditionedOn,
|
|
54
63
|
thenAppendsConditionedOnExactly,
|
|
55
64
|
}
|
|
@@ -23,6 +23,10 @@ let Spec = include.Spec;
|
|
|
23
23
|
|
|
24
24
|
let todo = include.todo;
|
|
25
25
|
|
|
26
|
+
let Caller = include.Caller;
|
|
27
|
+
|
|
28
|
+
let asCaller = include.asCaller;
|
|
29
|
+
|
|
26
30
|
let thenEvents = include.thenEvents;
|
|
27
31
|
|
|
28
32
|
let thenNoEvent = include.thenNoEvent;
|
|
@@ -33,6 +37,8 @@ let thenEventsWithError = include.thenEventsWithError;
|
|
|
33
37
|
|
|
34
38
|
let thenError = include.thenError;
|
|
35
39
|
|
|
40
|
+
let thenRefused = include.thenRefused;
|
|
41
|
+
|
|
36
42
|
let thenAppendsConditionedOn = include.thenAppendsConditionedOn;
|
|
37
43
|
|
|
38
44
|
let thenAppendsConditionedOnExactly = include.thenAppendsConditionedOnExactly;
|
|
@@ -43,6 +49,8 @@ export {
|
|
|
43
49
|
todo,
|
|
44
50
|
test,
|
|
45
51
|
givenEvents,
|
|
52
|
+
Caller,
|
|
53
|
+
asCaller,
|
|
46
54
|
whenCmd,
|
|
47
55
|
thenEvent,
|
|
48
56
|
thenEvents,
|
|
@@ -50,6 +58,7 @@ export {
|
|
|
50
58
|
thenEventWithError,
|
|
51
59
|
thenEventsWithError,
|
|
52
60
|
thenError,
|
|
61
|
+
thenRefused,
|
|
53
62
|
thenAppendsConditionedOn,
|
|
54
63
|
thenAppendsConditionedOnExactly,
|
|
55
64
|
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// `asCaller` with roles: a command's `@authorize` rule is checked against the
|
|
2
|
+
// caller's roles before `decide`, and before the ownership rule, as production
|
|
3
|
+
// checks them. A scenario without `asCaller` is checked against neither.
|
|
4
|
+
|
|
5
|
+
@@reventless.gwt
|
|
6
|
+
|
|
7
|
+
module ShelveSlice = {
|
|
8
|
+
let name = "Shelve"
|
|
9
|
+
|
|
10
|
+
// Inside the spec, so the zero-argument form still finds the spec and its
|
|
11
|
+
// behaviour first; the rule's `Roles` resolves by ordinary scoping either way.
|
|
12
|
+
module Roles = {
|
|
13
|
+
type t =
|
|
14
|
+
| Admin
|
|
15
|
+
| Merchandiser
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@schema
|
|
19
|
+
type consumedEvent = ShelfOpened({shelfId: @s.matches(Reventless.DcbTag.string) string})
|
|
20
|
+
|
|
21
|
+
@schema
|
|
22
|
+
type command =
|
|
23
|
+
| @authorize(AllowRoles([Admin, Merchandiser]))
|
|
24
|
+
Shelve({
|
|
25
|
+
shelfId: @s.matches(Reventless.DcbTag.string) string,
|
|
26
|
+
})
|
|
27
|
+
| Browse({shelfId: @s.matches(Reventless.DcbTag.string) string})
|
|
28
|
+
| @authorize(AllowAnonymous) Peek({shelfId: @s.matches(Reventless.DcbTag.string) string})
|
|
29
|
+
| @authorize(DenyAll) Purge({shelfId: @s.matches(Reventless.DcbTag.string) string})
|
|
30
|
+
|
|
31
|
+
@schema
|
|
32
|
+
type error = unit
|
|
33
|
+
|
|
34
|
+
@schema
|
|
35
|
+
type event = Done({shelfId: @s.matches(Reventless.DcbTag.string) string})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module ShelveSliceBehavior = {
|
|
39
|
+
module Spec = ShelveSlice
|
|
40
|
+
open ShelveSlice
|
|
41
|
+
|
|
42
|
+
type state = unit
|
|
43
|
+
let initialState = ()
|
|
44
|
+
let evolve = (_state, _event: consumedEvent) => ()
|
|
45
|
+
let decide = (_state, command: command): result<array<event>, error> =>
|
|
46
|
+
switch command {
|
|
47
|
+
| Shelve({shelfId}) | Browse({shelfId}) | Peek({shelfId}) | Purge({shelfId}) =>
|
|
48
|
+
Ok([Done({shelfId: shelfId})])
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let s1 = "s1"
|
|
53
|
+
|
|
54
|
+
describe("a command's rule, checked against the caller's roles", () => {
|
|
55
|
+
test("accepted for each role the rule names: Admin", () =>
|
|
56
|
+
givenEvents([])
|
|
57
|
+
->asCaller(Caller.inRoles([Admin]))
|
|
58
|
+
->whenCmd(Shelve({shelfId: s1}))
|
|
59
|
+
->thenEvent(Done({shelfId: s1}))
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
test("accepted for each role the rule names: Merchandiser", () =>
|
|
63
|
+
givenEvents([])
|
|
64
|
+
->asCaller(Caller.inRoles([Merchandiser]))
|
|
65
|
+
->whenCmd(Shelve({shelfId: s1}))
|
|
66
|
+
->thenEvent(Done({shelfId: s1}))
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
test("refused for a signed-in caller holding none of them", () =>
|
|
70
|
+
givenEvents([])->asCaller(Caller.owner("c1"))->whenCmd(Shelve({shelfId: s1}))->thenRefused
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
test("refused for an anonymous caller", () =>
|
|
74
|
+
givenEvents([])->asCaller(Caller.anonymous)->whenCmd(Shelve({shelfId: s1}))->thenRefused
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
// Elevation lifts ownership rules, not rules about who may issue a command.
|
|
78
|
+
test("refused for an operator, who holds no role", () =>
|
|
79
|
+
givenEvents([])->asCaller(Caller.operator)->whenCmd(Shelve({shelfId: s1}))->thenRefused
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
test("DenyAll refuses every caller, Admin included", () =>
|
|
83
|
+
givenEvents([])
|
|
84
|
+
->asCaller(Caller.inRoles([Admin]))
|
|
85
|
+
->whenCmd(Purge({shelfId: s1}))
|
|
86
|
+
->thenRefused
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
test("AllowAnonymous accepts an anonymous caller", () =>
|
|
90
|
+
givenEvents([])
|
|
91
|
+
->asCaller(Caller.anonymous)
|
|
92
|
+
->whenCmd(Peek({shelfId: s1}))
|
|
93
|
+
->thenEvent(Done({shelfId: s1}))
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
test("the default rule refuses an anonymous caller", () =>
|
|
97
|
+
givenEvents([])->asCaller(Caller.anonymous)->whenCmd(Browse({shelfId: s1}))->thenRefused
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
test("a scenario without asCaller is checked against no rule", () =>
|
|
101
|
+
givenEvents([])->whenCmd(Purge({shelfId: s1}))->thenEvent(Done({shelfId: s1}))
|
|
102
|
+
)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// The same command on a slice whose history records an owner, through the same
|
|
106
|
+
// functor the attribute includes, to show the two checks in production's order.
|
|
107
|
+
module ClaimSlice = {
|
|
108
|
+
let name = "Claim"
|
|
109
|
+
|
|
110
|
+
module Roles = {
|
|
111
|
+
type t = Merchandiser
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@schema
|
|
115
|
+
type consumedEvent =
|
|
116
|
+
| ShelfClaimed({
|
|
117
|
+
shelfId: @s.matches(Reventless.DcbTag.string) string,
|
|
118
|
+
owner: @s.matches(Reventless.Owner.string) string,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
@schema
|
|
122
|
+
type command =
|
|
123
|
+
| @authorize(AllowRoles([Merchandiser]))
|
|
124
|
+
Shelve({
|
|
125
|
+
shelfId: @s.matches(Reventless.DcbTag.string) string,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
@schema
|
|
129
|
+
type error = unit
|
|
130
|
+
|
|
131
|
+
@schema
|
|
132
|
+
type event = Done({shelfId: @s.matches(Reventless.DcbTag.string) string})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module Claim = Behavior_GWT.Make(
|
|
136
|
+
ClaimSlice,
|
|
137
|
+
{
|
|
138
|
+
type state = unit
|
|
139
|
+
let initialState = ()
|
|
140
|
+
let evolve = (_state, _event: ClaimSlice.consumedEvent) => ()
|
|
141
|
+
let decide = (_state, command: ClaimSlice.command): result<
|
|
142
|
+
array<ClaimSlice.event>,
|
|
143
|
+
ClaimSlice.error,
|
|
144
|
+
> =>
|
|
145
|
+
switch command {
|
|
146
|
+
| Shelve({shelfId}) => Ok([Done({shelfId: shelfId})])
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
let claimedByC1 = [ClaimSlice.ShelfClaimed({shelfId: s1, owner: "c1"})]
|
|
152
|
+
|
|
153
|
+
describe("the rule, then ownership", () => {
|
|
154
|
+
Claim.test("an owner holding the role acts on what they own", () =>
|
|
155
|
+
Claim.givenEvents(claimedByC1)
|
|
156
|
+
->Claim.asCaller(Claim.Caller.owner("c1", ~roles=[Merchandiser]))
|
|
157
|
+
->Claim.whenCmd(Shelve({shelfId: s1}))
|
|
158
|
+
->Claim.thenEvent(Done({shelfId: s1}))
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
Claim.test("passing the rule does not let a caller act on someone else's thing", () =>
|
|
162
|
+
Claim.givenEvents(claimedByC1)
|
|
163
|
+
->Claim.asCaller(Claim.Caller.owner("c2", ~roles=[Merchandiser]))
|
|
164
|
+
->Claim.whenCmd(Shelve({shelfId: s1}))
|
|
165
|
+
->Claim.thenRefused
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
Claim.test("owning a thing does not pass a rule the owner fails", () =>
|
|
169
|
+
Claim.givenEvents(claimedByC1)
|
|
170
|
+
->Claim.asCaller(Claim.Caller.owner("c1"))
|
|
171
|
+
->Claim.whenCmd(Shelve({shelfId: s1}))
|
|
172
|
+
->Claim.thenRefused
|
|
173
|
+
)
|
|
174
|
+
})
|