state_machines-audit_trail 1.0.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.
@@ -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