statesman 7.4.0 → 10.2.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|