workflow_on_mongoid 0.8.0.3 → 0.8.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,487 @@
1
+ require 'test_helper'
2
+
3
+ require 'mongoid'
4
+ require 'workflow'
5
+ require 'workflow_on_mongoid'
6
+
7
+ # redefine this so it works with mongoid!
8
+ def assert_state(title, expected_state, klass = Order)
9
+ puts 'mongoid assert_state'
10
+ o = klass.where(:title => title).first
11
+ assert_equal expected_state, o.send(klass.workflow_column)
12
+ o
13
+ end
14
+
15
+
16
+ class MongoidOrder
17
+ include Mongoid::Document
18
+
19
+ field :title
20
+
21
+ include Workflow
22
+ workflow do
23
+ state :submitted do
24
+ event :accept, :transitions_to => :accepted, :meta => {:doc_weight => 8} do |reviewer, args|
25
+ end
26
+ end
27
+ state :accepted do
28
+ event :ship, :transitions_to => :shipped
29
+ end
30
+ state :shipped
31
+ end
32
+ end
33
+
34
+ class MongoidLegacyOrder
35
+ include Mongoid::Document
36
+
37
+ field :title
38
+ field :foo_bar
39
+
40
+ include Workflow
41
+
42
+ workflow_column :foo_bar # use this legacy database column for persistence
43
+
44
+ workflow do
45
+ state :submitted do
46
+ event :accept, :transitions_to => :accepted, :meta => {:doc_weight => 8} do |reviewer, args|
47
+ end
48
+ end
49
+ state :accepted do
50
+ event :ship, :transitions_to => :shipped
51
+ end
52
+ state :shipped
53
+ end
54
+ end
55
+
56
+ class MongoidImage
57
+ include Mongoid::Document
58
+
59
+ field :title
60
+ field :status
61
+
62
+ include Workflow
63
+
64
+ workflow_column :status
65
+
66
+ workflow do
67
+ state :unconverted do
68
+ event :convert, :transitions_to => :converted
69
+ end
70
+ state :converted
71
+ end
72
+ end
73
+
74
+ class MongoidSmallImage < MongoidImage
75
+ end
76
+
77
+ class MongoidSpecialSmallImage < MongoidSmallImage
78
+ end
79
+
80
+ class MongoidTest < MongoidTestCase
81
+
82
+ def setup
83
+ super
84
+ MongoidOrder.create!(:title => 'some MongoidOrder', :workflow_state => 'accepted')
85
+ MongoidLegacyOrder.create!(:title => 'some MongoidOrder', :foo_bar => 'accepted')
86
+ end
87
+
88
+ def assert_state(title, expected_state, klass = MongoidOrder)
89
+ o = klass.where(:title => title).first
90
+ assert_equal expected_state, o.send(klass.workflow_column)
91
+ o
92
+ end
93
+
94
+ test 'immediately save the new workflow_state on state machine transition' do
95
+ o = assert_state 'some MongoidOrder', 'accepted'
96
+ assert o.ship!
97
+ assert_state 'some MongoidOrder', 'shipped'
98
+ end
99
+
100
+ test 'immediately save the new workflow_state on state machine transition with custom column name' do
101
+ o = assert_state 'some MongoidOrder', 'accepted', MongoidLegacyOrder
102
+ assert o.ship!
103
+ assert_state 'some MongoidOrder', 'shipped', MongoidLegacyOrder
104
+ end
105
+
106
+ # There was a bug where calling valid? on the record would cause it to be saved
107
+ # see https://github.com/bowsersenior/workflow_on_mongoid/pull/3
108
+ test 'does not save the record when setting initial state' do
109
+ new_order = MongoidOrder.new
110
+ assert new_order.new_record?
111
+
112
+ new_order.valid?
113
+ assert new_order.new_record?
114
+ end
115
+
116
+ test 'persist workflow_state in the db and reload' do
117
+ o = assert_state 'some MongoidOrder', 'accepted'
118
+ assert_equal :accepted, o.current_state.name
119
+ o.ship!
120
+ o.save!
121
+
122
+ assert_state 'some MongoidOrder', 'shipped'
123
+
124
+ o.reload
125
+ assert_equal 'shipped', o.send(:workflow_state)
126
+ end
127
+
128
+ test 'persist workflow_state in the db with_custom_name and reload' do
129
+ o = assert_state 'some MongoidOrder', 'accepted', MongoidLegacyOrder
130
+ assert_equal :accepted, o.current_state.name
131
+ o.ship!
132
+ o.save!
133
+
134
+ assert_state 'some MongoidOrder', 'shipped', MongoidLegacyOrder
135
+
136
+ o.reload
137
+ assert_equal 'shipped', o.send(:foo_bar)
138
+ end
139
+
140
+ test 'default workflow column should be workflow_state' do
141
+ o = assert_state 'some MongoidOrder', 'accepted'
142
+ assert_equal :workflow_state, o.class.workflow_column
143
+ end
144
+
145
+ test 'custom workflow column should be foo_bar' do
146
+ o = assert_state 'some MongoidOrder', 'accepted', MongoidLegacyOrder
147
+ assert_equal :foo_bar, o.class.workflow_column
148
+ end
149
+
150
+ test 'access workflow specification' do
151
+ assert_equal 3, MongoidOrder.workflow_spec.states.length
152
+ assert_equal ['submitted', 'accepted', 'shipped'].sort,
153
+ MongoidOrder.workflow_spec.state_names.map{|n| n.to_s}.sort
154
+ end
155
+
156
+ test 'current state object' do
157
+ o = assert_state 'some MongoidOrder', 'accepted'
158
+ assert_equal 'accepted', o.current_state.to_s
159
+ assert_equal 1, o.current_state.events.length
160
+ end
161
+
162
+ test 'on_entry and on_exit invoked' do
163
+ c = Class.new
164
+ callbacks = mock()
165
+ callbacks.expects(:my_on_exit_new).once
166
+ callbacks.expects(:my_on_entry_old).once
167
+ c.class_eval do
168
+ include Workflow
169
+ workflow do
170
+ state :new do
171
+ event :age, :transitions_to => :old
172
+ end
173
+ on_exit do
174
+ callbacks.my_on_exit_new
175
+ end
176
+ state :old
177
+ on_entry do
178
+ callbacks.my_on_entry_old
179
+ end
180
+ on_exit do
181
+ fail "wrong on_exit executed"
182
+ end
183
+ end
184
+ end
185
+
186
+ o = c.new
187
+ assert_equal 'new', o.current_state.to_s
188
+ o.age!
189
+ end
190
+
191
+ test 'on_transition invoked' do
192
+ callbacks = mock()
193
+ callbacks.expects(:on_tran).once # this is validated at the end
194
+ c = Class.new
195
+ c.class_eval do
196
+ include Workflow
197
+ workflow do
198
+ state :one do
199
+ event :increment, :transitions_to => :two
200
+ end
201
+ state :two
202
+ on_transition do |from, to, triggering_event, *event_args|
203
+ callbacks.on_tran
204
+ end
205
+ end
206
+ end
207
+ assert_not_nil c.workflow_spec.on_transition_proc
208
+ c.new.increment!
209
+ end
210
+
211
+ test 'access event meta information' do
212
+ c = Class.new
213
+ c.class_eval do
214
+ include Workflow
215
+ workflow do
216
+ state :main, :meta => {:importance => 8}
217
+ state :supplemental, :meta => {:importance => 1}
218
+ end
219
+ end
220
+ assert_equal 1, c.workflow_spec.states[:supplemental].meta[:importance]
221
+ end
222
+
223
+ test 'initial state' do
224
+ c = Class.new
225
+ c.class_eval do
226
+ include Workflow
227
+ workflow { state :one; state :two }
228
+ end
229
+ assert_equal 'one', c.new.current_state.to_s
230
+ end
231
+
232
+ test 'nil as initial state' do
233
+ MongoidOrder.create!(:title => 'nil state', :workflow_state => nil)
234
+ o = MongoidOrder.where(:title =>'nil state').first
235
+ assert o.submitted?, 'if workflow_state is nil, the initial state should be assumed'
236
+ assert !o.shipped?
237
+ end
238
+
239
+ test 'initial state immediately set for new objects' do
240
+ o = MongoidOrder.create(:title => 'new object')
241
+ assert_equal 'submitted', o.send(:workflow_state)
242
+ end
243
+
244
+ test 'question methods for state' do
245
+ o = assert_state 'some MongoidOrder', 'accepted'
246
+ assert o.accepted?
247
+ assert !o.shipped?
248
+ end
249
+
250
+ test 'correct exception for event that is not allowed in current state' do
251
+ o = assert_state 'some MongoidOrder', 'accepted'
252
+
253
+ assert_raise Workflow::NoTransitionAllowed do
254
+ o.accept!
255
+ end
256
+ end
257
+
258
+ test 'multiple events with the same name and different arguments lists from different states'
259
+
260
+ test 'implicit transition callback' do
261
+ args = mock()
262
+ args.expects(:my_tran).once # this is validated at the end
263
+ c = Class.new
264
+ c.class_eval do
265
+ include Workflow
266
+ def my_transition(args)
267
+ args.my_tran
268
+ end
269
+ workflow do
270
+ state :one do
271
+ event :my_transition, :transitions_to => :two
272
+ end
273
+ state :two
274
+ end
275
+ end
276
+ c.new.my_transition!(args)
277
+ end
278
+
279
+ test 'Single table inheritance (STI)' do
280
+ class BigMongoidOrder < MongoidOrder
281
+ end
282
+
283
+ bo = BigMongoidOrder.new
284
+ assert bo.submitted?
285
+ assert !bo.accepted?
286
+ end
287
+
288
+ test 'STI when parent changed the workflow_state column' do
289
+ assert_equal 'status', MongoidImage.workflow_column.to_s
290
+ assert_equal 'status', MongoidSmallImage.workflow_column.to_s
291
+ assert_equal 'status', MongoidSpecialSmallImage.workflow_column.to_s
292
+ end
293
+
294
+ test 'Two-level inheritance' do
295
+ class BigMongoidOrder < MongoidOrder
296
+ end
297
+
298
+ class EvenBiggerMongoidOrder < BigMongoidOrder
299
+ end
300
+
301
+ assert EvenBiggerMongoidOrder.new.submitted?
302
+ end
303
+
304
+ test 'Iheritance with workflow definition override' do
305
+ class BigMongoidOrder < MongoidOrder
306
+ end
307
+
308
+ class SpecialBigMongoidOrder < BigMongoidOrder
309
+ workflow do
310
+ state :start_big
311
+ end
312
+ end
313
+
314
+ special = SpecialBigMongoidOrder.new
315
+ assert_equal 'start_big', special.current_state.to_s
316
+ end
317
+
318
+ test 'Better error message for missing target state' do
319
+ class Problem
320
+ include Workflow
321
+ workflow do
322
+ state :initial do
323
+ event :solve, :transitions_to => :solved
324
+ end
325
+ end
326
+ end
327
+ assert_raise Workflow::WorkflowError do
328
+ Problem.new.solve!
329
+ end
330
+ end
331
+
332
+ # Intermixing of transition graph definition (states, transitions)
333
+ # on the one side and implementation of the actions on the other side
334
+ # for a bigger state machine can introduce clutter.
335
+ #
336
+ # To reduce this clutter it is now possible to use state entry- and
337
+ # exit- hooks defined through a naming convention. For example, if there
338
+ # is a state :pending, then you can hook in by defining method
339
+ # `def on_pending_exit(new_state, event, *args)` instead of using a
340
+ # block:
341
+ #
342
+ # state :pending do
343
+ # on_entry do
344
+ # # your implementation here
345
+ # end
346
+ # end
347
+ #
348
+ # If both a function with a name according to naming convention and the
349
+ # on_entry/on_exit block are given, then only on_entry/on_exit block is used.
350
+ test 'on_entry and on_exit hooks in separate methods' do
351
+ c = Class.new
352
+ c.class_eval do
353
+ include Workflow
354
+ attr_reader :history
355
+ def initialize
356
+ @history = []
357
+ end
358
+ workflow do
359
+ state :new do
360
+ event :next, :transitions_to => :next_state
361
+ end
362
+ state :next_state
363
+ end
364
+
365
+ def on_next_state_entry(prior_state, event, *args)
366
+ @history << "on_next_state_entry #{event} #{prior_state} ->"
367
+ end
368
+
369
+ def on_new_exit(new_state, event, *args)
370
+ @history << "on_new_exit #{event} -> #{new_state}"
371
+ end
372
+ end
373
+
374
+ o = c.new
375
+ assert_equal 'new', o.current_state.to_s
376
+ assert_equal [], o.history
377
+ o.next!
378
+ assert_equal ['on_new_exit next -> next_state', 'on_next_state_entry next new ->'], o.history
379
+
380
+ end
381
+
382
+ test 'diagram generation' do
383
+ begin
384
+ $stdout = StringIO.new('', 'w')
385
+ Workflow::create_workflow_diagram(MongoidOrder, 'doc')
386
+ assert_match(/open.+\.pdf/, $stdout.string,
387
+ 'PDF should be generate and a hint be given to the user.')
388
+ ensure
389
+ $stdout = STDOUT
390
+ end
391
+ end
392
+
393
+ test 'halt stops the transition' do
394
+ c = Class.new do
395
+ include Workflow
396
+ workflow do
397
+ state :young do
398
+ event :age, :transitions_to => :old
399
+ end
400
+ state :old
401
+ end
402
+
403
+ def age(by=1)
404
+ halt 'too fast' if by > 100
405
+ end
406
+ end
407
+
408
+ joe = c.new
409
+ assert joe.young?
410
+ joe.age! 120
411
+ assert joe.young?, 'Transition should have been halted'
412
+ assert_equal 'too fast', joe.halted_because
413
+ end
414
+
415
+ test 'halt! raises exception immediately' do
416
+ article_class = Class.new do
417
+ include Workflow
418
+ attr_accessor :too_far
419
+ workflow do
420
+ state :new do
421
+ event :reject, :transitions_to => :rejected
422
+ end
423
+ state :rejected
424
+ end
425
+
426
+ def reject(reason)
427
+ halt! 'We do not reject articles unless the reason is important' \
428
+ unless reason =~ /important/i
429
+ self.too_far = "This line should not be executed"
430
+ end
431
+ end
432
+
433
+ article = article_class.new
434
+ assert article.new?
435
+ assert_raise Workflow::TransitionHalted do
436
+ article.reject! 'Too funny'
437
+ end
438
+ assert_nil article.too_far
439
+ assert article.new?, 'Transition should have been halted'
440
+ article.reject! 'Important: too short'
441
+ assert article.rejected?, 'Transition should happen now'
442
+ end
443
+
444
+ # published gem doesn't have the `can_?` methods yet!
445
+ # test 'can fire event?' do
446
+ # c = Class.new do
447
+ # include Workflow
448
+ # workflow do
449
+ # state :newborn do
450
+ # event :go_to_school, :transitions_to => :schoolboy
451
+ # end
452
+ # state :schoolboy do
453
+ # event :go_to_college, :transitions_to => :student
454
+ # end
455
+ # state :student
456
+ # end
457
+ # end
458
+ #
459
+ # human = c.new
460
+ # assert human.can_go_to_school?
461
+ # assert_equal false, human.can_go_to_college?
462
+ # end
463
+
464
+ test 'workflow graph generation' do
465
+ Dir.chdir('tmp') do
466
+ capture_streams do
467
+ Workflow::create_workflow_diagram(MongoidOrder)
468
+ end
469
+ end
470
+ end
471
+
472
+ test 'workflow graph generation in path with spaces' do
473
+ `mkdir -p '/tmp/Workflow test'`
474
+ capture_streams do
475
+ Workflow::create_workflow_diagram(MongoidOrder, '/tmp/Workflow test')
476
+ end
477
+ end
478
+
479
+ def capture_streams
480
+ old_stdout = $stdout
481
+ $stdout = captured_stdout = StringIO.new
482
+ yield
483
+ $stdout = old_stdout
484
+ captured_stdout
485
+ end
486
+
487
+ end
@@ -0,0 +1,80 @@
1
+ require 'test_helper'
2
+
3
+ unless RUBY_VERSION =~ /^1\.9/
4
+ class MultipleWorkflowsTest < ActiveRecordTestCase
5
+
6
+ test 'multiple workflows' do
7
+
8
+ ActiveRecord::Schema.define do
9
+ create_table :bookings do |t|
10
+ t.string :title, :null => false
11
+ t.string :workflow_state
12
+ t.string :workflow_type
13
+ end
14
+ end
15
+
16
+ exec "INSERT INTO bookings(title, workflow_state, workflow_type) VALUES('booking1', 'initial', 'workflow_1')"
17
+ exec "INSERT INTO bookings(title, workflow_state, workflow_type) VALUES('booking2', 'initial', 'workflow_2')"
18
+
19
+ class Booking < ActiveRecord::Base
20
+ def initialize_workflow
21
+ # define workflow per object instead of per class
22
+ case workflow_type
23
+ when 'workflow_1'
24
+ class << self
25
+ include Workflow
26
+ workflow do
27
+ state :initial do
28
+ event :progress, :transitions_to => :last
29
+ end
30
+ state :last
31
+ end
32
+ end
33
+ when 'workflow_2'
34
+ class << self
35
+ include Workflow
36
+ workflow do
37
+ state :initial do
38
+ event :progress, :transitions_to => :intermediate
39
+ end
40
+ state :intermediate
41
+ state :last
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ def metaclass; class << self; self; end; end
48
+
49
+ def workflow_spec
50
+ metaclass.workflow_spec
51
+ end
52
+
53
+ end
54
+
55
+ booking1 = Booking.find_by_title('booking1')
56
+ booking1.initialize_workflow
57
+
58
+ booking2 = Booking.find_by_title('booking2')
59
+ booking2.initialize_workflow
60
+
61
+ assert booking1.initial?
62
+ booking1.progress!
63
+ assert booking1.last?, 'booking1 should transition to the "last" state'
64
+
65
+ assert booking2.initial?
66
+ booking2.progress!
67
+ assert booking2.intermediate?, 'booking2 should transition to the "intermediate" state'
68
+
69
+ assert booking1.workflow_spec, 'can access the individual workflow specification'
70
+ assert_equal 2, booking1.workflow_spec.states.length
71
+ assert_equal 3, booking2.workflow_spec.states.length
72
+ end
73
+
74
+ class Object
75
+ # The hidden singleton lurks behind everyone
76
+ def metaclass; class << self; self; end; end
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,37 @@
1
+ require 'workflow'
2
+ class Article
3
+ include Workflow
4
+ workflow do
5
+ state :new do
6
+ event :submit, :transitions_to => :awaiting_review
7
+ end
8
+ state :awaiting_review do
9
+ event :review, :transitions_to => :being_reviewed
10
+ end
11
+ state :being_reviewed do
12
+ event :accept, :transitions_to => :accepted
13
+ event :reject, :transitions_to => :rejected
14
+ end
15
+ state :accepted
16
+ state :rejected
17
+ end
18
+ end
19
+
20
+ article = Article.new
21
+ article.accepted? # => false
22
+ article.new? # => true
23
+ article.submit!
24
+ article.review!
25
+
26
+ puts article.current_state # => being_reviewed
27
+
28
+
29
+ class Article
30
+ def reject
31
+ puts "send email to the author here explaining the reason for the rejection"
32
+ end
33
+ end
34
+
35
+ article.reject! # will cause a state transition, would persist the new
36
+ # state (if inherited from ActiveRecord), and invoke the callback -
37
+ # send email to the author.
@@ -0,0 +1,57 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.setup
4
+
5
+ require 'test/unit'
6
+ require 'active_record'
7
+ require 'workflow_on_mongoid'
8
+
9
+ class << Test::Unit::TestCase
10
+ def test(name, &block)
11
+ test_name = :"test_#{name.gsub(' ','_')}"
12
+ raise ArgumentError, "#{test_name} is already defined" if self.instance_methods.include? test_name.to_s
13
+ if block
14
+ define_method test_name, &block
15
+ else
16
+ puts "PENDING: #{name}"
17
+ end
18
+ end
19
+ end
20
+
21
+ class ActiveRecordTestCase < Test::Unit::TestCase
22
+ def exec(sql)
23
+ ActiveRecord::Base.connection.execute sql
24
+ end
25
+
26
+ def setup
27
+ ActiveRecord::Base.establish_connection(
28
+ :adapter => "sqlite3",
29
+ :database => ":memory:" #"tmp/test"
30
+ )
31
+
32
+ # eliminate ActiveRecord warning. TODO: delete as soon as ActiveRecord is fixed
33
+ ActiveRecord::Base.connection.reconnect!
34
+ end
35
+
36
+ def teardown
37
+ ActiveRecord::Base.connection.disconnect!
38
+ end
39
+
40
+ def default_test
41
+ end
42
+ end
43
+
44
+ require 'mongoid'
45
+ class MongoidTestCase < Test::Unit::TestCase
46
+ Mongoid.configure do |config|
47
+ config.master = Mongo::Connection.new('127.0.0.1', 27017).db("workflow_on_mongoid")
48
+ end
49
+
50
+ def teardown
51
+ Mongoid.master.collections.select do |collection|
52
+ collection.name !~ /system/
53
+ end.each(&:drop)
54
+ end
55
+
56
+ def default_test; end
57
+ end
@@ -0,0 +1,57 @@
1
+ # require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ require 'test_helper'
4
+
5
+ require 'workflow'
6
+
7
+ class WithoutWorkflowTest < Test::Unit::TestCase
8
+ class Article
9
+ include Workflow
10
+ workflow do
11
+ state :new do
12
+ event :submit, :transitions_to => :awaiting_review
13
+ end
14
+ state :awaiting_review do
15
+ event :review, :transitions_to => :being_reviewed
16
+ end
17
+ state :being_reviewed do
18
+ event :accept, :transitions_to => :accepted
19
+ event :reject, :transitions_to => :rejected
20
+ end
21
+ state :accepted
22
+ state :rejected
23
+ end
24
+ end
25
+
26
+ def test_readme_example_article
27
+ article = Article.new
28
+ assert article.new?
29
+ end
30
+
31
+ test 'better error message on transitions_to typo' do
32
+ assert_raise Workflow::WorkflowDefinitionError do
33
+ Class.new do
34
+ include Workflow
35
+ workflow do
36
+ state :new do
37
+ event :event1, :transitionnn => :next # missing transitions_to target
38
+ end
39
+ state :next
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ test 'check transition_to alias' do
46
+ Class.new do
47
+ include Workflow
48
+ workflow do
49
+ state :new do
50
+ event :event1, :transition_to => :next
51
+ end
52
+ state :next
53
+ end
54
+ end
55
+ end
56
+ end
57
+