state_machines-audit_trail 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,222 @@
1
+ require 'active_record'
2
+
3
+ ### Setup test database
4
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
5
+
6
+ class ARModelStateTransition < ActiveRecord::Base
7
+ belongs_to :ar_model
8
+ end
9
+ class ARModelNoInitialStateTransition < ActiveRecord::Base
10
+ belongs_to :ar_model_no_initial
11
+ end
12
+
13
+ class ARModelWithContextStateTransition < ActiveRecord::Base
14
+ belongs_to :ar_model_with_context
15
+ end
16
+
17
+ class ARModelWithMultipleContextStateTransition < ActiveRecord::Base
18
+ belongs_to :ar_model_with_multiple_context
19
+ end
20
+
21
+ class ARModelWithMultipleStateMachinesFirstTransition < ActiveRecord::Base
22
+ belongs_to :ar_model_with_multiple_state_machines
23
+ end
24
+
25
+ class ARModelWithMultipleStateMachinesSecondTransition < ActiveRecord::Base
26
+ belongs_to :ar_model_with_multiple_state_machines
27
+ end
28
+
29
+ class ARModelWithMultipleStateMachinesThirdTransition < ActiveRecord::Base
30
+ belongs_to :ar_model_with_multiple_state_machines
31
+ end
32
+
33
+ class ARModel < ActiveRecord::Base
34
+
35
+ state_machine :state, initial: :waiting do # log initial state?
36
+ audit_trail
37
+
38
+ event :start do
39
+ transition [:waiting, :stopped] => :started
40
+ end
41
+
42
+ event :stop do
43
+ transition :started => :stopped
44
+ end
45
+ end
46
+ end
47
+
48
+ class ARModelNoInitial < ActiveRecord::Base
49
+
50
+ state_machine :state, initial: :waiting do # log initial state?
51
+ audit_trail initial: false
52
+
53
+ event :start do
54
+ transition [:waiting, :stopped] => :started
55
+ end
56
+
57
+ event :stop do
58
+ transition :started => :stopped
59
+ end
60
+ end
61
+ end
62
+ #
63
+ class ARModelWithContext < ActiveRecord::Base
64
+ state_machine :state, initial: :waiting do # log initial state?
65
+ audit_trail context: :context
66
+
67
+ event :start do
68
+ transition [:waiting, :stopped] => :started
69
+ end
70
+
71
+ event :stop do
72
+ transition :started => :stopped
73
+ end
74
+ end
75
+
76
+ def context
77
+ 'Some context'
78
+ end
79
+ end
80
+
81
+ class ARModelWithMultipleContext < ActiveRecord::Base
82
+ state_machine :state, initial: :waiting do # log initial state?
83
+ audit_trail context: [:context, :second_context, :context_with_args]
84
+
85
+ event :start do
86
+ transition [:waiting, :stopped] => :started
87
+ end
88
+
89
+ event :stop do
90
+ transition :started => :stopped
91
+ end
92
+ end
93
+
94
+ def context
95
+ 'Some context'
96
+ end
97
+
98
+ def second_context
99
+ 'Extra context'
100
+ end
101
+
102
+ def context_with_args(transition)
103
+ id = transition.args.last.delete(:id) if transition.args.present?
104
+ id
105
+ end
106
+
107
+ end
108
+
109
+ class ARModelDescendant < ARModel
110
+ end
111
+
112
+ class ARModelDescendantWithOwnStateMachines < ARModel
113
+ state_machine :state, :initial => :new do
114
+ audit_trail
115
+
116
+ event :complete do
117
+ transition [:new] => :completed
118
+ end
119
+ end
120
+ end
121
+
122
+ class ARModelWithMultipleStateMachines < ActiveRecord::Base
123
+
124
+ state_machine :first, :initial => :beginning do
125
+ audit_trail
126
+
127
+ event :begin_first do
128
+ transition :beginning => :end
129
+ end
130
+ end
131
+
132
+ state_machine :second do
133
+ audit_trail
134
+
135
+ event :begin_second do
136
+ transition nil => :beginning_second
137
+ end
138
+ end
139
+
140
+ state_machine :third, :action => nil do
141
+ audit_trail
142
+
143
+ event :begin_third do
144
+ transition nil => :beginning_third
145
+ end
146
+
147
+ event :end_third do
148
+ transition :beginning_third => :done_third
149
+ end
150
+ end
151
+ end
152
+
153
+ module SomeNamespace
154
+ class ARModelStateTransition < ActiveRecord::Base
155
+ belongs_to :test_model
156
+ end
157
+
158
+ class ARModel < ActiveRecord::Base
159
+
160
+ state_machine :state, initial: :waiting do # log initial state?
161
+ audit_trail
162
+
163
+ event :start do
164
+ transition [:waiting, :stopped] => :started
165
+ end
166
+
167
+ event :stop do
168
+ transition :started => :stopped
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ #
175
+ # Generate tables
176
+ #
177
+ def create_model_table(owner_class, multiple_state_machines = false)
178
+ ActiveRecord::Base.connection.create_table(owner_class.name.tableize) do |t|
179
+ t.string :state unless multiple_state_machines
180
+ t.string :type
181
+
182
+ if multiple_state_machines
183
+ t.string :first
184
+ t.string :second
185
+ t.string :third
186
+ end
187
+
188
+ t.timestamps null: false
189
+ end
190
+ end
191
+
192
+ create_model_table(ARModel)
193
+ create_model_table(ARModelNoInitial)
194
+ create_model_table(ARModelWithContext)
195
+ create_model_table(ARModelWithMultipleContext)
196
+ create_model_table(ARModelWithMultipleStateMachines, true)
197
+
198
+
199
+ def create_transition_table(owner_class, state, add_context = false)
200
+ class_name = "#{owner_class.name}#{state.to_s.camelize}Transition"
201
+ ActiveRecord::Base.connection.create_table(class_name.tableize) do |t|
202
+
203
+ # t.references :"#{owner_class.name.pluralize.demodulize.tableize}"
204
+ t.integer owner_class.name.foreign_key
205
+ t.string :event
206
+ t.string :from
207
+ t.string :to
208
+
209
+ t.string :context if add_context
210
+ t.string :second_context if add_context
211
+ t.string :context_with_args if add_context
212
+ t.datetime :created_at
213
+ end
214
+ end
215
+
216
+ create_transition_table(ARModel, :state)
217
+ create_transition_table(ARModelNoInitial, :state)
218
+ create_transition_table(ARModelWithContext, :state, true)
219
+ create_transition_table(ARModelWithMultipleContext, :state, true)
220
+ create_transition_table(ARModelWithMultipleStateMachines, :first)
221
+ create_transition_table(ARModelWithMultipleStateMachines, :second)
222
+ create_transition_table(ARModelWithMultipleStateMachines, :third)
@@ -0,0 +1,79 @@
1
+ require 'mongoid'
2
+
3
+ ### Setup test database
4
+ Mongoid.load!(File.expand_path('../mongoid.yml', __FILE__), :test)
5
+
6
+ # We probably want to provide a generator for this model and the accompanying migration.
7
+ class MongoidTestModelStateTransition
8
+ include Mongoid::Document
9
+ include Mongoid::Timestamps
10
+ belongs_to :mongoid_test_model
11
+
12
+ field :event, type: String
13
+ field :from, type: String
14
+ field :to, type: String
15
+ end
16
+
17
+ class MongoidTestModelWithMultipleStateMachinesFirstTransition
18
+ include Mongoid::Document
19
+ include Mongoid::Timestamps
20
+ belongs_to :mongoid_test_model
21
+
22
+ field :event, type: String
23
+ field :from, type: String
24
+ field :to, type: String
25
+ end
26
+
27
+ class MongoidTestModelWithMultipleStateMachinesSecondTransition
28
+ include Mongoid::Document
29
+ include Mongoid::Timestamps
30
+ belongs_to :mongoid_test_model
31
+
32
+ field :event, type: String
33
+ field :from, type: String
34
+ field :to, type: String
35
+ end
36
+
37
+ class MongoidTestModel
38
+
39
+ include Mongoid::Document
40
+ include Mongoid::Timestamps
41
+
42
+ state_machine :state, initial: :waiting do # log initial state?
43
+ audit_trail :orm => :mongoid
44
+
45
+ event :start do
46
+ transition [:waiting, :stopped] => :started
47
+ end
48
+
49
+ event :stop do
50
+ transition :started => :stopped
51
+ end
52
+ end
53
+ end
54
+
55
+ class MongoidTestModelDescendant < MongoidTestModel
56
+ include Mongoid::Timestamps
57
+ end
58
+
59
+ class MongoidTestModelWithMultipleStateMachines
60
+
61
+ include Mongoid::Document
62
+ include Mongoid::Timestamps
63
+
64
+ state_machine :first, initial: :beginning do
65
+ audit_trail
66
+
67
+ event :begin_first do
68
+ transition :beginning => :end
69
+ end
70
+ end
71
+
72
+ state_machine :second do
73
+ audit_trail
74
+
75
+ event :begin_second do
76
+ transition nil => :beginning_second
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,6 @@
1
+ test:
2
+ sessions:
3
+ default:
4
+ database: sm_audit_trail
5
+ hosts:
6
+ - localhost:27017
@@ -0,0 +1,225 @@
1
+ # reset integrations so that something like Mongoid is not loaded and conflicting
2
+ require 'state_machines'
3
+ StateMachines::Integrations.reset
4
+
5
+ require 'spec_helper'
6
+ require 'state_machines-activerecord'
7
+ require 'helpers/active_record'
8
+
9
+ describe StateMachines::AuditTrail::Backend::ActiveRecord do
10
+
11
+ context ':initial option' do
12
+ it 'default logs' do
13
+ target = ARModel.new
14
+ # initial transition is built but not saved
15
+ expect(target.new_record?).to be_truthy
16
+ expect(target.ar_model_state_transitions.count).to eq 0
17
+ target.save!
18
+
19
+ # initial transition is saved and should be present
20
+ expect(target.new_record?).to be_falsey
21
+ expect(target.ar_model_state_transitions.count).to eq 1
22
+ state_transition = target.ar_model_state_transitions.first
23
+ expect(state_transition.from).to be_nil
24
+ expect(state_transition.to).to eq 'waiting'
25
+ expect(state_transition.event).to be_nil
26
+ end
27
+
28
+ it 'false skips log' do
29
+ target = ARModelNoInitial.new
30
+ # initial transition is not-built
31
+ expect(target.new_record?).to be_truthy
32
+ expect(target.ar_model_no_initial_state_transitions.count).to eq 0
33
+ target.save!
34
+
35
+ # after save, initial transition is not-saved
36
+ expect(target.new_record?).to be_falsey
37
+ expect(target.ar_model_no_initial_state_transitions.count).to eq 0
38
+ end
39
+ end
40
+
41
+ context '#create_for' do
42
+ it 'should be Backend::ActiveRecord' do
43
+ backend = StateMachines::AuditTrail::Backend.create_for(ARModelWithContextStateTransition, ARModel)
44
+ expect(backend).to be_instance_of(StateMachines::AuditTrail::Backend::ActiveRecord)
45
+ end
46
+
47
+ it 'should create a has many association on the state machine owner' do
48
+ StateMachines::AuditTrail::Backend.create_for(ARModelWithContextStateTransition, ARModel)
49
+ expect(ARModel.reflect_on_association(:ar_model_state_transitions).collection?).to be_truthy
50
+ end
51
+
52
+ it 'should handle namespaced models' do
53
+ StateMachines::AuditTrail::Backend.create_for(ARModelWithContextStateTransition, SomeNamespace::ARModel)
54
+ expect(SomeNamespace::ARModel.reflect_on_association(:ar_model_state_transitions).collection?).to be_truthy
55
+ end
56
+
57
+ it 'should handle namespaced state transition model' do
58
+ StateMachines::AuditTrail::Backend.create_for(SomeNamespace::ARModelStateTransition, ARModel)
59
+ expect(ARModel.reflect_on_association(:ar_model_state_transitions).collection?).to be_truthy
60
+ end
61
+ end
62
+
63
+ context 'single state machine' do
64
+ shared_examples 'audit trail with context' do
65
+ it 'should populate all fields' do
66
+
67
+ # state_transition =target.state_transitions.first
68
+ # expect(state_transition.from).to be_nil
69
+
70
+ expect(target.state_name).to eq :waiting
71
+ target.start!
72
+ expect(target.state_name).to eq :started
73
+
74
+ last_transition = ARModelWithContextStateTransition.where(:ar_model_with_context_id => target.id).last
75
+
76
+ expect(last_transition).not_to be_nil
77
+ expect(last_transition.event).to eq 'start'
78
+ expect(last_transition.from).to eq 'waiting'
79
+ expect(last_transition.to).to eq 'started'
80
+ expect(last_transition.context).not_to be_nil
81
+ expect(last_transition.created_at).to be_within(10.seconds).of(Time.now.utc)
82
+ end
83
+
84
+ it 'do nothing on failed transition' do
85
+ expect { target.stop }.not_to change(ARModelWithContextStateTransition, :count)
86
+ end
87
+ end
88
+
89
+ context 'on created model' do
90
+ let!(:target) { ARModelWithContext.create! }
91
+ include_examples 'audit trail with context'
92
+
93
+ it 'should log multiple events' do
94
+ expect { target.start && target.stop && target.start }.to change(ARModelWithContextStateTransition, :count).by(3)
95
+ end
96
+ end
97
+
98
+ context 'on new model' do
99
+ let!(:target) { ARModelWithContext.new }
100
+ include_examples 'audit trail with context'
101
+
102
+ it 'should log multiple events including the first event from save' do
103
+ expect { target.start && target.stop && target.start }.to change(ARModelWithContextStateTransition, :count).by(4)
104
+ end
105
+ end
106
+
107
+ context 'wants to log a single context' do
108
+ before(:each) do
109
+ StateMachines::AuditTrail::Backend.create_for(ARModelWithContextStateTransition, ARModelWithContext, :context)
110
+ end
111
+
112
+ let!(:target) { ARModelWithContext.create! }
113
+
114
+ it 'should populate all fields' do
115
+ target.start!
116
+ last_transition = ARModelWithContextStateTransition.where(:ar_model_with_context_id => target.id).last
117
+ expect(last_transition.context).to eq target.context
118
+ end
119
+ end
120
+
121
+ context 'wants to log multiple context fields' do
122
+ before(:each) do
123
+ StateMachines::AuditTrail::Backend.create_for(ARModelWithMultipleContextStateTransition, ARModelWithMultipleContext, [:context, :second_context, :context_with_args])
124
+ end
125
+
126
+ let!(:target) { ARModelWithMultipleContext.create! }
127
+
128
+ it 'should populate all fields' do
129
+ target.start!
130
+ last_transition = ARModelWithMultipleContextStateTransition.where(:ar_model_with_multiple_context_id => target.id).last
131
+ expect(last_transition.context).to eq target.context
132
+ expect(last_transition.second_context).to eq target.second_context
133
+ end
134
+
135
+ it 'should log an event with passed arguments' do
136
+ target.start!('one', 'two', 'three', 'for', id: 1)
137
+ last_transition = ARModelWithMultipleContextStateTransition.where(:ar_model_with_multiple_context_id => target.id).last
138
+ expect(last_transition.context_with_args).to eq '1'
139
+ end
140
+ end
141
+
142
+ end
143
+
144
+
145
+ context 'multiple state machines' do
146
+ let!(:target) { ARModelWithMultipleStateMachines.create! }
147
+
148
+ it 'should log a state transition for the affected state machine' do
149
+ expect { target.begin_first! }.to change(ARModelWithMultipleStateMachinesFirstTransition, :count).by(1)
150
+ end
151
+
152
+ it 'should not log a state transition for the unaffected state machine' do
153
+ expect { target.begin_first! }.not_to change(ARModelWithMultipleStateMachinesSecondTransition, :count)
154
+ end
155
+ end
156
+
157
+ context 'with an initial state' do
158
+ let(:target_class) { ARModelWithMultipleStateMachines }
159
+ let(:state_transition_class) { ARModelWithMultipleStateMachinesFirstTransition }
160
+
161
+ it 'should log a state transition for the inital state' do
162
+ expect { target_class.create! }.to change(state_transition_class, :count).by(1)
163
+ end
164
+
165
+ it 'should only set the :to state for the initial transition' do
166
+ target_class.create!
167
+ initial_transition = state_transition_class.last
168
+ expect(initial_transition.event).to be_nil
169
+ expect(initial_transition.from).to be_nil
170
+ expect(initial_transition.to).to eq 'beginning'
171
+ expect(initial_transition.created_at).to be_within(10.seconds).of(Time.now.utc)
172
+ end
173
+ end
174
+
175
+ context 'without an initial state' do
176
+ let(:target_class) { ARModelWithMultipleStateMachines }
177
+ let(:state_transition_class) { ARModelWithMultipleStateMachinesSecondTransition }
178
+
179
+ it 'should not log a transition when the object is created' do
180
+ expect { target_class.create! }.not_to change(state_transition_class, :count)
181
+ end
182
+
183
+ it 'should log a transition for the first event' do
184
+ expect { target_class.create.begin_second! }.to change(state_transition_class, :count).by(1)
185
+ end
186
+
187
+ it 'should not set a value for the :from state on the first transition' do
188
+ target_class.create.begin_second!
189
+ first_transition = state_transition_class.last
190
+ expect(first_transition.event).to eq 'begin_second'
191
+ expect(first_transition.from).to be_nil
192
+ expect(first_transition.to).to eq 'beginning_second'
193
+ expect(first_transition.created_at).to be_within(10.seconds).of(Time.now.utc)
194
+ end
195
+
196
+ it 'should be fine transitioning before saved on an :action => nil state machine' do
197
+ expect {
198
+ machine = target_class.new
199
+ machine.begin_third
200
+ machine.save!
201
+ }.to change(ARModelWithMultipleStateMachinesThirdTransition, :count).by(1)
202
+ end
203
+
204
+ it 'should queue up transitions to be saved before being saved on an :action => nil state machine' do
205
+ expect {
206
+ machine = target_class.new
207
+ machine.begin_third
208
+ machine.end_third
209
+ machine.save!
210
+ }.to change(ARModelWithMultipleStateMachinesThirdTransition, :count).by(2)
211
+ end
212
+ end
213
+
214
+ context 'STI' do
215
+ it 'resolve class name' do
216
+ m = ARModelDescendant.create!
217
+ expect { m.start! }.not_to raise_error
218
+ end
219
+
220
+ it 'resolve class name on own state machine' do
221
+ m = ARModelDescendantWithOwnStateMachines.create!
222
+ expect { m.complete! }.not_to raise_error
223
+ end
224
+ end
225
+ end