stepper_motor 0.1.7 → 0.1.8

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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +12 -0
  3. data/.github/workflows/ci.yml +51 -0
  4. data/CHANGELOG.md +77 -2
  5. data/Gemfile +11 -0
  6. data/README.md +13 -374
  7. data/Rakefile +21 -3
  8. data/bin/test +5 -0
  9. data/lib/generators/install_generator.rb +6 -1
  10. data/lib/generators/stepper_motor_migration_003.rb.erb +6 -0
  11. data/lib/generators/stepper_motor_migration_004.rb.erb +26 -0
  12. data/lib/stepper_motor/forward_scheduler.rb +8 -4
  13. data/lib/stepper_motor/journey/flow_control.rb +58 -0
  14. data/lib/stepper_motor/journey/recovery.rb +34 -0
  15. data/lib/stepper_motor/journey.rb +85 -84
  16. data/lib/stepper_motor/perform_step_job_v2.rb +2 -2
  17. data/lib/stepper_motor/recover_stuck_journeys_job_v1.rb +3 -1
  18. data/lib/stepper_motor/step.rb +70 -5
  19. data/lib/stepper_motor/version.rb +1 -1
  20. data/lib/stepper_motor.rb +0 -1
  21. data/lib/tasks/stepper_motor_tasks.rake +8 -0
  22. data/manual/MANUAL.md +538 -0
  23. data/rbi/stepper_motor.rbi +459 -0
  24. data/sig/stepper_motor.rbs +406 -3
  25. data/stepper_motor.gemspec +49 -0
  26. data/test/dummy/Rakefile +8 -0
  27. data/test/dummy/app/assets/stylesheets/application.css +1 -0
  28. data/test/dummy/app/controllers/application_controller.rb +6 -0
  29. data/test/dummy/app/helpers/application_helper.rb +4 -0
  30. data/test/dummy/app/jobs/application_job.rb +9 -0
  31. data/test/dummy/app/mailers/application_mailer.rb +6 -0
  32. data/test/dummy/app/models/application_record.rb +5 -0
  33. data/test/dummy/app/views/layouts/application.html.erb +27 -0
  34. data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
  35. data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  36. data/test/dummy/app/views/pwa/manifest.json.erb +22 -0
  37. data/test/dummy/app/views/pwa/service-worker.js +26 -0
  38. data/test/dummy/bin/dev +2 -0
  39. data/test/dummy/bin/rails +4 -0
  40. data/test/dummy/bin/rake +4 -0
  41. data/test/dummy/bin/setup +34 -0
  42. data/test/dummy/config/application.rb +28 -0
  43. data/test/dummy/config/boot.rb +7 -0
  44. data/test/dummy/config/cable.yml +10 -0
  45. data/test/dummy/config/database.yml +32 -0
  46. data/test/dummy/config/environment.rb +7 -0
  47. data/test/dummy/config/environments/development.rb +71 -0
  48. data/test/dummy/config/environments/production.rb +91 -0
  49. data/test/dummy/config/environments/test.rb +55 -0
  50. data/test/dummy/config/initializers/content_security_policy.rb +27 -0
  51. data/test/dummy/config/initializers/filter_parameter_logging.rb +10 -0
  52. data/test/dummy/config/initializers/inflections.rb +18 -0
  53. data/test/dummy/config/initializers/stepper_motor.rb +3 -0
  54. data/test/dummy/config/locales/en.yml +31 -0
  55. data/test/dummy/config/puma.rb +40 -0
  56. data/test/dummy/config/routes.rb +16 -0
  57. data/test/dummy/config/storage.yml +34 -0
  58. data/test/dummy/config.ru +8 -0
  59. data/test/dummy/db/migrate/20250520094921_stepper_motor_migration_001.rb +38 -0
  60. data/test/dummy/db/migrate/20250520094922_stepper_motor_migration_002.rb +8 -0
  61. data/test/dummy/db/migrate/20250522212312_stepper_motor_migration_003.rb +7 -0
  62. data/test/dummy/db/migrate/20250525110812_stepper_motor_migration_004.rb +28 -0
  63. data/test/dummy/db/schema.rb +37 -0
  64. data/test/dummy/public/400.html +114 -0
  65. data/test/dummy/public/404.html +114 -0
  66. data/test/dummy/public/406-unsupported-browser.html +114 -0
  67. data/test/dummy/public/422.html +114 -0
  68. data/test/dummy/public/500.html +114 -0
  69. data/test/dummy/public/icon.png +0 -0
  70. data/test/dummy/public/icon.svg +3 -0
  71. data/test/side_effects_helper.rb +67 -0
  72. data/test/stepper_motor/cyclic_scheduler_test.rb +77 -0
  73. data/{spec/stepper_motor/forward_scheduler_spec.rb → test/stepper_motor/forward_scheduler_test.rb} +9 -10
  74. data/test/stepper_motor/journey/exception_handling_test.rb +89 -0
  75. data/test/stepper_motor/journey/flow_control_test.rb +78 -0
  76. data/test/stepper_motor/journey/idempotency_test.rb +65 -0
  77. data/test/stepper_motor/journey/step_definition_test.rb +187 -0
  78. data/test/stepper_motor/journey/uniqueness_test.rb +48 -0
  79. data/test/stepper_motor/journey_test.rb +352 -0
  80. data/{spec/stepper_motor/recover_stuck_journeys_job_spec.rb → test/stepper_motor/recover_stuck_journeys_job_test.rb} +14 -14
  81. data/{spec/stepper_motor/recovery_spec.rb → test/stepper_motor/recovery_test.rb} +27 -27
  82. data/test/stepper_motor/test_helper_test.rb +44 -0
  83. data/test/stepper_motor_test.rb +9 -0
  84. data/test/test_helper.rb +46 -0
  85. metadata +120 -24
  86. data/.rspec +0 -3
  87. data/.ruby-version +0 -1
  88. data/.standard.yml +0 -4
  89. data/.yardopts +0 -1
  90. data/spec/helpers/side_effects.rb +0 -85
  91. data/spec/spec_helper.rb +0 -90
  92. data/spec/stepper_motor/cyclic_scheduler_spec.rb +0 -68
  93. data/spec/stepper_motor/generator_spec.rb +0 -16
  94. data/spec/stepper_motor/journey_spec.rb +0 -401
  95. data/spec/stepper_motor/test_helper_spec.rb +0 -48
  96. data/spec/stepper_motor_spec.rb +0 -7
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class CyclicSchedulerTest < ActiveSupport::TestCase
6
+ include ActiveJob::TestHelper
7
+
8
+ setup do
9
+ @previous_scheduler = StepperMotor.scheduler
10
+ StepperMotor::Journey.delete_all
11
+ end
12
+
13
+ teardown do
14
+ StepperMotor.scheduler = @previous_scheduler
15
+ end
16
+
17
+ def far_future_journey_class
18
+ @klass ||= create_journey_subclass do
19
+ step :do_thing, wait: 40.minutes do
20
+ raise "We do not test this so it should never run"
21
+ end
22
+ end
23
+ end
24
+
25
+ test "does not schedule a journey which is too far in the future" do
26
+ scheduler = StepperMotor::CyclicScheduler.new(cycle_duration: 30.seconds)
27
+ StepperMotor.scheduler = scheduler
28
+
29
+ assert_no_enqueued_jobs do
30
+ far_future_journey_class.create!
31
+ end
32
+
33
+ assert_no_enqueued_jobs do
34
+ scheduler.run_scheduling_cycle
35
+ end
36
+ end
37
+
38
+ test "for a job inside the current scheduling cycle, enqueues the job immediately" do
39
+ scheduler = StepperMotor::CyclicScheduler.new(cycle_duration: 40.minutes)
40
+ StepperMotor.scheduler = scheduler
41
+
42
+ assert_enqueued_jobs 1, only: StepperMotor::PerformStepJobV2 do
43
+ far_future_journey_class.create!
44
+ end
45
+ end
46
+
47
+ test "also schedules journeys which had to run in the past" do
48
+ scheduler = StepperMotor::CyclicScheduler.new(cycle_duration: 10.seconds)
49
+ StepperMotor.scheduler = scheduler
50
+
51
+ journey = nil
52
+ assert_no_enqueued_jobs do
53
+ journey = far_future_journey_class.create!
54
+ end
55
+ journey.update!(next_step_to_be_performed_at: 10.minutes.ago)
56
+
57
+ assert_enqueued_jobs 1, only: StepperMotor::PerformStepJobV2 do
58
+ scheduler.run_scheduling_cycle
59
+ end
60
+ end
61
+
62
+ test "performs the scheduling job without raising exceptions even if the cycling scheduler is not the one active" do
63
+ StepperMotor.scheduler = StepperMotor::ForwardScheduler.new
64
+ assert_nothing_raised do
65
+ StepperMotor::CyclicScheduler::RunSchedulingCycleJob.new.perform
66
+ end
67
+ end
68
+
69
+ test "performs the scheduling job without raising exceptions if the cycling scheduler is the active" do
70
+ scheduler = StepperMotor::CyclicScheduler.new(cycle_duration: 10.seconds)
71
+ StepperMotor.scheduler = scheduler
72
+
73
+ assert_nothing_raised do
74
+ StepperMotor::CyclicScheduler::RunSchedulingCycleJob.new.perform
75
+ end
76
+ end
77
+ end
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../spec_helper"
3
+ require "test_helper"
4
4
 
5
- RSpec.describe "StepperMotor::ForwardScheduler" do
5
+ class ForwardSchedulerTest < ActiveSupport::TestCase
6
6
  include ActiveJob::TestHelper
7
7
 
8
- before do
8
+ setup do
9
9
  @previous_scheduler = StepperMotor.scheduler
10
10
  StepperMotor::Journey.delete_all
11
11
  end
12
12
 
13
- after do
13
+ teardown do
14
14
  StepperMotor.scheduler = @previous_scheduler
15
15
  end
16
16
 
@@ -22,20 +22,19 @@ RSpec.describe "StepperMotor::ForwardScheduler" do
22
22
  end
23
23
  end
24
24
 
25
- it "schedules a journey 40 minutes ahead" do
25
+ test "schedules a journey 40 minutes ahead" do
26
26
  scheduler = StepperMotor::ForwardScheduler.new
27
27
  StepperMotor.scheduler = scheduler
28
28
 
29
- expect(scheduler).to receive(:schedule).with(instance_of(far_future_journey_class)).once.and_call_original
30
29
  _journey = far_future_journey_class.create!
31
30
 
32
- expect(enqueued_jobs.size).to eq(1)
31
+ assert_equal 1, enqueued_jobs.size
33
32
  job = enqueued_jobs.first
34
33
 
35
- expect(job["job_class"]).to eq("StepperMotor::PerformStepJobV2")
36
- expect(job["scheduled_at"]).not_to be_nil
34
+ assert_equal "StepperMotor::PerformStepJobV2", job["job_class"]
35
+ assert_not_nil job["scheduled_at"]
37
36
 
38
37
  scheduled_at = Time.parse(job["scheduled_at"])
39
- expect(scheduled_at).to be_within(5.seconds).of(40.minutes.from_now)
38
+ assert_in_delta 40.minutes.from_now.to_f, scheduled_at.to_f, 5.seconds.to_f
40
39
  end
41
40
  end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class ExceptionHandlingTest < ActiveSupport::TestCase
6
+ # See below.
7
+ self.use_transactional_tests = false
8
+
9
+ class CustomEx < StandardError
10
+ end
11
+
12
+ test "with :reattempt!, reattempts the failing step and bumps the idempotency key" do
13
+ faulty_journey_class = create_journey_subclass do
14
+ step on_exception: :reattempt! do
15
+ raise CustomEx, "Something went wrong"
16
+ end
17
+ end
18
+
19
+ faulty_journey = faulty_journey_class.create!
20
+ assert faulty_journey.ready?
21
+ ik_before_step = faulty_journey.idempotency_key
22
+
23
+ assert_raises(CustomEx) { faulty_journey.perform_next_step! }
24
+
25
+ assert faulty_journey.persisted?
26
+ refute faulty_journey.changed?
27
+ assert faulty_journey.ready?
28
+ refute_equal faulty_journey.idempotency_key, ik_before_step
29
+ end
30
+
31
+ test "with :cancel!, cancels at the failig step" do
32
+ faulty_journey_class = create_journey_subclass do
33
+ step on_exception: :cancel! do
34
+ raise CustomEx, "Something went wrong"
35
+ end
36
+ end
37
+
38
+ faulty_journey = faulty_journey_class.create!
39
+ assert faulty_journey.ready?
40
+ faulty_journey.idempotency_key
41
+
42
+ assert_raises(CustomEx) { faulty_journey.perform_next_step! }
43
+
44
+ assert faulty_journey.persisted?
45
+ refute faulty_journey.changed?
46
+ assert faulty_journey.canceled?
47
+ end
48
+
49
+ test "pauses the journey by default at the failig step" do
50
+ faulty_journey_class = create_journey_subclass do
51
+ step do
52
+ raise CustomEx, "Something went wrong"
53
+ end
54
+ end
55
+
56
+ faulty_journey = faulty_journey_class.create!
57
+ assert faulty_journey.ready?
58
+ faulty_journey.idempotency_key
59
+
60
+ assert_raises(CustomEx) { faulty_journey.perform_next_step! }
61
+
62
+ assert faulty_journey.persisted?
63
+ refute faulty_journey.changed?
64
+ assert faulty_journey.paused?
65
+ end
66
+
67
+ test "is able to get the journey into reattempt even if the step has caused an invalid transaction" do
68
+ # We need to test a situation where a Journey causes a database transaction
69
+ # becoming invalid due to an invalid statement. Since we work with the same database
70
+ # as the code of the step, we won't be able to perform any SQL statments if the transaction
71
+ # gets left in the broken state and is not rolled back before we try to persist the failing
72
+ # Journey.
73
+
74
+ faulty_journey_class = create_journey_subclass do
75
+ step on_exception: :reattempt! do
76
+ StepperMotor::Journey.connection.execute("KERFUFFLE")
77
+ end
78
+ end
79
+
80
+ faulty_journey = faulty_journey_class.create!
81
+ assert faulty_journey.ready?
82
+ ik_before_step = faulty_journey.idempotency_key
83
+ assert_raises(ActiveRecord::StatementInvalid) { faulty_journey.perform_next_step! }
84
+
85
+ assert faulty_journey.persisted?
86
+ refute faulty_journey.changed?
87
+ refute_equal faulty_journey.idempotency_key, ik_before_step
88
+ end
89
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class FlowControlTest < ActiveSupport::TestCase
6
+ include ActiveJob::TestHelper
7
+ include SideEffects::TestHelper
8
+
9
+ test "can pause a Journey during a step" do
10
+ pausing_journey = create_journey_subclass do
11
+ step do
12
+ SideEffects.touch! "before pausing"
13
+ pause!
14
+ SideEffects.touch! "after pausing"
15
+ end
16
+ end
17
+
18
+ journey = pausing_journey.create!
19
+ assert journey.ready?
20
+
21
+ assert_produced_side_effects("before pausing") do
22
+ assert_did_not_produce_side_effects("after pausing") do
23
+ journey.perform_next_step!
24
+ end
25
+ end
26
+ assert journey.paused?
27
+ end
28
+
29
+ test "schedules a job on resume" do
30
+ pausing_journey = create_journey_subclass do
31
+ step do
32
+ SideEffects.touch! "after resume"
33
+ end
34
+ end
35
+
36
+ journey = pausing_journey.create!
37
+ journey.pause!
38
+ assert journey.paused?
39
+
40
+ clear_enqueued_jobs
41
+
42
+ assert_produced_side_effects("after resume") do
43
+ journey.resume!
44
+ perform_enqueued_jobs
45
+ end
46
+ end
47
+
48
+ test "changes the idempotency key at resume" do
49
+ pausing_journey = create_journey_subclass do
50
+ step do
51
+ SideEffects.touch! "after resume"
52
+ end
53
+ end
54
+
55
+ journey = pausing_journey.create!
56
+ journey.pause!
57
+ assert journey.paused?
58
+ ik_before = journey.idempotency_key
59
+
60
+ journey.resume!
61
+ assert journey.persisted?
62
+ refute_equal ik_before, journey.idempotency_key
63
+ end
64
+
65
+ test "does not perform the step on job that has been paused" do
66
+ pausing_journey = create_journey_subclass do
67
+ step do
68
+ SideEffects.touch! "should not run"
69
+ end
70
+ end
71
+
72
+ journey = pausing_journey.create!
73
+ journey.pause!
74
+ assert journey.paused?
75
+
76
+ assert_no_side_effects { journey.perform_next_step! }
77
+ end
78
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class JourneyIdempotencyTest < ActiveSupport::TestCase
6
+ include ActiveJob::TestHelper
7
+ include SideEffects::TestHelper
8
+
9
+ test "does not perform the step if the idempotency key passed does not match the one stored" do
10
+ journey_class = create_journey_subclass do
11
+ step do
12
+ raise "Should not happen"
13
+ end
14
+ end
15
+
16
+ journey = journey_class.create!
17
+
18
+ assert_predicate journey, :ready?
19
+ assert journey.idempotency_key
20
+ assert_nothing_raised do
21
+ journey.perform_next_step!(idempotency_key: journey.idempotency_key + "n")
22
+ end
23
+ assert_predicate journey, :ready?
24
+ end
25
+
26
+ test "does perform the step if the idempotency key is set but not passed to perform_next_step!" do
27
+ journey_class = create_journey_subclass do
28
+ step do
29
+ SideEffects.touch! :with_ik
30
+ end
31
+ end
32
+
33
+ journey = journey_class.create!
34
+
35
+ assert_predicate journey, :ready?
36
+ assert journey.idempotency_key
37
+ assert_produced_side_effects(:with_ik) do
38
+ journey.perform_next_step!
39
+ end
40
+ assert_predicate journey, :finished?
41
+ end
42
+
43
+ test "updates the idempotency key when a step gets reattempted" do
44
+ some_journey_class = create_journey_subclass do
45
+ step :one do
46
+ reattempt! wait: 2.minutes
47
+ end
48
+ end
49
+ freeze_time
50
+
51
+ journey = some_journey_class.create!
52
+
53
+ assert_equal "one", journey.next_step_name
54
+ assert journey.idempotency_key.present? # Since it was scheduled for the initial step
55
+ previous_idempotency_key = journey.idempotency_key
56
+
57
+ perform_enqueued_jobs # Should be reattempted
58
+
59
+ journey.reload
60
+
61
+ assert_equal "one", journey.next_step_name
62
+ assert journey.idempotency_key
63
+ refute_equal journey.idempotency_key, previous_idempotency_key
64
+ end
65
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+ require "minitest/mock"
5
+
6
+ class StepDefinitionTest < ActiveSupport::TestCase
7
+ include ActiveJob::TestHelper
8
+ include SideEffects::TestHelper
9
+
10
+ test "requires either a block or a name" do
11
+ assert_raises(StepperMotor::StepConfigurationError) do
12
+ create_journey_subclass do
13
+ step
14
+ end
15
+ end
16
+ end
17
+
18
+ test "passes any additional options to the step definition" do
19
+ step_def = StepperMotor::Step.new(name: "a_step", seq: 1, on_exception: :reattempt!)
20
+ assert_extra_arguments = ->(**options) {
21
+ assert options.key?(:extra)
22
+ # Return the original definition
23
+ step_def
24
+ }
25
+
26
+ StepperMotor::Step.stub :new, assert_extra_arguments do
27
+ create_journey_subclass do
28
+ step extra: true do
29
+ # noop
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ test "returns the created step definition" do
36
+ test_case = self # To pass it into the class_eval of create_journey_subclass
37
+ create_journey_subclass do
38
+ step_def1 = step do
39
+ # noop
40
+ end
41
+
42
+ step_def2 = step :another_step do
43
+ # noop
44
+ end
45
+
46
+ test_case.assert_kind_of StepperMotor::Step, step_def1
47
+ test_case.assert_kind_of StepperMotor::Step, step_def2
48
+ test_case.assert_equal "another_step", step_def2.name
49
+ end
50
+ end
51
+
52
+ test "allows steps to be defined as instance method names" do
53
+ journey_class = create_journey_subclass do
54
+ step :one
55
+ step :two
56
+
57
+ def one
58
+ SideEffects.touch!("from method one")
59
+ end
60
+
61
+ def two
62
+ SideEffects.touch!("from method two")
63
+ end
64
+ end
65
+
66
+ journey = journey_class.create!
67
+
68
+ assert_produced_side_effects("from method one", "from method two") do
69
+ 2.times { journey.perform_next_step! }
70
+ end
71
+
72
+ assert journey.finished?
73
+ end
74
+
75
+ test "raises a custom NoMethodError when a blockless step was defined but no method to carry it" do
76
+ journey_class = create_journey_subclass do
77
+ step :one
78
+ end
79
+
80
+ journey = journey_class.create!
81
+
82
+ ex = assert_raises(NoMethodError) do
83
+ journey.perform_next_step!
84
+ end
85
+
86
+ assert_kind_of NoMethodError, ex
87
+ assert_match(/No block or method/, ex.message)
88
+ end
89
+
90
+ test "allows `step def'" do
91
+ journey_class = create_journey_subclass do
92
+ step def one
93
+ SideEffects.touch!(:woof)
94
+ end
95
+ end
96
+
97
+ journey = journey_class.create!
98
+ assert_produced_side_effects(:woof) do
99
+ journey.perform_next_step!
100
+ end
101
+ end
102
+
103
+ test "adds steps to step_definitions" do
104
+ journey_class = create_journey_subclass do
105
+ step :one do
106
+ # noop
107
+ end
108
+ step :two, wait: 20.minutes do
109
+ # noop
110
+ end
111
+ end
112
+
113
+ assert_kind_of Array, journey_class.step_definitions
114
+ assert_equal 2, journey_class.step_definitions.length
115
+
116
+ step_one, step_two = *journey_class.step_definitions
117
+
118
+ assert_equal "one", step_one.name
119
+ assert_equal 0, step_one.wait
120
+
121
+ assert_equal "two", step_two.name
122
+ assert_equal 20.minutes, step_two.wait
123
+ end
124
+
125
+ test "gives automatic names to anonymous steps" do
126
+ journey_class = create_journey_subclass do
127
+ step :one do
128
+ # noop
129
+ end
130
+ step wait: 20.minutes do
131
+ # noop
132
+ end
133
+ end
134
+
135
+ assert_kind_of Array, journey_class.step_definitions
136
+ assert_equal 2, journey_class.step_definitions.length
137
+
138
+ step_one, step_two = *journey_class.step_definitions
139
+
140
+ assert_equal "one", step_one.name
141
+ assert_equal "step_2", step_two.name
142
+ end
143
+
144
+ test "does not allow invalid values for after: and wait:" do
145
+ assert_raises(ArgumentError) do
146
+ create_journey_subclass do
147
+ step after: 10.hours do
148
+ # pass
149
+ end
150
+
151
+ step after: 5.hours do
152
+ # pass
153
+ end
154
+ end
155
+ end
156
+
157
+ assert_raises(ArgumentError) do
158
+ create_journey_subclass do
159
+ step wait: -5.hours do
160
+ # pass
161
+ end
162
+ end
163
+ end
164
+
165
+ assert_raises(ArgumentError) do
166
+ create_journey_subclass do
167
+ step after: 5.hours, wait: 2.seconds do
168
+ # pass
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ test "forbids multiple steps with the same name within a journey" do
175
+ assert_raises(ArgumentError) do
176
+ create_journey_subclass do
177
+ step :foo do
178
+ true
179
+ end
180
+
181
+ step "foo" do
182
+ true
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UniquenessTest < ActiveSupport::TestCase
6
+ test "forbids multiple similar journeys for the same hero at the same time unless allow_multiple is set" do
7
+ actor_class = create_journey_subclass
8
+ hero = actor_class.create!
9
+
10
+ exclusive_journey_class = create_journey_subclass do
11
+ step do
12
+ raise "The step should never be entered as we are not testing the step itself here"
13
+ end
14
+ end
15
+
16
+ assert_nothing_raised do
17
+ 2.times { exclusive_journey_class.create! }
18
+ end
19
+
20
+ assert_raises(ActiveRecord::RecordNotUnique) do
21
+ 2.times { exclusive_journey_class.create!(hero: hero) }
22
+ end
23
+
24
+ assert_nothing_raised do
25
+ 2.times { exclusive_journey_class.create!(hero: hero, allow_multiple: true) }
26
+ end
27
+ end
28
+
29
+ test "forbids multiple similar journeys for the same hero even if one of them is paused" do
30
+ actor_class = create_journey_subclass
31
+ hero = actor_class.create!
32
+
33
+ exclusive_journey_class = create_journey_subclass do
34
+ step do
35
+ raise "The step should never be entered as we are not testing the step itself here"
36
+ end
37
+ end
38
+
39
+ ready_journey = exclusive_journey_class.create!(hero:)
40
+ assert ready_journey.ready?
41
+ ready_journey.pause!
42
+ assert ready_journey.paused?
43
+
44
+ assert_raises(ActiveRecord::RecordNotUnique) {
45
+ exclusive_journey_class.create!(hero:)
46
+ }
47
+ end
48
+ end