@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.
@@ -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
- let commandAuthorization = (_: command): Reventless.Authorization.permission => AllowAuthenticated
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
- let commandAuthorization = (_: command): Reventless.Authorization.permission => AllowAuthenticated
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
- let commandAuthorization = (_: command): Reventless.Authorization.permission => AllowAuthenticated
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
- let commandAuthorization = (_: command): Reventless.Authorization.permission => AllowAuthenticated
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
- let commandAuthorization = (_: command): Reventless.Authorization.permission => AllowAuthenticated
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
- let commandAuthorization = (_: command): Reventless.Authorization.permission => AllowAuthenticated
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
- let commandAuthorization = (_: command): Reventless.Authorization.permission => AllowAuthenticated
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
- let commandAuthorization = (_: command): Reventless.Authorization.permission => AllowAuthenticated
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
- let commandAuthorization = (_: command): Reventless.Authorization.permission => AllowAuthenticated
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
+ })