stealth 0.9.8 → 0.10.0
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.
- checksums.yaml +4 -4
- data/Gemfile.lock +5 -3
- data/VERSION +1 -1
- data/lib/stealth/base.rb +3 -1
- data/lib/stealth/controller/callbacks.rb +64 -0
- data/lib/stealth/controller/catch_all.rb +54 -0
- data/lib/stealth/{controller.rb → controller/controller.rb} +28 -20
- data/lib/stealth/errors.rb +6 -0
- data/lib/stealth/flow/base.rb +17 -211
- data/lib/stealth/flow/core_ext.rb +11 -0
- data/lib/stealth/flow/specification.rb +12 -45
- data/lib/stealth/flow/state.rb +49 -19
- data/lib/stealth/session.rb +50 -7
- data/spec/configuration_spec.rb +2 -2
- data/spec/controller/callbacks_spec.rb +225 -0
- data/spec/controller/state_transitions_spec.rb +108 -0
- data/spec/flow/flow_spec.rb +13 -43
- data/spec/flow/state_spec.rb +71 -0
- data/spec/session_spec.rb +116 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/support/sample_messages.rb +65 -0
- data/spec/{sample_services_yml → support}/services.yml +0 -0
- data/spec/{sample_services_yml → support}/services_with_erb.yml +0 -0
- data/stealth.gemspec +1 -0
- metadata +35 -15
- data/lib/stealth/flow/errors.rb +0 -25
- data/lib/stealth/flow/event.rb +0 -43
- data/lib/stealth/flow/event_collection.rb +0 -41
- data/spec/flow/custom_transitions_spec.rb +0 -99
- data/spec/flow/transition_callbacks_spec.rb +0 -228
data/lib/stealth/flow/state.rb
CHANGED
@@ -7,33 +7,51 @@ module Stealth
|
|
7
7
|
|
8
8
|
include Comparable
|
9
9
|
|
10
|
-
attr_accessor :name
|
11
|
-
attr_reader :spec
|
10
|
+
attr_accessor :name
|
11
|
+
attr_reader :spec, :fails_to
|
12
12
|
|
13
|
-
def initialize(name, spec,
|
14
|
-
|
13
|
+
def initialize(name, spec, fails_to = nil)
|
14
|
+
if fails_to.present? && !fails_to.is_a?(Stealth::Flow::State)
|
15
|
+
raise(ArgumentError, 'fails_to state should be a Stealth::Flow::State')
|
16
|
+
end
|
17
|
+
|
18
|
+
@name, @spec, @fails_to = name, spec, fails_to
|
15
19
|
end
|
16
20
|
|
17
|
-
def
|
18
|
-
|
19
|
-
|
20
|
-
:width => '1',
|
21
|
-
:height => '1',
|
22
|
-
:shape => 'ellipse'
|
23
|
-
}
|
21
|
+
def <=>(other_state)
|
22
|
+
state_position(self) <=> state_position(other_state)
|
23
|
+
end
|
24
24
|
|
25
|
-
|
25
|
+
def +(steps)
|
26
|
+
if steps < 0
|
27
|
+
new_position = state_position(self) + steps
|
26
28
|
|
27
|
-
|
28
|
-
|
29
|
+
# we don't want to allow the array index to wrap here so we return
|
30
|
+
# the first state instead
|
31
|
+
if new_position < 0
|
32
|
+
new_state = spec.states.keys.first
|
33
|
+
else
|
34
|
+
new_state = spec.states.keys.at(new_position)
|
35
|
+
end
|
36
|
+
else
|
37
|
+
new_state = spec.states.keys[state_position(self) + steps]
|
29
38
|
|
30
|
-
|
39
|
+
# we may have been told to access an out-of-bounds state
|
40
|
+
# return the last state
|
41
|
+
if new_state.blank?
|
42
|
+
new_state = spec.states.keys.last
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
new_state
|
31
47
|
end
|
32
48
|
|
33
|
-
def
|
34
|
-
|
35
|
-
|
36
|
-
|
49
|
+
def -(steps)
|
50
|
+
if steps < 0
|
51
|
+
return self + steps.abs
|
52
|
+
else
|
53
|
+
return self + (-steps)
|
54
|
+
end
|
37
55
|
end
|
38
56
|
|
39
57
|
def to_s
|
@@ -43,6 +61,18 @@ module Stealth
|
|
43
61
|
def to_sym
|
44
62
|
name.to_sym
|
45
63
|
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def state_position(state)
|
68
|
+
states = spec.states.keys
|
69
|
+
|
70
|
+
unless states.include?(state.to_sym)
|
71
|
+
raise(ArgumentError, "state `#{state}' does not exist")
|
72
|
+
end
|
73
|
+
|
74
|
+
states.index(state.to_sym)
|
75
|
+
end
|
46
76
|
end
|
47
77
|
end
|
48
78
|
end
|
data/lib/stealth/session.rb
CHANGED
@@ -6,12 +6,14 @@ module Stealth
|
|
6
6
|
|
7
7
|
SLUG_SEPARATOR = '->'
|
8
8
|
|
9
|
-
attr_reader :
|
9
|
+
attr_reader :flow, :state, :user_id, :previous
|
10
|
+
attr_accessor :session
|
10
11
|
|
11
|
-
def initialize(user_id:)
|
12
|
+
def initialize(user_id:, previous: false)
|
12
13
|
@user_id = user_id
|
14
|
+
@previous = previous
|
13
15
|
|
14
|
-
unless defined?($redis)
|
16
|
+
unless defined?($redis) && $redis.present?
|
15
17
|
raise(Stealth::Errors::RedisNotConfigured, "Please make sure REDIS_URL is configured before using sessions")
|
16
18
|
end
|
17
19
|
|
@@ -27,7 +29,9 @@ module Stealth
|
|
27
29
|
end
|
28
30
|
|
29
31
|
def flow
|
30
|
-
|
32
|
+
return nil if flow_string.blank?
|
33
|
+
|
34
|
+
@flow ||= begin
|
31
35
|
flow_klass = [flow_string, 'flow'].join('_').classify.constantize
|
32
36
|
flow = flow_klass.new.init_state(state_string)
|
33
37
|
flow
|
@@ -35,7 +39,7 @@ module Stealth
|
|
35
39
|
end
|
36
40
|
|
37
41
|
def state
|
38
|
-
flow
|
42
|
+
flow&.current_state
|
39
43
|
end
|
40
44
|
|
41
45
|
def flow_string
|
@@ -47,12 +51,18 @@ module Stealth
|
|
47
51
|
end
|
48
52
|
|
49
53
|
def get
|
50
|
-
|
54
|
+
if previous?
|
55
|
+
@session ||= $redis.get(previous_session_key(user_id: user_id))
|
56
|
+
else
|
57
|
+
@session ||= $redis.get(user_id)
|
58
|
+
end
|
51
59
|
end
|
52
60
|
|
53
61
|
def set(flow:, state:)
|
62
|
+
store_current_to_previous
|
63
|
+
|
64
|
+
@flow = nil
|
54
65
|
@session = canonical_session_slug(flow: flow, state: state)
|
55
|
-
flow
|
56
66
|
$redis.set(user_id, session)
|
57
67
|
end
|
58
68
|
|
@@ -64,11 +74,44 @@ module Stealth
|
|
64
74
|
!present?
|
65
75
|
end
|
66
76
|
|
77
|
+
def previous?
|
78
|
+
@previous
|
79
|
+
end
|
80
|
+
|
81
|
+
def +(steps)
|
82
|
+
return nil if flow.blank?
|
83
|
+
return self if steps.zero?
|
84
|
+
|
85
|
+
new_state = self.state + steps
|
86
|
+
new_session = Stealth::Session.new(user_id: self.user_id)
|
87
|
+
new_session.session = canonical_session_slug(flow: self.flow_string, state: new_state)
|
88
|
+
|
89
|
+
new_session
|
90
|
+
end
|
91
|
+
|
92
|
+
def -(steps)
|
93
|
+
return nil if flow.blank?
|
94
|
+
|
95
|
+
if steps < 0
|
96
|
+
return self + steps.abs
|
97
|
+
else
|
98
|
+
return self + (-steps)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
67
102
|
private
|
68
103
|
|
69
104
|
def canonical_session_slug(flow:, state:)
|
70
105
|
[flow, state].join(SLUG_SEPARATOR)
|
71
106
|
end
|
72
107
|
|
108
|
+
def previous_session_key(user_id:)
|
109
|
+
[user_id, 'previous'].join('-')
|
110
|
+
end
|
111
|
+
|
112
|
+
def store_current_to_previous
|
113
|
+
$redis.set(previous_session_key(user_id: user_id), session)
|
114
|
+
end
|
115
|
+
|
73
116
|
end
|
74
117
|
end
|
data/spec/configuration_spec.rb
CHANGED
@@ -5,7 +5,7 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
|
5
5
|
describe "Stealth::Configuration" do
|
6
6
|
|
7
7
|
describe "accessing via method calling" do
|
8
|
-
let(:services_yml) { File.read(File.join(File.dirname(__FILE__), '
|
8
|
+
let(:services_yml) { File.read(File.join(File.dirname(__FILE__), 'support', 'services.yml')) }
|
9
9
|
let(:parsed_config) { YAML.load(ERB.new(services_yml).result)[Stealth.env] }
|
10
10
|
let(:config) { Stealth.load_services_config!(services_yml) }
|
11
11
|
|
@@ -31,7 +31,7 @@ describe "Stealth::Configuration" do
|
|
31
31
|
end
|
32
32
|
|
33
33
|
describe "config files with ERB" do
|
34
|
-
let(:services_yml) { File.read(File.join(File.dirname(__FILE__), '
|
34
|
+
let(:services_yml) { File.read(File.join(File.dirname(__FILE__), 'support', 'services_with_erb.yml')) }
|
35
35
|
let(:config) { Stealth.load_services_config!(services_yml) }
|
36
36
|
|
37
37
|
it "should replace available embedded env vars" do
|
@@ -0,0 +1,225 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', '/spec_helper'))
|
4
|
+
|
5
|
+
class BotController < Stealth::Controller
|
6
|
+
before_action :fetch_user_name
|
7
|
+
|
8
|
+
attr_accessor :record
|
9
|
+
|
10
|
+
def some_action
|
11
|
+
@record = []
|
12
|
+
step_to flow: 'flow_tester', state: 'my_action'
|
13
|
+
end
|
14
|
+
|
15
|
+
def other_action
|
16
|
+
@record = []
|
17
|
+
step_to flow: 'other_flow_tester', state: 'other_action'
|
18
|
+
end
|
19
|
+
|
20
|
+
def halted_action
|
21
|
+
@record = []
|
22
|
+
step_to flow: 'flow_tester', state: 'my_action2'
|
23
|
+
end
|
24
|
+
|
25
|
+
def filtered_action
|
26
|
+
@record = []
|
27
|
+
step_to flow: 'flow_tester', state: 'my_action3'
|
28
|
+
end
|
29
|
+
|
30
|
+
def some_other_action2
|
31
|
+
@record = []
|
32
|
+
step_to flow: 'other_flow_tester', state: 'other_action2'
|
33
|
+
end
|
34
|
+
|
35
|
+
def some_other_action3
|
36
|
+
@record = []
|
37
|
+
step_to flow: 'other_flow_tester', state: 'other_action3'
|
38
|
+
end
|
39
|
+
|
40
|
+
def some_other_action4
|
41
|
+
@record = []
|
42
|
+
step_to flow: 'other_flow_tester', state: 'other_action4'
|
43
|
+
end
|
44
|
+
|
45
|
+
def some_other_action5
|
46
|
+
@record = []
|
47
|
+
step_to flow: 'other_flow_tester', state: 'other_action5'
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def fetch_user_name
|
53
|
+
@record << "fetched user name"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class FlowTestersController < BotController
|
58
|
+
before_action :test_before_halting, only: :my_action2
|
59
|
+
before_action :test_action
|
60
|
+
before_action :test_filtering, except: [:my_action, :my_action2]
|
61
|
+
|
62
|
+
attr_reader :action_ran
|
63
|
+
|
64
|
+
def my_action
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
def my_action2
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
def my_action3
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
protected
|
77
|
+
|
78
|
+
def test_action
|
79
|
+
@record << "tested action"
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_before_halting
|
83
|
+
throw(:abort)
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_filtering
|
87
|
+
@record << "filtered"
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_after_halting
|
91
|
+
@record << "after action ran"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class OtherFlowTestersController < BotController
|
96
|
+
after_action :after_action1, only: :other_action2
|
97
|
+
after_action :after_action2, only: :other_action2
|
98
|
+
|
99
|
+
before_action :run_halt, only: [:other_action3, :other_action5]
|
100
|
+
after_action :after_action3, only: :other_action3
|
101
|
+
|
102
|
+
around_action :run_around_filter, only: [:other_action4, :other_action5]
|
103
|
+
|
104
|
+
def other_action
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
def other_action2
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
def other_action3
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
def other_action4
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
def other_action5
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def after_action1
|
127
|
+
@record << "after action 1"
|
128
|
+
end
|
129
|
+
|
130
|
+
def after_action2
|
131
|
+
@record << "after action 2"
|
132
|
+
end
|
133
|
+
|
134
|
+
def run_halt
|
135
|
+
throw(:abort)
|
136
|
+
end
|
137
|
+
|
138
|
+
def run_around_filter
|
139
|
+
@record << "around before"
|
140
|
+
yield
|
141
|
+
@record << "around after"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
class FlowTesterFlow
|
146
|
+
include Stealth::Flow
|
147
|
+
|
148
|
+
flow do
|
149
|
+
state :my_action
|
150
|
+
state :my_action2
|
151
|
+
state :my_action3
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
class OtherFlowTesterFlow
|
156
|
+
include Stealth::Flow
|
157
|
+
|
158
|
+
flow do
|
159
|
+
state :other_action
|
160
|
+
state :other_action2
|
161
|
+
state :other_action3
|
162
|
+
state :other_action4
|
163
|
+
state :other_action5
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
describe "Stealth::Controller callbacks" do
|
168
|
+
|
169
|
+
let(:facebook_message) { SampleMessage.new(service: 'facebook') }
|
170
|
+
|
171
|
+
describe "before_action" do
|
172
|
+
it "should fire the callback on the parent class" do
|
173
|
+
controller = BotController.new(service_message: facebook_message.message_with_text)
|
174
|
+
controller.other_action
|
175
|
+
expect(controller.record).to eq ["fetched user name"]
|
176
|
+
end
|
177
|
+
|
178
|
+
it "should fire the callback on a child class" do
|
179
|
+
controller = FlowTestersController.new(service_message: facebook_message.message_with_text)
|
180
|
+
controller.some_action
|
181
|
+
expect(controller.record).to eq ["fetched user name", "tested action"]
|
182
|
+
end
|
183
|
+
|
184
|
+
it "should halt the callback chain when :abort is thrown" do
|
185
|
+
controller = FlowTestersController.new(service_message: facebook_message.message_with_text)
|
186
|
+
controller.halted_action
|
187
|
+
expect(controller.record).to eq ["fetched user name"]
|
188
|
+
end
|
189
|
+
|
190
|
+
it "should respect 'unless' filter" do
|
191
|
+
controller = FlowTestersController.new(service_message: facebook_message.message_with_text)
|
192
|
+
controller.filtered_action
|
193
|
+
expect(controller.record).to eq ["fetched user name", "tested action", "filtered"]
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
describe "after_action" do
|
198
|
+
it "should fire the after callbacks in reverse order" do
|
199
|
+
controller = OtherFlowTestersController.new(service_message: facebook_message.message_with_text)
|
200
|
+
controller.some_other_action2
|
201
|
+
expect(controller.record).to eq ["fetched user name", "after action 2", "after action 1"]
|
202
|
+
end
|
203
|
+
|
204
|
+
it "should not fire after callbacks if a before callback throws an :abort" do
|
205
|
+
controller = OtherFlowTestersController.new(service_message: facebook_message.message_with_text)
|
206
|
+
controller.some_other_action3
|
207
|
+
expect(controller.record).to eq ["fetched user name"]
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
describe "around_action" do
|
212
|
+
it "should fire the around callback before and after" do
|
213
|
+
controller = OtherFlowTestersController.new(service_message: facebook_message.message_with_text)
|
214
|
+
controller.some_other_action4
|
215
|
+
expect(controller.record).to eq ["fetched user name", "around before", "around after"]
|
216
|
+
end
|
217
|
+
|
218
|
+
it "should not fire the around callback if a before callback throws abort" do
|
219
|
+
controller = OtherFlowTestersController.new(service_message: facebook_message.message_with_text)
|
220
|
+
controller.some_other_action5
|
221
|
+
expect(controller.record).to eq ["fetched user name"]
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', '/spec_helper'))
|
4
|
+
|
5
|
+
describe "Stealth::Controller state transitions" do
|
6
|
+
|
7
|
+
class MrRobotsController < BotController
|
8
|
+
def my_action
|
9
|
+
[:success, :my_action]
|
10
|
+
end
|
11
|
+
|
12
|
+
def my_action2
|
13
|
+
[:success, :my_action2]
|
14
|
+
end
|
15
|
+
|
16
|
+
def my_action3
|
17
|
+
[:success, :my_action3]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class MrTronsController < BotController
|
22
|
+
def other_action
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
def other_action2
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
def other_action3
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class MrRobotFlow
|
36
|
+
include Stealth::Flow
|
37
|
+
|
38
|
+
flow do
|
39
|
+
state :my_action
|
40
|
+
state :my_action2
|
41
|
+
state :my_action3
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class MrTronFlow
|
46
|
+
include Stealth::Flow
|
47
|
+
|
48
|
+
flow do
|
49
|
+
state :other_action
|
50
|
+
state :other_action2
|
51
|
+
state :other_action3
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
let(:facebook_message) { SampleMessage.new(service: 'facebook') }
|
56
|
+
|
57
|
+
describe "step_to" do
|
58
|
+
it "should raise an ArgumentError if a session, flow, or state is not specified" do
|
59
|
+
controller = MrTronsController.new(service_message: facebook_message.message_with_text)
|
60
|
+
expect {
|
61
|
+
controller.step_to
|
62
|
+
}.to raise_error(ArgumentError)
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should call the flow's first state's controller action when only a flow is provided" do
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should call a controller's corresponding action when only a state is provided" do
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should call a controller's corresponding action when a state and flow is provided" do
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should call a controller's corresponding action when a session is provided" do
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe "update_session_to" do
|
83
|
+
it "should raise an ArgumentError if a session, flow, or state is not specified" do
|
84
|
+
controller = MrTronsController.new(service_message: facebook_message.message_with_text)
|
85
|
+
expect {
|
86
|
+
controller.update_session_to
|
87
|
+
}.to raise_error(ArgumentError)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "step_to_in" do
|
92
|
+
it "should raise an ArgumentError if a session, flow, or state is not specified" do
|
93
|
+
controller = MrTronsController.new(service_message: facebook_message.message_with_text)
|
94
|
+
expect {
|
95
|
+
controller.step_to_in
|
96
|
+
}.to raise_error(ArgumentError)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe "step_to_next" do
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
describe "update_session_to_next" do
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
data/spec/flow/flow_spec.rb
CHANGED
@@ -8,67 +8,37 @@ describe Stealth::Flow do
|
|
8
8
|
include Stealth::Flow
|
9
9
|
|
10
10
|
flow do
|
11
|
-
state :new
|
12
|
-
event :submit_todo, :transitions_to => :get_due_date
|
13
|
-
event :error_in_input, :transitions_to => :error
|
14
|
-
end
|
11
|
+
state :new
|
15
12
|
|
16
|
-
state :get_due_date
|
17
|
-
event :submit_due_date, :transitions_to => :created
|
18
|
-
end
|
13
|
+
state :get_due_date
|
19
14
|
|
20
15
|
state :created
|
21
16
|
|
22
|
-
state :error
|
23
|
-
event :submit_todo, :transitions_to => :get_due_date
|
24
|
-
event :error_in_input, :transitions_to => :error
|
25
|
-
end
|
17
|
+
state :error
|
26
18
|
end
|
27
19
|
end
|
28
20
|
|
29
21
|
let(:flow) { NewTodoFlow.new }
|
30
22
|
|
31
|
-
describe "
|
32
|
-
it "should
|
33
|
-
|
34
|
-
end
|
35
|
-
|
36
|
-
it "should transition into the 'get_due_date' state after submit" do
|
37
|
-
flow.submit_todo!
|
38
|
-
expect(flow.current_state).to eq :get_due_date
|
39
|
-
end
|
40
|
-
|
41
|
-
it "should transition into the 'error' state after error_in_input" do
|
42
|
-
flow.error_in_input!
|
43
|
-
expect(flow.current_state).to eq :error
|
44
|
-
end
|
45
|
-
|
46
|
-
it "should transition through multiple states" do
|
47
|
-
flow.submit_todo!
|
48
|
-
flow.submit_due_date!
|
23
|
+
describe "inititating with states" do
|
24
|
+
it "should init a state given a state name" do
|
25
|
+
flow.init_state(:created)
|
49
26
|
expect(flow.current_state).to eq :created
|
50
|
-
end
|
51
27
|
|
52
|
-
|
53
|
-
flow.error_in_input!
|
54
|
-
expect(flow.current_state).to eq :error
|
55
|
-
flow.error_in_input!
|
28
|
+
flow.init_state('error')
|
56
29
|
expect(flow.current_state).to eq :error
|
57
30
|
end
|
58
31
|
|
59
|
-
it "should
|
60
|
-
expect
|
61
|
-
|
62
|
-
|
63
|
-
it "should be false when checking the possibility of a valid transition" do
|
64
|
-
flow.submit_todo!
|
65
|
-
expect(flow.can_submit_due_date?).to be true
|
32
|
+
it "should raise an error if an invalid state is specified" do
|
33
|
+
expect {
|
34
|
+
flow.init_state(:invalid)
|
35
|
+
}.to raise_error(Stealth::Errors::InvalidStateTransition)
|
66
36
|
end
|
67
37
|
end
|
68
38
|
|
69
39
|
describe "accessing states" do
|
70
|
-
it "should start out in the
|
71
|
-
expect(flow.
|
40
|
+
it "should start out in the initial state" do
|
41
|
+
expect(flow.current_state).to eq :new
|
72
42
|
end
|
73
43
|
|
74
44
|
it "should support comparing states" do
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
4
|
+
|
5
|
+
describe Stealth::Flow::State do
|
6
|
+
|
7
|
+
class NewTodoFlow
|
8
|
+
include Stealth::Flow
|
9
|
+
|
10
|
+
flow do
|
11
|
+
state :new
|
12
|
+
|
13
|
+
state :get_due_date
|
14
|
+
|
15
|
+
state :created, fails_to: :new
|
16
|
+
|
17
|
+
state :error
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
let(:flow) { NewTodoFlow.new }
|
22
|
+
|
23
|
+
describe "flow states" do
|
24
|
+
it "should convert itself to a string" do
|
25
|
+
expect(flow.current_state.to_s).to be_a(String)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should convert itself to a symbol" do
|
29
|
+
expect(flow.current_state.to_sym).to be_a(Symbol)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "fails_to" do
|
34
|
+
it "should be nil for a state that has not specified a fails_to" do
|
35
|
+
expect(flow.current_state.fails_to).to be_nil
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should return the fail_state if a fails_to was specified" do
|
39
|
+
flow.init_state(:created)
|
40
|
+
expect(flow.current_state.fails_to).to be_a(Stealth::Flow::State)
|
41
|
+
expect(flow.current_state.fails_to).to eq :new
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "state incrementing and decrementing" do
|
46
|
+
it "should increment the state" do
|
47
|
+
flow.init_state(:get_due_date)
|
48
|
+
new_state = flow.current_state + 1.state
|
49
|
+
expect(new_state).to eq(:created)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should decrement the state" do
|
53
|
+
flow.init_state(:error)
|
54
|
+
new_state = flow.current_state - 2.states
|
55
|
+
expect(new_state).to eq(:get_due_date)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should return the first state if the decrement is out of bounds" do
|
59
|
+
flow.init_state(:get_due_date)
|
60
|
+
new_state = flow.current_state - 5.states
|
61
|
+
expect(new_state).to eq(:new)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should return the last state if the increment is out of bounds" do
|
65
|
+
flow.init_state(:created)
|
66
|
+
new_state = flow.current_state + 5.states
|
67
|
+
expect(new_state).to eq(:error)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|