statesman 10.0.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.
@@ -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 ::ActiveRecord.respond_to?(:gem_version) &&
39
- ::ActiveRecord.gem_version >= Gem::Version.new("4.2.0.a")
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 ::ActiveRecord.respond_to?(:gem_version) &&
64
- ::ActiveRecord.gem_version >= Gem::Version.new("4.2.0.a")
65
- serialized_type = ::ActiveRecord::Type::Serialized.new(
66
- "", ::ActiveRecord::Coders::JSON
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 ::ActiveRecord.respond_to?(:gem_version) &&
92
- ::ActiveRecord.gem_version >= Gem::Version.new("4.2.0.a")
93
- serialized_type = ::ActiveRecord::Type::Serialized.new(
94
- "", ::ActiveRecord::Coders::JSON
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 { -> { create } }
118
+ subject(:transition) { create }
116
119
 
117
120
  let!(:adapter) do
118
121
  described_class.new(MyActiveRecordModelTransition, model, observer)
@@ -165,27 +168,25 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
165
168
 
166
169
  context "ActiveRecord::RecordNotUnique unrelated to this transition" do
167
170
  let(:error) do
168
- if ::ActiveRecord.respond_to?(:gem_version) &&
169
- ::ActiveRecord.gem_version >= Gem::Version.new("4.0.0")
171
+ if ActiveRecord.respond_to?(:gem_version) &&
172
+ ActiveRecord.gem_version >= Gem::Version.new("4.0.0")
170
173
  ActiveRecord::RecordNotUnique.new("unrelated")
171
174
  else
172
175
  ActiveRecord::RecordNotUnique.new("unrelated", nil)
173
176
  end
174
177
  end
175
178
 
176
- it { is_expected.to raise_exception(ActiveRecord::RecordNotUnique) }
179
+ it { expect { transition }.to raise_exception(ActiveRecord::RecordNotUnique) }
177
180
  end
178
181
 
179
182
  context "other errors" do
180
183
  let(:error) { StandardError }
181
184
 
182
- it { is_expected.to raise_exception(StandardError) }
185
+ it { expect { transition }.to raise_exception(StandardError) }
183
186
  end
184
187
  end
185
188
 
186
189
  describe "updating the most_recent column" do
187
- subject { create }
188
-
189
190
  context "with no previous transition" do
190
191
  its(:most_recent) { is_expected.to eq(true) }
191
192
  end
@@ -302,6 +303,57 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
302
303
  from(true).to be_falsey
303
304
  end
304
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
305
357
  end
306
358
  end
307
359
 
@@ -310,9 +362,9 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
310
362
  described_class.new(MyActiveRecordModelTransition, model, observer)
311
363
  end
312
364
 
313
- before { adapter.create(:x, :y) }
314
-
315
365
  context "with a previously looked up transition" do
366
+ before { adapter.create(:x, :y) }
367
+
316
368
  before { adapter.last }
317
369
 
318
370
  it "caches the transition" do
@@ -378,6 +430,22 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
378
430
  expect(adapter.last.to_state).to eq("y")
379
431
  end
380
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
381
449
  end
382
450
 
383
451
  it "resets last with #reload" 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 { -> { create } }
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 { is_expected.to change(adapter.history, :count).by(1) }
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 }
@@ -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
@@ -64,12 +64,18 @@ describe Statesman do
64
64
  end
65
65
 
66
66
  describe "GuardFailedError" do
67
- subject(:error) { Statesman::GuardFailedError.new("from", "to") }
67
+ subject(:error) { Statesman::GuardFailedError.new("from", "to", callback) }
68
+
69
+ let(:callback) { -> { "hello" } }
68
70
 
69
71
  its(:message) do
70
72
  is_expected.to eq("Guard on transition from: 'from' to 'to' returned false")
71
73
  end
72
74
 
75
+ its(:backtrace) do
76
+ is_expected.to eq([callback.source_location.join(":")])
77
+ end
78
+
73
79
  its "string matches its message" do
74
80
  expect(error.to_s).to eq(error.message)
75
81
  end
@@ -28,7 +28,7 @@ describe Statesman::Machine do
28
28
  end
29
29
 
30
30
  describe ".remove_state" do
31
- subject(:remove_state) { -> { machine.remove_state(:x) } }
31
+ subject(:remove_state) { machine.remove_state(:x) }
32
32
 
33
33
  before do
34
34
  machine.class_eval do
@@ -39,7 +39,7 @@ describe Statesman::Machine do
39
39
  end
40
40
 
41
41
  it "removes the state" do
42
- expect(remove_state).
42
+ expect { remove_state }.
43
43
  to change(machine, :states).
44
44
  from(match_array(%w[x y z])).
45
45
  to(%w[y z])
@@ -49,7 +49,7 @@ describe Statesman::Machine do
49
49
  before { machine.transition from: :x, to: :y }
50
50
 
51
51
  it "removes the transition" do
52
- expect(remove_state).
52
+ expect { remove_state }.
53
53
  to change(machine, :successors).
54
54
  from({ "x" => ["y"] }).
55
55
  to({})
@@ -59,7 +59,7 @@ describe Statesman::Machine do
59
59
  before { machine.transition from: :x, to: :z }
60
60
 
61
61
  it "removes all transitions" do
62
- expect(remove_state).
62
+ expect { remove_state }.
63
63
  to change(machine, :successors).
64
64
  from({ "x" => %w[y z] }).
65
65
  to({})
@@ -71,7 +71,7 @@ describe Statesman::Machine do
71
71
  before { machine.transition from: :y, to: :x }
72
72
 
73
73
  it "removes the transition" do
74
- expect(remove_state).
74
+ expect { remove_state }.
75
75
  to change(machine, :successors).
76
76
  from({ "y" => ["x"] }).
77
77
  to({})
@@ -81,7 +81,7 @@ describe Statesman::Machine do
81
81
  before { machine.transition from: :z, to: :x }
82
82
 
83
83
  it "removes all transitions" do
84
- expect(remove_state).
84
+ expect { remove_state }.
85
85
  to change(machine, :successors).
86
86
  from({ "y" => ["x"], "z" => ["x"] }).
87
87
  to({})
@@ -104,7 +104,7 @@ describe Statesman::Machine do
104
104
  end
105
105
 
106
106
  it "removes the guard" do
107
- expect(remove_state).
107
+ expect { remove_state }.
108
108
  to change(machine, :callbacks).
109
109
  from(a_hash_including(guards: match_array(guards))).
110
110
  to(a_hash_including(guards: []))
@@ -125,7 +125,7 @@ describe Statesman::Machine do
125
125
  end
126
126
 
127
127
  it "removes the guard" do
128
- expect(remove_state).
128
+ expect { remove_state }.
129
129
  to change(machine, :callbacks).
130
130
  from(a_hash_including(guards: match_array(guards))).
131
131
  to(a_hash_including(guards: []))
@@ -935,10 +935,10 @@ describe Statesman::Machine do
935
935
  it { is_expected.to be(:some_state) }
936
936
  end
937
937
 
938
- context "when it is unsuccesful" do
938
+ context "when it is unsuccessful" do
939
939
  before do
940
940
  allow(instance).to receive(:transition_to!).
941
- and_raise(Statesman::GuardFailedError.new(:x, :some_state))
941
+ and_raise(Statesman::GuardFailedError.new(:x, :some_state, nil))
942
942
  end
943
943
 
944
944
  it { is_expected.to be_falsey }
@@ -976,20 +976,20 @@ describe Statesman::Machine do
976
976
  end
977
977
 
978
978
  context "with defined callbacks" do
979
- let(:callback_1) { -> { "Hi" } }
980
- let(:callback_2) { -> { "Bye" } }
979
+ let(:callback_one) { -> { "Hi" } }
980
+ let(:callback_two) { -> { "Bye" } }
981
981
 
982
982
  before do
983
- machine.send(definer, from: :x, to: :y, &callback_1)
984
- machine.send(definer, from: :y, to: :z, &callback_2)
983
+ machine.send(definer, from: :x, to: :y, &callback_one)
984
+ machine.send(definer, from: :y, to: :z, &callback_two)
985
985
  end
986
986
 
987
987
  it "contains the relevant callback" do
988
- expect(callbacks.map(&:callback)).to include(callback_1)
988
+ expect(callbacks.map(&:callback)).to include(callback_one)
989
989
  end
990
990
 
991
991
  it "does not contain the irrelevant callback" do
992
- expect(callbacks.map(&:callback)).to_not include(callback_2)
992
+ expect(callbacks.map(&:callback)).to_not include(callback_two)
993
993
  end
994
994
  end
995
995
  end