statesman 7.4.0 → 10.2.3
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/.github/dependabot.yml +7 -0
- data/.github/workflows/tests.yml +106 -0
- data/.gitignore +68 -15
- data/.rubocop.yml +14 -1
- data/.rubocop_todo.yml +37 -28
- data/.ruby-version +1 -0
- data/CHANGELOG.md +87 -6
- data/CONTRIBUTING.md +18 -0
- data/Gemfile +3 -5
- data/README.md +147 -5
- data/lib/generators/statesman/active_record_transition_generator.rb +1 -1
- data/lib/generators/statesman/generator_helpers.rb +11 -3
- data/lib/statesman/adapters/active_record.rb +61 -25
- data/lib/statesman/adapters/active_record_queries.rb +17 -5
- data/lib/statesman/adapters/memory.rb +5 -1
- data/lib/statesman/adapters/type_safe_active_record_queries.rb +21 -0
- data/lib/statesman/exceptions.rb +13 -7
- data/lib/statesman/guard.rb +1 -1
- data/lib/statesman/machine.rb +60 -0
- data/lib/statesman/version.rb +1 -1
- data/lib/statesman.rb +2 -0
- data/lib/tasks/statesman.rake +3 -3
- data/spec/spec_helper.rb +11 -0
- data/spec/statesman/adapters/active_record_queries_spec.rb +33 -9
- data/spec/statesman/adapters/active_record_spec.rb +125 -19
- data/spec/statesman/adapters/shared_examples.rb +3 -2
- data/spec/statesman/adapters/type_safe_active_record_queries_spec.rb +208 -0
- data/spec/statesman/exceptions_spec.rb +16 -1
- data/spec/statesman/machine_spec.rb +181 -13
- data/spec/support/active_record.rb +105 -15
- data/statesman.gemspec +8 -9
- metadata +28 -57
- data/.circleci/config.yml +0 -187
data/lib/tasks/statesman.rake
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
namespace :statesman do
|
4
|
-
desc "Set most_recent to false for old transitions and to true for the "\
|
4
|
+
desc "Set most_recent to false for old transitions and to true for the " \
|
5
5
|
"latest one. Safe to re-run"
|
6
6
|
task :backfill_most_recent, [:parent_model_name] => :environment do |_, args|
|
7
7
|
parent_model_name = args.parent_model_name
|
@@ -56,8 +56,8 @@ namespace :statesman do
|
|
56
56
|
end
|
57
57
|
|
58
58
|
done_models += batch_size
|
59
|
-
puts "Updated #{transition_class.name.pluralize} for "\
|
60
|
-
"#{[done_models, total_models].min}/#{total_models} "\
|
59
|
+
puts "Updated #{transition_class.name.pluralize} for " \
|
60
|
+
"#{[done_models, total_models].min}/#{total_models} " \
|
61
61
|
"#{parent_model_name.pluralize}"
|
62
62
|
end
|
63
63
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -48,6 +48,8 @@ RSpec.configure do |config|
|
|
48
48
|
my_namespace_my_active_record_model_transitions
|
49
49
|
other_active_record_models
|
50
50
|
other_active_record_model_transitions
|
51
|
+
sti_active_record_models
|
52
|
+
sti_active_record_model_transitions
|
51
53
|
]
|
52
54
|
tables.each do |table_name|
|
53
55
|
sql = "DROP TABLE IF EXISTS #{table_name};"
|
@@ -72,6 +74,15 @@ RSpec.configure do |config|
|
|
72
74
|
OtherActiveRecordModelTransition.reset_column_information
|
73
75
|
end
|
74
76
|
|
77
|
+
def prepare_sti_model_table
|
78
|
+
CreateStiActiveRecordModelMigration.migrate(:up)
|
79
|
+
end
|
80
|
+
|
81
|
+
def prepare_sti_transitions_table
|
82
|
+
CreateStiActiveRecordModelTransitionMigration.migrate(:up)
|
83
|
+
StiActiveRecordModelTransition.reset_column_information
|
84
|
+
end
|
85
|
+
|
75
86
|
MyNamespace::MyActiveRecordModelTransition.serialize(:metadata, JSON)
|
76
87
|
end
|
77
88
|
end
|
@@ -50,10 +50,11 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
|
|
50
50
|
|
51
51
|
shared_examples "testing methods" do
|
52
52
|
before do
|
53
|
-
|
53
|
+
case config_type
|
54
|
+
when :old
|
54
55
|
configure_old(MyActiveRecordModel, MyActiveRecordModelTransition)
|
55
56
|
configure_old(OtherActiveRecordModel, OtherActiveRecordModelTransition)
|
56
|
-
|
57
|
+
when :new
|
57
58
|
configure_new(MyActiveRecordModel, MyActiveRecordModelTransition)
|
58
59
|
configure_new(OtherActiveRecordModel, OtherActiveRecordModelTransition)
|
59
60
|
else
|
@@ -116,8 +117,8 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
|
|
116
117
|
subject(:not_in_state) { MyActiveRecordModel.not_in_state(:succeeded, :failed) }
|
117
118
|
|
118
119
|
it do
|
119
|
-
expect(not_in_state).to
|
120
|
-
|
120
|
+
expect(not_in_state).to contain_exactly(initial_state_model,
|
121
|
+
returned_to_initial_model)
|
121
122
|
end
|
122
123
|
end
|
123
124
|
|
@@ -125,8 +126,8 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
|
|
125
126
|
subject(:not_in_state) { MyActiveRecordModel.not_in_state(%i[succeeded failed]) }
|
126
127
|
|
127
128
|
it do
|
128
|
-
expect(not_in_state).to
|
129
|
-
|
129
|
+
expect(not_in_state).to contain_exactly(initial_state_model,
|
130
|
+
returned_to_initial_model)
|
130
131
|
end
|
131
132
|
end
|
132
133
|
end
|
@@ -154,6 +155,31 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
|
|
154
155
|
end
|
155
156
|
end
|
156
157
|
|
158
|
+
context "with a custom primary key for the model" do
|
159
|
+
before do
|
160
|
+
# Switch to using OtherActiveRecordModelTransition, so the existing
|
161
|
+
# relation with MyActiveRecordModelTransition doesn't interfere with
|
162
|
+
# this spec.
|
163
|
+
# Configure the relationship to use a different primary key,
|
164
|
+
MyActiveRecordModel.send(:has_many,
|
165
|
+
:custom_name,
|
166
|
+
class_name: "OtherActiveRecordModelTransition",
|
167
|
+
primary_key: :external_id)
|
168
|
+
|
169
|
+
MyActiveRecordModel.class_eval do
|
170
|
+
def self.transition_class
|
171
|
+
OtherActiveRecordModelTransition
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
describe ".in_state" do
|
177
|
+
subject(:query) { MyActiveRecordModel.in_state(:succeeded) }
|
178
|
+
|
179
|
+
specify { expect { query }.to_not raise_error }
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
157
183
|
context "after_commit transactional integrity" do
|
158
184
|
before do
|
159
185
|
MyStateMachine.class_eval do
|
@@ -176,7 +202,6 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
|
|
176
202
|
MyActiveRecordModel.create
|
177
203
|
end
|
178
204
|
|
179
|
-
# rubocop:disable RSpec/ExampleLength
|
180
205
|
it do
|
181
206
|
expect do
|
182
207
|
ActiveRecord::Base.transaction do
|
@@ -185,7 +210,6 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
|
|
185
210
|
end
|
186
211
|
end.to_not change(MyStateMachine, :after_commit_callback_executed)
|
187
212
|
end
|
188
|
-
# rubocop:enable RSpec/ExampleLength
|
189
213
|
end
|
190
214
|
end
|
191
215
|
|
@@ -230,7 +254,7 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
|
|
230
254
|
end
|
231
255
|
|
232
256
|
it "does not raise an error" do
|
233
|
-
expect { check_missing_methods! }.to_not raise_exception
|
257
|
+
expect { check_missing_methods! }.to_not raise_exception
|
234
258
|
end
|
235
259
|
end
|
236
260
|
|
@@ -12,6 +12,9 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
12
12
|
|
13
13
|
MyActiveRecordModelTransition.serialize(:metadata, JSON)
|
14
14
|
|
15
|
+
prepare_sti_model_table
|
16
|
+
prepare_sti_transitions_table
|
17
|
+
|
15
18
|
Statesman.configure do
|
16
19
|
# Rubocop requires described_class to be used, but this block
|
17
20
|
# is instance_eval'd and described_class won't be defined
|
@@ -35,8 +38,8 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
35
38
|
allow(metadata_column).to receive_messages(sql_type: "")
|
36
39
|
allow(MyActiveRecordModelTransition).to receive_messages(columns_hash:
|
37
40
|
{ "metadata" => metadata_column })
|
38
|
-
if
|
39
|
-
|
41
|
+
if ActiveRecord.respond_to?(:gem_version) &&
|
42
|
+
ActiveRecord.gem_version >= Gem::Version.new("4.2.0.a")
|
40
43
|
expect(MyActiveRecordModelTransition).
|
41
44
|
to receive(:type_for_attribute).with("metadata").
|
42
45
|
and_return(ActiveRecord::Type::Value.new)
|
@@ -60,10 +63,10 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
60
63
|
allow(metadata_column).to receive_messages(sql_type: "json")
|
61
64
|
allow(MyActiveRecordModelTransition).to receive_messages(columns_hash:
|
62
65
|
{ "metadata" => metadata_column })
|
63
|
-
if
|
64
|
-
|
65
|
-
serialized_type =
|
66
|
-
"",
|
66
|
+
if ActiveRecord.respond_to?(:gem_version) &&
|
67
|
+
ActiveRecord.gem_version >= Gem::Version.new("4.2.0.a")
|
68
|
+
serialized_type = ActiveRecord::Type::Serialized.new(
|
69
|
+
"", ActiveRecord::Coders::JSON
|
67
70
|
)
|
68
71
|
expect(MyActiveRecordModelTransition).
|
69
72
|
to receive(:type_for_attribute).with("metadata").
|
@@ -88,10 +91,10 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
88
91
|
allow(metadata_column).to receive_messages(sql_type: "jsonb")
|
89
92
|
allow(MyActiveRecordModelTransition).to receive_messages(columns_hash:
|
90
93
|
{ "metadata" => metadata_column })
|
91
|
-
if
|
92
|
-
|
93
|
-
serialized_type =
|
94
|
-
"",
|
94
|
+
if ActiveRecord.respond_to?(:gem_version) &&
|
95
|
+
ActiveRecord.gem_version >= Gem::Version.new("4.2.0.a")
|
96
|
+
serialized_type = ActiveRecord::Type::Serialized.new(
|
97
|
+
"", ActiveRecord::Coders::JSON
|
95
98
|
)
|
96
99
|
expect(MyActiveRecordModelTransition).
|
97
100
|
to receive(:type_for_attribute).with("metadata").
|
@@ -112,7 +115,7 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
112
115
|
end
|
113
116
|
|
114
117
|
describe "#create" do
|
115
|
-
subject {
|
118
|
+
subject(:transition) { create }
|
116
119
|
|
117
120
|
let!(:adapter) do
|
118
121
|
described_class.new(MyActiveRecordModelTransition, model, observer)
|
@@ -130,6 +133,31 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
130
133
|
expect { adapter.create(:y, :z) }.
|
131
134
|
to raise_exception(Statesman::TransitionConflictError)
|
132
135
|
end
|
136
|
+
|
137
|
+
it "does not pollute the state when the transition fails" do
|
138
|
+
# this increments the sort_key in the database
|
139
|
+
adapter.create(:x, :y)
|
140
|
+
|
141
|
+
# we then pre-load the transitions for efficiency
|
142
|
+
preloaded_model = MyActiveRecordModel.
|
143
|
+
includes(:my_active_record_model_transitions).
|
144
|
+
find(model.id)
|
145
|
+
|
146
|
+
adapter2 = described_class.
|
147
|
+
new(MyActiveRecordModelTransition, preloaded_model, observer)
|
148
|
+
|
149
|
+
# Now we generate a race
|
150
|
+
adapter.create(:y, :z)
|
151
|
+
expect { adapter2.create(:y, :a) }.
|
152
|
+
to raise_error(Statesman::TransitionConflictError)
|
153
|
+
|
154
|
+
# The preloaded adapter should discard the preloaded info
|
155
|
+
expect(adapter2.last).to have_attributes(to_state: "z")
|
156
|
+
expect(adapter2.history).to contain_exactly(
|
157
|
+
have_attributes(to_state: "y"),
|
158
|
+
have_attributes(to_state: "z"),
|
159
|
+
)
|
160
|
+
end
|
133
161
|
end
|
134
162
|
|
135
163
|
context "when other exceptions occur" do
|
@@ -140,27 +168,25 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
140
168
|
|
141
169
|
context "ActiveRecord::RecordNotUnique unrelated to this transition" do
|
142
170
|
let(:error) do
|
143
|
-
if
|
144
|
-
|
171
|
+
if ActiveRecord.respond_to?(:gem_version) &&
|
172
|
+
ActiveRecord.gem_version >= Gem::Version.new("4.0.0")
|
145
173
|
ActiveRecord::RecordNotUnique.new("unrelated")
|
146
174
|
else
|
147
175
|
ActiveRecord::RecordNotUnique.new("unrelated", nil)
|
148
176
|
end
|
149
177
|
end
|
150
178
|
|
151
|
-
it {
|
179
|
+
it { expect { transition }.to raise_exception(ActiveRecord::RecordNotUnique) }
|
152
180
|
end
|
153
181
|
|
154
182
|
context "other errors" do
|
155
183
|
let(:error) { StandardError }
|
156
184
|
|
157
|
-
it {
|
185
|
+
it { expect { transition }.to raise_exception(StandardError) }
|
158
186
|
end
|
159
187
|
end
|
160
188
|
|
161
189
|
describe "updating the most_recent column" do
|
162
|
-
subject { create }
|
163
|
-
|
164
190
|
context "with no previous transition" do
|
165
191
|
its(:most_recent) { is_expected.to eq(true) }
|
166
192
|
end
|
@@ -277,6 +303,57 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
277
303
|
from(true).to be_falsey
|
278
304
|
end
|
279
305
|
end
|
306
|
+
|
307
|
+
context "when transition uses STI" do
|
308
|
+
let(:sti_model) { StiActiveRecordModel.create }
|
309
|
+
|
310
|
+
let(:adapter_a) do
|
311
|
+
described_class.new(
|
312
|
+
StiAActiveRecordModelTransition,
|
313
|
+
sti_model,
|
314
|
+
observer,
|
315
|
+
{ association_name: :sti_a_active_record_model_transitions },
|
316
|
+
)
|
317
|
+
end
|
318
|
+
let(:adapter_b) do
|
319
|
+
described_class.new(
|
320
|
+
StiBActiveRecordModelTransition,
|
321
|
+
sti_model,
|
322
|
+
observer,
|
323
|
+
{ association_name: :sti_b_active_record_model_transitions },
|
324
|
+
)
|
325
|
+
end
|
326
|
+
let(:create) { adapter_a.create(from, to) }
|
327
|
+
|
328
|
+
context "with a previous unrelated transition" do
|
329
|
+
let!(:transition_b) { adapter_b.create(from, to) }
|
330
|
+
|
331
|
+
its(:most_recent) { is_expected.to eq(true) }
|
332
|
+
|
333
|
+
it "doesn't update the previous transition's most_recent flag" do
|
334
|
+
expect { create }.
|
335
|
+
to_not(change { transition_b.reload.most_recent })
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
context "with previous related and unrelated transitions" do
|
340
|
+
let!(:transition_a) { adapter_a.create(from, to) }
|
341
|
+
let!(:transition_b) { adapter_b.create(from, to) }
|
342
|
+
|
343
|
+
its(:most_recent) { is_expected.to eq(true) }
|
344
|
+
|
345
|
+
it "updates the previous transition's most_recent flag" do
|
346
|
+
expect { create }.
|
347
|
+
to change { transition_a.reload.most_recent }.
|
348
|
+
from(true).to be_falsey
|
349
|
+
end
|
350
|
+
|
351
|
+
it "doesn't update the previous unrelated transition's most_recent flag" do
|
352
|
+
expect { create }.
|
353
|
+
to_not(change { transition_b.reload.most_recent })
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
280
357
|
end
|
281
358
|
end
|
282
359
|
|
@@ -285,9 +362,9 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
285
362
|
described_class.new(MyActiveRecordModelTransition, model, observer)
|
286
363
|
end
|
287
364
|
|
288
|
-
before { adapter.create(:x, :y) }
|
289
|
-
|
290
365
|
context "with a previously looked up transition" do
|
366
|
+
before { adapter.create(:x, :y) }
|
367
|
+
|
291
368
|
before { adapter.last }
|
292
369
|
|
293
370
|
it "caches the transition" do
|
@@ -353,6 +430,35 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
353
430
|
expect(adapter.last.to_state).to eq("y")
|
354
431
|
end
|
355
432
|
end
|
433
|
+
|
434
|
+
context "without previous transitions" do
|
435
|
+
it "does query the database only once" do
|
436
|
+
expect(model.my_active_record_model_transitions).
|
437
|
+
to receive(:order).once.and_call_original
|
438
|
+
|
439
|
+
expect(adapter.last).to eq(nil)
|
440
|
+
expect(adapter.last).to eq(nil)
|
441
|
+
end
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
describe "#reset" do
|
446
|
+
it "works with empty cache" do
|
447
|
+
expect { model.state_machine.reset }.to_not raise_error
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
it "resets last with #reload" do
|
452
|
+
model.save!
|
453
|
+
ActiveRecord::Base.transaction do
|
454
|
+
model.state_machine.transition_to!(:succeeded)
|
455
|
+
# force to cache value in last_transition instance variable
|
456
|
+
expect(model.state_machine.current_state).to eq("succeeded")
|
457
|
+
raise ActiveRecord::Rollback
|
458
|
+
end
|
459
|
+
expect(model.state_machine.current_state).to eq("succeeded")
|
460
|
+
model.reload
|
461
|
+
expect(model.state_machine.current_state).to eq("initial")
|
356
462
|
end
|
357
463
|
|
358
464
|
context "with a namespaced model" do
|
@@ -30,14 +30,14 @@ shared_examples_for "an adapter" do |adapter_class, transition_class, options =
|
|
30
30
|
end
|
31
31
|
|
32
32
|
describe "#create" do
|
33
|
-
subject {
|
33
|
+
subject(:transition) { create }
|
34
34
|
|
35
35
|
let(:from) { :x }
|
36
36
|
let(:to) { :y }
|
37
37
|
let(:there) { :z }
|
38
38
|
let(:create) { adapter.create(from, to) }
|
39
39
|
|
40
|
-
it {
|
40
|
+
it { expect { transition }.to change(adapter.history, :count).by(1) }
|
41
41
|
|
42
42
|
context "the new transition" do
|
43
43
|
subject(:instance) { create }
|
@@ -128,6 +128,7 @@ shared_examples_for "an adapter" do |adapter_class, transition_class, options =
|
|
128
128
|
|
129
129
|
it { is_expected.to be_a(transition_class) }
|
130
130
|
specify { expect(adapter.last.to_state.to_sym).to eq(:z) }
|
131
|
+
|
131
132
|
specify do
|
132
133
|
expect(adapter.last(force_reload: true).to_state.to_sym).to eq(:z)
|
133
134
|
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
describe Statesman::Adapters::TypeSafeActiveRecordQueries, active_record: true do
|
6
|
+
def configure(klass, transition_class)
|
7
|
+
klass.send(:extend, described_class)
|
8
|
+
klass.configure_state_machine(
|
9
|
+
transition_class: transition_class,
|
10
|
+
initial_state: :initial,
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
before do
|
15
|
+
prepare_model_table
|
16
|
+
prepare_transitions_table
|
17
|
+
prepare_other_model_table
|
18
|
+
prepare_other_transitions_table
|
19
|
+
|
20
|
+
Statesman.configure do
|
21
|
+
storage_adapter(Statesman::Adapters::ActiveRecord)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
after { Statesman.configure { storage_adapter(Statesman::Adapters::Memory) } }
|
26
|
+
|
27
|
+
let!(:model) do
|
28
|
+
model = MyActiveRecordModel.create
|
29
|
+
model.state_machine.transition_to(:succeeded)
|
30
|
+
model
|
31
|
+
end
|
32
|
+
|
33
|
+
let!(:other_model) do
|
34
|
+
model = MyActiveRecordModel.create
|
35
|
+
model.state_machine.transition_to(:failed)
|
36
|
+
model
|
37
|
+
end
|
38
|
+
|
39
|
+
let!(:initial_state_model) { MyActiveRecordModel.create }
|
40
|
+
|
41
|
+
let!(:returned_to_initial_model) do
|
42
|
+
model = MyActiveRecordModel.create
|
43
|
+
model.state_machine.transition_to(:failed)
|
44
|
+
model.state_machine.transition_to(:initial)
|
45
|
+
model
|
46
|
+
end
|
47
|
+
|
48
|
+
shared_examples "testing methods" do
|
49
|
+
before do
|
50
|
+
configure(MyActiveRecordModel, MyActiveRecordModelTransition)
|
51
|
+
configure(OtherActiveRecordModel, OtherActiveRecordModelTransition)
|
52
|
+
|
53
|
+
MyActiveRecordModel.send(:has_one, :other_active_record_model)
|
54
|
+
OtherActiveRecordModel.send(:belongs_to, :my_active_record_model)
|
55
|
+
end
|
56
|
+
|
57
|
+
describe ".in_state" do
|
58
|
+
context "given a single state" do
|
59
|
+
subject { MyActiveRecordModel.in_state(:succeeded) }
|
60
|
+
|
61
|
+
it { is_expected.to include model }
|
62
|
+
it { is_expected.to_not include other_model }
|
63
|
+
end
|
64
|
+
|
65
|
+
context "given multiple states" do
|
66
|
+
subject { MyActiveRecordModel.in_state(:succeeded, :failed) }
|
67
|
+
|
68
|
+
it { is_expected.to include model }
|
69
|
+
it { is_expected.to include other_model }
|
70
|
+
end
|
71
|
+
|
72
|
+
context "given the initial state" do
|
73
|
+
subject { MyActiveRecordModel.in_state(:initial) }
|
74
|
+
|
75
|
+
it { is_expected.to include initial_state_model }
|
76
|
+
it { is_expected.to include returned_to_initial_model }
|
77
|
+
end
|
78
|
+
|
79
|
+
context "given an array of states" do
|
80
|
+
subject { MyActiveRecordModel.in_state(%i[succeeded failed]) }
|
81
|
+
|
82
|
+
it { is_expected.to include model }
|
83
|
+
it { is_expected.to include other_model }
|
84
|
+
end
|
85
|
+
|
86
|
+
context "merging two queries" do
|
87
|
+
subject do
|
88
|
+
MyActiveRecordModel.in_state(:succeeded).
|
89
|
+
joins(:other_active_record_model).
|
90
|
+
merge(OtherActiveRecordModel.in_state(:initial))
|
91
|
+
end
|
92
|
+
|
93
|
+
it { is_expected.to be_empty }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe ".not_in_state" do
|
98
|
+
context "given a single state" do
|
99
|
+
subject { MyActiveRecordModel.not_in_state(:failed) }
|
100
|
+
|
101
|
+
it { is_expected.to include model }
|
102
|
+
it { is_expected.to_not include other_model }
|
103
|
+
end
|
104
|
+
|
105
|
+
context "given multiple states" do
|
106
|
+
subject(:not_in_state) { MyActiveRecordModel.not_in_state(:succeeded, :failed) }
|
107
|
+
|
108
|
+
it do
|
109
|
+
expect(not_in_state).to contain_exactly(initial_state_model,
|
110
|
+
returned_to_initial_model)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
context "given an array of states" do
|
115
|
+
subject(:not_in_state) { MyActiveRecordModel.not_in_state(%i[succeeded failed]) }
|
116
|
+
|
117
|
+
it do
|
118
|
+
expect(not_in_state).to contain_exactly(initial_state_model,
|
119
|
+
returned_to_initial_model)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
context "with a custom name for the transition association" do
|
125
|
+
before do
|
126
|
+
# Switch to using OtherActiveRecordModelTransition, so the existing
|
127
|
+
# relation with MyActiveRecordModelTransition doesn't interfere with
|
128
|
+
# this spec.
|
129
|
+
MyActiveRecordModel.send(:has_many,
|
130
|
+
:custom_name,
|
131
|
+
class_name: "OtherActiveRecordModelTransition")
|
132
|
+
|
133
|
+
MyActiveRecordModel.class_eval do
|
134
|
+
def self.transition_class
|
135
|
+
OtherActiveRecordModelTransition
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
describe ".in_state" do
|
141
|
+
subject(:query) { MyActiveRecordModel.in_state(:succeeded) }
|
142
|
+
|
143
|
+
specify { expect { query }.to_not raise_error }
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
context "with a custom primary key for the model" do
|
148
|
+
before do
|
149
|
+
# Switch to using OtherActiveRecordModelTransition, so the existing
|
150
|
+
# relation with MyActiveRecordModelTransition doesn't interfere with
|
151
|
+
# this spec.
|
152
|
+
# Configure the relationship to use a different primary key,
|
153
|
+
MyActiveRecordModel.send(:has_many,
|
154
|
+
:custom_name,
|
155
|
+
class_name: "OtherActiveRecordModelTransition",
|
156
|
+
primary_key: :external_id)
|
157
|
+
|
158
|
+
MyActiveRecordModel.class_eval do
|
159
|
+
def self.transition_class
|
160
|
+
OtherActiveRecordModelTransition
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
describe ".in_state" do
|
166
|
+
subject(:query) { MyActiveRecordModel.in_state(:succeeded) }
|
167
|
+
|
168
|
+
specify { expect { query }.to_not raise_error }
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
context "after_commit transactional integrity" do
|
173
|
+
before do
|
174
|
+
MyStateMachine.class_eval do
|
175
|
+
cattr_accessor(:after_commit_callback_executed) { false }
|
176
|
+
|
177
|
+
after_transition(from: :initial, to: :succeeded, after_commit: true) do
|
178
|
+
# This leaks state in a testable way if transactional integrity is broken.
|
179
|
+
MyStateMachine.after_commit_callback_executed = true
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
after do
|
185
|
+
MyStateMachine.class_eval do
|
186
|
+
callbacks[:after_commit] = []
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
let!(:model) do
|
191
|
+
MyActiveRecordModel.create
|
192
|
+
end
|
193
|
+
|
194
|
+
it do
|
195
|
+
expect do
|
196
|
+
ActiveRecord::Base.transaction do
|
197
|
+
model.state_machine.transition_to!(:succeeded)
|
198
|
+
raise ActiveRecord::Rollback
|
199
|
+
end
|
200
|
+
end.to_not change(MyStateMachine, :after_commit_callback_executed)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
context "using configuration method" do
|
206
|
+
include_examples "testing methods"
|
207
|
+
end
|
208
|
+
end
|
@@ -7,6 +7,7 @@ describe Statesman do
|
|
7
7
|
subject(:error) { Statesman::InvalidStateError.new }
|
8
8
|
|
9
9
|
its(:message) { is_expected.to eq("Statesman::InvalidStateError") }
|
10
|
+
|
10
11
|
its "string matches its message" do
|
11
12
|
expect(error.to_s).to eq(error.message)
|
12
13
|
end
|
@@ -16,6 +17,7 @@ describe Statesman do
|
|
16
17
|
subject(:error) { Statesman::InvalidTransitionError.new }
|
17
18
|
|
18
19
|
its(:message) { is_expected.to eq("Statesman::InvalidTransitionError") }
|
20
|
+
|
19
21
|
its "string matches its message" do
|
20
22
|
expect(error.to_s).to eq(error.message)
|
21
23
|
end
|
@@ -25,6 +27,7 @@ describe Statesman do
|
|
25
27
|
subject(:error) { Statesman::InvalidTransitionError.new }
|
26
28
|
|
27
29
|
its(:message) { is_expected.to eq("Statesman::InvalidTransitionError") }
|
30
|
+
|
28
31
|
its "string matches its message" do
|
29
32
|
expect(error.to_s).to eq(error.message)
|
30
33
|
end
|
@@ -34,6 +37,7 @@ describe Statesman do
|
|
34
37
|
subject(:error) { Statesman::TransitionConflictError.new }
|
35
38
|
|
36
39
|
its(:message) { is_expected.to eq("Statesman::TransitionConflictError") }
|
40
|
+
|
37
41
|
its "string matches its message" do
|
38
42
|
expect(error.to_s).to eq(error.message)
|
39
43
|
end
|
@@ -43,6 +47,7 @@ describe Statesman do
|
|
43
47
|
subject(:error) { Statesman::MissingTransitionAssociation.new }
|
44
48
|
|
45
49
|
its(:message) { is_expected.to eq("Statesman::MissingTransitionAssociation") }
|
50
|
+
|
46
51
|
its "string matches its message" do
|
47
52
|
expect(error.to_s).to eq(error.message)
|
48
53
|
end
|
@@ -52,17 +57,25 @@ describe Statesman do
|
|
52
57
|
subject(:error) { Statesman::TransitionFailedError.new("from", "to") }
|
53
58
|
|
54
59
|
its(:message) { is_expected.to eq("Cannot transition from 'from' to 'to'") }
|
60
|
+
|
55
61
|
its "string matches its message" do
|
56
62
|
expect(error.to_s).to eq(error.message)
|
57
63
|
end
|
58
64
|
end
|
59
65
|
|
60
66
|
describe "GuardFailedError" do
|
61
|
-
subject(:error) { Statesman::GuardFailedError.new("from", "to") }
|
67
|
+
subject(:error) { Statesman::GuardFailedError.new("from", "to", callback) }
|
68
|
+
|
69
|
+
let(:callback) { -> { "hello" } }
|
62
70
|
|
63
71
|
its(:message) do
|
64
72
|
is_expected.to eq("Guard on transition from: 'from' to 'to' returned false")
|
65
73
|
end
|
74
|
+
|
75
|
+
its(:backtrace) do
|
76
|
+
is_expected.to eq([callback.source_location.join(":")])
|
77
|
+
end
|
78
|
+
|
66
79
|
its "string matches its message" do
|
67
80
|
expect(error.to_s).to eq(error.message)
|
68
81
|
end
|
@@ -72,6 +85,7 @@ describe Statesman do
|
|
72
85
|
subject(:error) { Statesman::UnserializedMetadataError.new("foo") }
|
73
86
|
|
74
87
|
its(:message) { is_expected.to match(/foo#metadata is not serialized/) }
|
88
|
+
|
75
89
|
its "string matches its message" do
|
76
90
|
expect(error.to_s).to eq(error.message)
|
77
91
|
end
|
@@ -81,6 +95,7 @@ describe Statesman do
|
|
81
95
|
subject(:error) { Statesman::IncompatibleSerializationError.new("foo") }
|
82
96
|
|
83
97
|
its(:message) { is_expected.to match(/foo#metadata column type cannot be json/) }
|
98
|
+
|
84
99
|
its "string matches its message" do
|
85
100
|
expect(error.to_s).to eq(error.message)
|
86
101
|
end
|