stator 0.8.0 → 0.9.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.
data/spec/model_spec.rb DELETED
@@ -1,533 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- describe Stator::Model do
6
-
7
- it "should set the default state after initialization" do
8
- u = User.new
9
- u.state.should eql("pending")
10
- end
11
-
12
- it "should see the initial setting of the state as a change with the initial state as the previous value" do
13
- u = User.new
14
- u.state = "activated"
15
- u.state_in_database.should eql("pending")
16
- end
17
-
18
- it "should not obstruct normal validations" do
19
- u = User.new
20
- u.should_not be_valid
21
- u.errors[:email].grep(/length/).should_not be_empty
22
- end
23
-
24
- it "should ensure a valid state transition when given a bogus state" do
25
- u = User.new
26
- u.state = "anythingelse"
27
-
28
- u.should_not be_valid
29
- u.errors[:state].should eql(["is not a valid state"])
30
- end
31
-
32
- it "should allow creation at any state" do
33
- u = User.new(email: "doug@example.com")
34
- u.state = "hyperactivated"
35
-
36
- u.should be_valid
37
- end
38
-
39
- it "should work normally with active_record dirty methods" do
40
- u = User.new(email: "doug@example.com")
41
-
42
- u.will_save_change_to_state?.should_not be true
43
- u.state_in_database.should eq("pending")
44
- u.state_before_last_save.should be nil
45
-
46
- u.state = "hyperactivated"
47
-
48
- u.will_save_change_to_state?.should be true
49
- u.state_in_database.should eq("pending")
50
- u.state_before_last_save.should be nil
51
-
52
- u.save!
53
-
54
- u.will_save_change_to_state?.should_not be true
55
- u.state_in_database.should eq("hyperactivated")
56
- u.state_before_last_save.should eq("pending")
57
- end
58
-
59
- it "should ensure a valid state transition when given an illegal state based on the current state" do
60
- u = User.new
61
-
62
- allow(u).to receive(:new_record?).and_return(false)
63
-
64
- u.state = "hyperactivated"
65
-
66
- u.should_not be_valid
67
- u.errors[:state].should_not be_empty
68
- end
69
-
70
- it "should not allow a transition that is currently in a `to` state" do
71
- u = User.new(email: "fred@example.com")
72
- u.activate!
73
- u.hyperactivate!
74
-
75
- lambda {
76
- u.hyperactivate!
77
- }.should raise_error(/cannot transition to \"hyperactivated\" from \"hyperactivated\"/)
78
- end
79
-
80
- it "should run conditional validations" do
81
- u = User.new
82
- u.state = "semiactivated"
83
- u.should_not be_valid
84
-
85
- u.errors[:state].should be_empty
86
- u.errors[:email].grep(/format/).should_not be_empty
87
- end
88
-
89
- it "should invoke callbacks" do
90
- u = User.new(activated: true, email: "doug@example.com", name: "doug")
91
- u.activated.should == true
92
-
93
- u.deactivate
94
-
95
- u.activated.should == false
96
- u.state.should eql("deactivated")
97
- u.activated_state_at.should be_nil
98
- u.should be_persisted
99
- end
100
-
101
- it "should conditionally invoke after_save callbacks when use_previous is true" do
102
- u = User.new
103
- u.email = "doug@example.com"
104
-
105
- u.semiactivate!
106
-
107
- u.state.should eql("semiactivated")
108
- u.activation_notification_published.should_not be true
109
-
110
- u.activate!
111
-
112
- u.state.should eql("activated")
113
- u.activation_notification_published.should be true
114
- end
115
-
116
- it "should blow up if the record is invalid and a bang method is used" do
117
- u = User.new(email: "doug@other.com", name: "doug")
118
- lambda {
119
- u.activate!
120
- }.should raise_error(ActiveRecord::RecordInvalid)
121
- end
122
-
123
- it "should allow for other fields to be used other than state" do
124
- a = Animal.new
125
- a.should be_valid
126
-
127
- a.birth!
128
- end
129
-
130
- it "should create implicit transitions for state declarations" do
131
- a = Animal.new
132
- a.should_not be_grown_up
133
- a.status = "grown_up"
134
- a.save
135
- end
136
-
137
- it "should allow multiple machines in the same model" do
138
- f = Farm.new
139
- f.should be_dirty
140
- f.should be_house_dirty
141
-
142
- f.cleanup
143
-
144
- f.should_not be_dirty
145
- f.should be_house_dirty
146
-
147
- f.house_cleanup
148
-
149
- f.should_not be_house_dirty
150
- end
151
-
152
- it "should allow saving to be skipped" do
153
- f = Farm.new
154
- f.cleanup(false)
155
-
156
- f.should_not be_persisted
157
- end
158
-
159
- it "should allow no initial state" do
160
- f = Factory.new
161
- f.state.should be_nil
162
-
163
- f.construct.should eql(true)
164
-
165
- f.state.should eql("constructed")
166
- end
167
-
168
- it "should allow any transition if validations are opted out of" do
169
- u = User.new
170
- u.email = "doug@example.com"
171
-
172
- u.can_hyperactivate?.should eql(false)
173
- u.hyperactivate.should eql(false)
174
-
175
- u.state.should eql("pending")
176
-
177
- u.without_state_transition_validations do
178
- u.can_hyperactivate?.should eql(true)
179
- u.hyperactivate.should eql(true)
180
- end
181
- end
182
-
183
- it "should skip tracking timestamps if opted out of" do
184
- u = User.new
185
- u.email = "doug@example.com"
186
-
187
- u.without_state_transition_tracking do
188
- u.semiactivate!
189
- u.state.should eql("semiactivated")
190
- u.semiactivated_state_at.should be_nil
191
- end
192
-
193
- # Make sure that tracking is ensured back to
194
- # original value
195
- u.activate!
196
- u.activated_state_at.should_not be_nil
197
- end
198
-
199
- it "should skip tracking timestamps if opted out of with thread safety" do
200
- threads = []
201
- skip = User.new(email: "skip@example.com")
202
- nope = User.new(email: "nope@example.com")
203
-
204
- threads << Thread.new do
205
- sleep 0.5
206
- nope.semiactivate!
207
- end
208
- threads << Thread.new do
209
- skip.without_state_transition_tracking do
210
- sleep 1
211
- skip.semiactivate!
212
- end
213
- end
214
-
215
- threads.each(&:join)
216
-
217
- nope.semiactivated_state_at.should_not be_nil
218
- skip.semiactivated_state_at.should be_nil
219
- end
220
-
221
- it "should not inherit _integration cache on dup" do
222
- u = User.new(email: "user@example.com")
223
- u.save!
224
-
225
- u_duped = u.dup
226
-
227
- u.semiactivate!
228
-
229
- u_duped_integration = u_duped.send(:_integration)
230
-
231
- u_duped_integration.state.should_not eql(u.state)
232
- u_duped_integration.instance_values["record"].should eq(u_duped)
233
- end
234
-
235
- describe "helper methods" do
236
- it "should answer the question of whether the state is currently the one invoked" do
237
- a = Animal.new
238
- a.should be_unborn
239
- a.should_not be_born
240
-
241
- a.birth
242
-
243
- a.should be_born
244
- a.should_not be_unborn
245
- end
246
-
247
- it "should determine if it can validly execute a transition" do
248
- a = Animal.new
249
- a.can_birth?.should eql(true)
250
-
251
- a.birth
252
-
253
- a.can_birth?.should eql(false)
254
- end
255
- end
256
-
257
- it "should validate state transitions using the db state after a transaction rollback" do
258
- is_active_record_6_or_higher = Gem::Requirement.new(">= 6.0").satisfied_by?(ActiveRecord.version)
259
-
260
- u = User.create!(email: 'doug@example.com')
261
- u.state.should eql('pending')
262
-
263
- lambda {
264
- ActiveRecord::Base.transaction do
265
- # The state change will be applied to the model object in memory.
266
- # An UPDATE query will be sent to the db, but it will be rolled back
267
- # when the error is raised below.
268
- u.activate!
269
- raise "Some error"
270
- end
271
- }.should raise_error("Some error")
272
-
273
- u.state.should eql("activated")
274
-
275
- # Rails 6.0 fixed a bug where a model's dirty state would be incorrect
276
- # in a scenario like this one, where a model is updated within a transaction,
277
- # and the transaction is then rolled back:
278
- #
279
- # https://github.com/rails/rails/pull/35987
280
- #
281
- # We show this dirty behavior change in Rails 6.0 below to clarify why stator itself
282
- # behaves differently starting in Rails 6.0.
283
- if is_active_record_6_or_higher
284
- # On Rails 6.0 or higher, attribute_in_database is "pending" — which is correct,
285
- # because the db transaction was rolled back.
286
- u.attribute_in_database("state").should eql("pending")
287
-
288
- # Attempting a state change to "hyperactivated" fails, which is correct,
289
- # because the previous state change to "activated" did not succeed.
290
- lambda {
291
- u.hyperactivate!
292
- }.should raise_error(ActiveRecord::RecordInvalid, 'Validation failed: State cannot transition to "hyperactivated" from "pending"')
293
- else
294
- # On Rails < 6.0, attribute_in_database is "activated" — which is incorrect,
295
- # because the db transaction was rolled back.
296
- u.attribute_in_database("state").should eql("activated")
297
-
298
- # On Rails < 6.0, stator incorrectly allows the state change to "hyperactivated"
299
- # because it incorrectly thinks the state has been successfully updated to "activated".
300
- lambda {
301
- u.hyperactivate!
302
- }.should_not raise_error
303
- end
304
- end
305
-
306
- it "should not support multiple state changes made between saves" do
307
- u = User.create!(email: "doug@example.com")
308
- u.state.should eql("pending")
309
-
310
- u.activate(false) # change state, but do not save to db
311
- u.state.should eql("activated")
312
-
313
- lambda {
314
- # Fails because the db state is still "pending", and
315
- # the db state is what stator uses for the "previous" state
316
- # to check that the state transition is valid.
317
- u.hyperactivate!
318
- }.should raise_error(ActiveRecord::RecordInvalid, 'Validation failed: State cannot transition to "hyperactivated" from "pending"')
319
-
320
- # The model is updated in-memory with the new state value,
321
- # but remains invalid.
322
- u.state.should eql("hyperactivated")
323
- u.valid?.should be false
324
- end
325
-
326
- describe "tracker methods" do
327
- before do
328
- Time.zone = "Eastern Time (US & Canada)"
329
- end
330
-
331
- it "should store the initial state timestamp when the record is created" do
332
- a = Animal.new
333
- a.save
334
- a.unborn_status_at.should be_within(1).of(Time.zone.now)
335
- end
336
-
337
- it "should store when a record changed state for the first time" do
338
- a = Animal.new
339
- a.unborn_status_at.should be_nil
340
- a.born_status_at.should be_nil
341
- a.birth
342
- a.unborn_status_at.should be_nil
343
- a.born_status_at.should be_within(1).of(Time.zone.now)
344
- end
345
-
346
- it "should store when a record change states" do
347
- a = Animal.new
348
- a.status_changed_at.should be_nil
349
-
350
- a.birth
351
-
352
- a.status_changed_at.should be_within(1).of(Time.zone.now)
353
-
354
- previous_status_changed_at = a.status_changed_at
355
-
356
- a.name = "new name"
357
- a.save
358
-
359
- a.status_changed_at.should eql(previous_status_changed_at)
360
- end
361
-
362
- it "should prepend the setting of the timestamp so other callbacks can use it" do
363
- u = User.new
364
- u.email = "doug@example.com"
365
-
366
- u.tagged_at.should be_nil
367
- u.semiactivate!
368
-
369
- u.semiactivated_state_at.should_not be_nil
370
- u.tagged_at.should_not be_nil
371
- end
372
-
373
- it "should respect the timestamp if explicitly provided" do
374
- t = Time.at(Time.now.to_i - 3600)
375
-
376
- u = User.new
377
- u.email = "doug@example.com"
378
- u.state = "semiactivated"
379
- u.semiactivated_state_at = t
380
- u.save!
381
-
382
- u.state.should eql("semiactivated")
383
- u.semiactivated_state_at.should eql(t)
384
- end
385
-
386
- it "should respect the timestamp if explicitly provided via create" do
387
- t = Time.at(Time.now.to_i - 3600)
388
-
389
- u = User.create!(
390
- email: "doug@example.com",
391
- state: "semiactivated",
392
- semiactivated_state_at: t
393
- )
394
-
395
- u.state.should eql("semiactivated")
396
- u.semiactivated_state_at.should eql(t)
397
- end
398
-
399
- it "should allow opting into track by namespace" do
400
- z = ZooKeeper.new(name: "Doug")
401
- z.employment_state.should eql("hired")
402
- z.employment_fire!
403
- z.fired_employment_state_at.should_not be_nil
404
-
405
- z.employment_hire!
406
- z.hired_employment_state_at.should_not be_nil
407
-
408
- z.working_start!
409
- z.started_working_state_at.should be_nil
410
- z.working_end!
411
- z.ended_working_state_at.should be_nil
412
- end
413
-
414
- describe "#state_by?" do
415
- it "should be true when the transition is earlier" do
416
- t = Time.now
417
- u = User.create!( email: "doug@example.com", activated_state_at: t)
418
- u.state_by?(:activated, Time.at(t.to_i + 1)).should be true
419
- u.activated_state_by?(Time.at(t.to_i + 1)).should be true
420
- end
421
-
422
- it "should be true when the transition is at the same time" do
423
- t = Time.now
424
- u = User.create!( email: "doug@example.com", activated_state_at: t)
425
- u.state_by?(:activated, t).should be true
426
- u.activated_state_by?(t).should be true
427
- end
428
-
429
- it "should be false when the transition is later" do
430
- t = Time.now
431
- u = User.create!( email: "doug@example.com", activated_state_at: t)
432
- u.state_by?(:activated, Time.at(t.to_i - 1)).should be false
433
- u.activated_state_by?(Time.at(t.to_i - 1)).should be false
434
- end
435
-
436
- it "should be false when the transition is nil" do
437
- t = Time.now
438
- u = User.create!( email: "doug@example.com", activated_state_at: nil)
439
- u.state_by?(:activated, t).should be false
440
- u.activated_state_by?(t).should be false
441
- end
442
-
443
- it "should be true when the transition is not nil and the time is nil" do
444
- u = User.create!( email: "doug@example.com", activated_state_at: Time.now)
445
- u.state_by?(:activated, nil).should be true
446
- u.activated_state_by?(nil).should be true
447
- end
448
-
449
- it "should be false when both are nil" do
450
- u = User.create!(email: "doug@example.com", activated_state_at: nil)
451
- u.state_by?(:activated, nil).should be false
452
- u.activated_state_by?(nil).should be false
453
- end
454
- end
455
- end
456
-
457
- describe "aliasing" do
458
- it "should allow aliasing within the dsl" do
459
- u = User.new(email: "doug@example.com")
460
- u.should respond_to(:active?)
461
- u.should respond_to(:inactive?)
462
-
463
- u.should_not be_active
464
-
465
- u.inactive?
466
- u.should be_inactive
467
-
468
- u.activate!
469
- u.should be_active
470
- u.should_not be_inactive
471
-
472
- u.hyperactivate!
473
- u.should be_active
474
- u.should_not be_inactive
475
-
476
- User::ACTIVE_STATES.should eql(%w[activated hyperactivated])
477
- User::INACTIVE_STATES.should eql(%w[pending deactivated semiactivated])
478
-
479
- is_active_record_72_or_higher = Gem::Requirement.new(">= 7.2").satisfied_by?(ActiveRecord.version)
480
-
481
- if (is_active_record_72_or_higher)
482
- User.active.to_sql.gsub(" ", " ").should eq("SELECT 'users'.* FROM 'users' WHERE 'users'.'state' IN ('activated', 'hyperactivated')")
483
- User.inactive.to_sql.gsub(" ", " ").should eq("SELECT 'users'.* FROM 'users' WHERE 'users'.'state' IN ('pending', 'deactivated', 'semiactivated')")
484
- else
485
- User.active.to_sql.gsub(" ", " ").should eq("SELECT users.* FROM users WHERE users.state IN ('activated', 'hyperactivated')")
486
- User.inactive.to_sql.gsub(" ", " ").should eq("SELECT users.* FROM users WHERE users.state IN ('pending', 'deactivated', 'semiactivated')")
487
- end
488
-
489
- end
490
-
491
- it "should evaluate inverses correctly" do
492
- f = Farm.new
493
- f.house_state = "dirty"
494
- f.should_not be_house_cleaned
495
-
496
- f.house_state = "disgusting"
497
- f.should_not be_house_cleaned
498
-
499
- f.house_state = "clean"
500
- f.should be_house_cleaned
501
- end
502
-
503
- it "should namespace aliases just like everything else" do
504
- f = Farm.new
505
- f.should respond_to(:house_cleaned?)
506
-
507
- f.should_not be_house_cleaned
508
- f.house_cleanup!
509
-
510
- f.should be_house_cleaned
511
- end
512
-
513
- it "should allow for explicit constant and scope names to be provided" do
514
- User.should respond_to(:luke_warmers)
515
- (!!defined?(User::LUKE_WARMERS)).should eql(true)
516
- u = User.new
517
- u.should respond_to(:luke_warm?)
518
- end
519
-
520
- it "should not create constants or scopes by default" do
521
- u = User.new
522
- u.should respond_to(:iced_tea?)
523
- (!!defined?(User::ICED_TEA_STATES)).should eql(false)
524
- User.should_not respond_to(:iced_tea)
525
- end
526
-
527
- it "should determine the full list of states correctly" do
528
- states = User._stator("").states
529
- states.should eql(%w[pending activated deactivated semiactivated hyperactivated])
530
- end
531
- end
532
-
533
- end
data/spec/spec_helper.rb DELETED
@@ -1,34 +0,0 @@
1
- # This file was generated by the `rspec --init` command. Conventionally, all
2
- # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
- # Require this file using `require "spec_helper"` to ensure that it is only
4
- # loaded once.
5
- #
6
- # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
-
8
- require 'active_record'
9
- require 'nulldb/core'
10
- require 'active_support/core_ext'
11
- require 'stator'
12
-
13
- RSpec.configure do |config|
14
- config.expect_with(:rspec) { |c| c.syntax = :should }
15
- config.run_all_when_everything_filtered = true
16
- config.filter_run :focus
17
-
18
- NullDB.configure do |c|
19
- c.project_root = File.dirname(__FILE__)
20
- end
21
-
22
- ActiveRecord::Base.establish_connection(
23
- :adapter => :nulldb,
24
- :schema => 'support/schema.rb'
25
- )
26
-
27
- require 'support/models'
28
-
29
- # Run specs in random order to surface order dependencies. If you find an
30
- # order dependency and want to debug it, you can fix the order by providing
31
- # the seed, which is printed after each run.
32
- # --seed 1234
33
- config.order = 'random'
34
- end