statesman 10.2.3 → 12.1.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/tests.yml +17 -11
  3. data/.gitignore +0 -3
  4. data/.rspec +1 -0
  5. data/.rubocop.yml +1 -1
  6. data/.ruby-version +1 -1
  7. data/CHANGELOG.md +183 -43
  8. data/CONTRIBUTING.md +14 -13
  9. data/Gemfile +2 -2
  10. data/README.md +120 -62
  11. data/docs/COMPATIBILITY.md +2 -2
  12. data/lib/generators/statesman/generator_helpers.rb +1 -1
  13. data/lib/statesman/adapters/active_record.rb +26 -33
  14. data/lib/statesman/adapters/active_record_queries.rb +2 -2
  15. data/lib/statesman/adapters/active_record_transition.rb +5 -1
  16. data/lib/statesman/callback.rb +2 -2
  17. data/lib/statesman/config.rb +3 -10
  18. data/lib/statesman/machine.rb +8 -0
  19. data/lib/statesman/version.rb +1 -1
  20. data/lib/statesman.rb +3 -5
  21. data/lib/tasks/statesman.rake +2 -2
  22. data/spec/generators/statesman/active_record_transition_generator_spec.rb +7 -1
  23. data/spec/generators/statesman/migration_generator_spec.rb +5 -1
  24. data/spec/spec_helper.rb +34 -8
  25. data/spec/statesman/adapters/active_record_queries_spec.rb +1 -3
  26. data/spec/statesman/adapters/active_record_spec.rb +58 -39
  27. data/spec/statesman/adapters/active_record_transition_spec.rb +5 -2
  28. data/spec/statesman/adapters/memory_spec.rb +0 -1
  29. data/spec/statesman/adapters/memory_transition_spec.rb +0 -1
  30. data/spec/statesman/adapters/shared_examples.rb +0 -2
  31. data/spec/statesman/adapters/type_safe_active_record_queries_spec.rb +1 -3
  32. data/spec/statesman/callback_spec.rb +0 -2
  33. data/spec/statesman/config_spec.rb +0 -2
  34. data/spec/statesman/exceptions_spec.rb +1 -3
  35. data/spec/statesman/guard_spec.rb +0 -2
  36. data/spec/statesman/machine_spec.rb +71 -2
  37. data/spec/statesman/utils_spec.rb +0 -2
  38. data/spec/support/active_record.rb +55 -13
  39. data/spec/support/exactly_query_databases.rb +35 -0
  40. data/statesman.gemspec +5 -5
  41. metadata +14 -12
@@ -7,19 +7,15 @@ module Statesman
7
7
  class ActiveRecord
8
8
  JSON_COLUMN_TYPES = %w[json jsonb].freeze
9
9
 
10
- def self.database_supports_partial_indexes?
10
+ def self.database_supports_partial_indexes?(model)
11
11
  # Rails 3 doesn't implement `supports_partial_index?`
12
- if ::ActiveRecord::Base.connection.respond_to?(:supports_partial_index?)
13
- ::ActiveRecord::Base.connection.supports_partial_index?
12
+ if model.connection.respond_to?(:supports_partial_index?)
13
+ model.connection.supports_partial_index?
14
14
  else
15
- ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
15
+ model.connection.adapter_name.casecmp("postgresql").zero?
16
16
  end
17
17
  end
18
18
 
19
- def self.adapter_name
20
- ::ActiveRecord::Base.connection.adapter_name.downcase
21
- end
22
-
23
19
  def initialize(transition_class, parent_model, observer, options = {})
24
20
  serialized = serialized?(transition_class)
25
21
  column_type = transition_class.columns_hash["metadata"].sql_type
@@ -88,10 +84,10 @@ module Statesman
88
84
  default_transition_attributes(to, metadata),
89
85
  )
90
86
 
91
- ::ActiveRecord::Base.transaction(requires_new: true) do
87
+ transition_class.transaction(requires_new: true) do
92
88
  @observer.execute(:before, from, to, transition)
93
89
 
94
- if mysql_gaplock_protection?
90
+ if mysql_gaplock_protection?(transition_class.connection)
95
91
  # We save the transition first with most_recent falsy, then mark most_recent
96
92
  # true after to avoid letting MySQL acquire a next-key lock which can cause
97
93
  # deadlocks.
@@ -130,8 +126,8 @@ module Statesman
130
126
  end
131
127
 
132
128
  def add_after_commit_callback(from, to, transition)
133
- ::ActiveRecord::Base.connection.add_transaction_record(
134
- ActiveRecordAfterCommitWrap.new do
129
+ transition_class.connection.add_transaction_record(
130
+ ActiveRecordAfterCommitWrap.new(transition_class.connection) do
135
131
  @observer.execute(:after_commit, from, to, transition)
136
132
  end,
137
133
  )
@@ -144,7 +140,7 @@ module Statesman
144
140
  # Sets the given transition most_recent = t while unsetting the most_recent of any
145
141
  # previous transitions.
146
142
  def update_most_recents(most_recent_id = nil)
147
- update = build_arel_manager(::Arel::UpdateManager)
143
+ update = build_arel_manager(::Arel::UpdateManager, transition_class)
148
144
  update.table(transition_table)
149
145
  update.where(most_recent_transitions(most_recent_id))
150
146
  update.set(build_most_recents_update_all_values(most_recent_id))
@@ -152,9 +148,11 @@ module Statesman
152
148
  # MySQL will validate index constraints across the intermediate result of an
153
149
  # update. This means we must order our update to deactivate the previous
154
150
  # most_recent before setting the new row to be true.
155
- update.order(transition_table[:most_recent].desc) if mysql_gaplock_protection?
151
+ if mysql_gaplock_protection?(transition_class.connection)
152
+ update.order(transition_table[:most_recent].desc)
153
+ end
156
154
 
157
- ::ActiveRecord::Base.connection.update(update.to_sql)
155
+ transition_class.connection.update(update.to_sql(transition_class))
158
156
  end
159
157
 
160
158
  def most_recent_transitions(most_recent_id = nil)
@@ -223,7 +221,7 @@ module Statesman
223
221
  if most_recent_id
224
222
  Arel::Nodes::Case.new.
225
223
  when(transition_table[:id].eq(most_recent_id)).then(db_true).
226
- else(not_most_recent_value).to_sql
224
+ else(not_most_recent_value).to_sql(transition_class)
227
225
  else
228
226
  Arel::Nodes::SqlLiteral.new(not_most_recent_value)
229
227
  end
@@ -233,11 +231,11 @@ module Statesman
233
231
  # change in Arel as we move into Rails >6.0.
234
232
  #
235
233
  # https://github.com/rails/rails/commit/7508284800f67b4611c767bff9eae7045674b66f
236
- def build_arel_manager(manager)
234
+ def build_arel_manager(manager, engine)
237
235
  if manager.instance_method(:initialize).arity.zero?
238
236
  manager.new
239
237
  else
240
- manager.new(::ActiveRecord::Base)
238
+ manager.new(engine)
241
239
  end
242
240
  end
243
241
 
@@ -246,13 +244,8 @@ module Statesman
246
244
  end
247
245
 
248
246
  def serialized?(transition_class)
249
- if ::ActiveRecord.respond_to?(:gem_version) &&
250
- ::ActiveRecord.gem_version >= Gem::Version.new("4.2.0.a")
251
- transition_class.type_for_attribute("metadata").
252
- is_a?(::ActiveRecord::Type::Serialized)
253
- else
254
- transition_class.serialized_attributes.include?("metadata")
255
- end
247
+ transition_class.type_for_attribute("metadata").
248
+ is_a?(::ActiveRecord::Type::Serialized)
256
249
  end
257
250
 
258
251
  def transition_conflict_error?(err)
@@ -263,7 +256,7 @@ module Statesman
263
256
  end
264
257
 
265
258
  def unique_indexes
266
- ::ActiveRecord::Base.connection.
259
+ transition_class.connection.
267
260
  indexes(transition_class.table_name).
268
261
  select do |index|
269
262
  next unless index.unique
@@ -334,16 +327,16 @@ module Statesman
334
327
  ::ActiveRecord::Base.default_timezone
335
328
  end
336
329
 
337
- def mysql_gaplock_protection?
338
- Statesman.mysql_gaplock_protection?
330
+ def mysql_gaplock_protection?(connection)
331
+ Statesman.mysql_gaplock_protection?(connection)
339
332
  end
340
333
 
341
334
  def db_true
342
- ::ActiveRecord::Base.connection.quote(type_cast(true))
335
+ transition_class.connection.quote(type_cast(true))
343
336
  end
344
337
 
345
338
  def db_false
346
- ::ActiveRecord::Base.connection.quote(type_cast(false))
339
+ transition_class.connection.quote(type_cast(false))
347
340
  end
348
341
 
349
342
  def db_null
@@ -353,7 +346,7 @@ module Statesman
353
346
  # Type casting against a column is deprecated and will be removed in Rails 6.2.
354
347
  # See https://github.com/rails/arel/commit/6160bfbda1d1781c3b08a33ec4955f170e95be11
355
348
  def type_cast(value)
356
- ::ActiveRecord::Base.connection.type_cast(value)
349
+ transition_class.connection.type_cast(value)
357
350
  end
358
351
 
359
352
  # Check whether the `most_recent` column allows null values. If it doesn't, set old
@@ -373,9 +366,9 @@ module Statesman
373
366
  end
374
367
 
375
368
  class ActiveRecordAfterCommitWrap
376
- def initialize(&block)
369
+ def initialize(connection, &block)
377
370
  @callback = block
378
- @connection = ::ActiveRecord::Base.connection
371
+ @connection = connection
379
372
  end
380
373
 
381
374
  def self.trigger_transactional_callbacks?
@@ -39,7 +39,7 @@ module Statesman
39
39
  end
40
40
 
41
41
  def included(base)
42
- ensure_inheritance(base)
42
+ ensure_inheritance(base) if base.respond_to?(:subclasses) && base.subclasses.any?
43
43
 
44
44
  query_builder = QueryBuilder.new(base, **@args)
45
45
 
@@ -153,7 +153,7 @@ module Statesman
153
153
  end
154
154
 
155
155
  def db_true
156
- ::ActiveRecord::Base.connection.quote(true)
156
+ model.connection.quote(true)
157
157
  end
158
158
  end
159
159
  end
@@ -10,7 +10,11 @@ module Statesman
10
10
  extend ActiveSupport::Concern
11
11
 
12
12
  included do
13
- serialize :metadata, JSON
13
+ if ::ActiveRecord.gem_version >= Gem::Version.new("7.1")
14
+ serialize :metadata, coder: JSON
15
+ else
16
+ serialize :metadata, JSON
17
+ end
14
18
 
15
19
  class_attribute :updated_timestamp_column
16
20
  self.updated_timestamp_column = DEFAULT_UPDATED_TIMESTAMP_COLUMN
@@ -40,11 +40,11 @@ module Statesman
40
40
  end
41
41
 
42
42
  def matches_from_state(from, to)
43
- (from == self.from && (to.nil? || self.to.empty?))
43
+ from == self.from && (to.nil? || self.to.empty?)
44
44
  end
45
45
 
46
46
  def matches_to_state(from, to)
47
- ((from.nil? || self.from.nil?) && self.to.include?(to))
47
+ (from.nil? || self.from.nil?) && self.to.include?(to)
48
48
  end
49
49
 
50
50
  def matches_both_states(from, to)
@@ -15,17 +15,10 @@ module Statesman
15
15
  @adapter_class = adapter_class
16
16
  end
17
17
 
18
- def mysql_gaplock_protection?
19
- return @mysql_gaplock_protection unless @mysql_gaplock_protection.nil?
20
-
18
+ def mysql_gaplock_protection?(connection)
21
19
  # If our adapter class suggests we're using mysql, enable gaplock protection by
22
20
  # default.
23
- enable_mysql_gaplock_protection if mysql_adapter?(adapter_class)
24
- @mysql_gaplock_protection
25
- end
26
-
27
- def enable_mysql_gaplock_protection
28
- @mysql_gaplock_protection = true
21
+ mysql_adapter?(connection)
29
22
  end
30
23
 
31
24
  private
@@ -34,7 +27,7 @@ module Statesman
34
27
  adapter_name = adapter_name(adapter_class)
35
28
  return false unless adapter_name
36
29
 
37
- adapter_name.start_with?("mysql")
30
+ adapter_name.downcase.start_with?("mysql", "trilogy")
38
31
  end
39
32
 
40
33
  def adapter_name(adapter_class)
@@ -233,12 +233,20 @@ module Statesman
233
233
  def initialize(object,
234
234
  options = {
235
235
  transition_class: Statesman::Adapters::MemoryTransition,
236
+ initial_transition: false,
236
237
  })
237
238
  @object = object
238
239
  @transition_class = options[:transition_class]
239
240
  @storage_adapter = adapter_class(@transition_class).new(
240
241
  @transition_class, object, self, options
241
242
  )
243
+
244
+ if options[:initial_transition]
245
+ if history.empty? && self.class.initial_state
246
+ @storage_adapter.create(nil, self.class.initial_state)
247
+ end
248
+ end
249
+
242
250
  send(:after_initialize) if respond_to? :after_initialize
243
251
  end
244
252
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Statesman
4
- VERSION = "10.2.3"
4
+ VERSION = "12.1.0"
5
5
  end
data/lib/statesman.rb CHANGED
@@ -6,7 +6,7 @@ module Statesman
6
6
  autoload :Callback, "statesman/callback"
7
7
  autoload :Guard, "statesman/guard"
8
8
  autoload :Utils, "statesman/utils"
9
- autoload :Version, "statesman/version"
9
+ autoload :VERSION, "statesman/version"
10
10
  module Adapters
11
11
  autoload :Memory, "statesman/adapters/memory"
12
12
  autoload :ActiveRecord, "statesman/adapters/active_record"
@@ -34,10 +34,8 @@ module Statesman
34
34
  @storage_adapter || Adapters::Memory
35
35
  end
36
36
 
37
- def self.mysql_gaplock_protection?
38
- return @mysql_gaplock_protection unless @mysql_gaplock_protection.nil?
39
-
40
- @mysql_gaplock_protection = config.mysql_gaplock_protection?
37
+ def self.mysql_gaplock_protection?(connection)
38
+ config.mysql_gaplock_protection?(connection)
41
39
  end
42
40
 
43
41
  def self.config
@@ -21,8 +21,8 @@ namespace :statesman do
21
21
  batch_size = 500
22
22
 
23
23
  parent_class.find_in_batches(batch_size: batch_size) do |models|
24
- ActiveRecord::Base.transaction(requires_new: true) do
25
- if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
24
+ transition_class.transaction(requires_new: true) do
25
+ if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(transition_class)
26
26
  # Set all transitions' most_recent to FALSE
27
27
  transition_class.where(parent_fk => models.map(&:id)).
28
28
  update_all(most_recent: false, updated_at: updated_at)
@@ -1,10 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
3
  require "support/generators_shared_examples"
5
4
  require "generators/statesman/active_record_transition_generator"
6
5
 
7
6
  describe Statesman::ActiveRecordTransitionGenerator, type: :generator do
7
+ before do
8
+ stub_const("Bacon", Class.new(ActiveRecord::Base))
9
+ stub_const("BaconTransition", Class.new(ActiveRecord::Base))
10
+ stub_const("Yummy::Bacon", Class.new(ActiveRecord::Base))
11
+ stub_const("Yummy::BaconTransition", Class.new(ActiveRecord::Base))
12
+ end
13
+
8
14
  it_behaves_like "a generator" do
9
15
  let(:migration_name) { "db/migrate/create_bacon_transitions.rb" }
10
16
  end
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
3
  require "support/generators_shared_examples"
5
4
  require "generators/statesman/migration_generator"
6
5
 
7
6
  describe Statesman::MigrationGenerator, type: :generator do
7
+ before do
8
+ stub_const("Yummy::Bacon", Class.new(ActiveRecord::Base))
9
+ stub_const("Yummy::BaconTransition", Class.new(ActiveRecord::Base))
10
+ end
11
+
8
12
  it_behaves_like "a generator" do
9
13
  let(:migration_name) { "db/migrate/add_statesman_to_bacon_transitions.rb" }
10
14
  end
data/spec/spec_helper.rb CHANGED
@@ -5,13 +5,14 @@ require "sqlite3"
5
5
  require "mysql2"
6
6
  require "pg"
7
7
  require "active_record"
8
+ require "active_record/database_configurations"
8
9
  # We have to include all of Rails to make rspec-rails work
9
10
  require "rails"
10
11
  require "action_view"
11
12
  require "action_dispatch"
12
13
  require "action_controller"
13
14
  require "rspec/rails"
14
- require "support/active_record"
15
+ require "support/exactly_query_databases"
15
16
  require "rspec/its"
16
17
  require "pry"
17
18
 
@@ -28,10 +29,31 @@ RSpec.configure do |config|
28
29
  if config.exclusion_filter[:active_record]
29
30
  puts "Skipping ActiveRecord tests"
30
31
  else
31
- # Connect to the database for activerecord tests
32
- db_conn_spec = ENV["DATABASE_URL"]
33
- db_conn_spec ||= { adapter: "sqlite3", database: ":memory:" }
34
- ActiveRecord::Base.establish_connection(db_conn_spec)
32
+ current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
33
+
34
+ # We have to parse this to a hash since ActiveRecord::Base.configurations
35
+ # will only consider a single URL config.
36
+ url_config = if ENV["DATABASE_URL"]
37
+ ActiveRecord::DatabaseConfigurations::ConnectionUrlResolver.
38
+ new(ENV["DATABASE_URL"]).to_hash.merge({ sslmode: "disable" })
39
+ end
40
+
41
+ db_config = {
42
+ current_env => {
43
+ primary: url_config || {
44
+ adapter: "sqlite3",
45
+ database: "/tmp/statesman.db",
46
+ },
47
+ secondary: url_config || {
48
+ adapter: "sqlite3",
49
+ database: "/tmp/statesman.db",
50
+ },
51
+ },
52
+ }
53
+
54
+ # Connect to the primary database for activerecord tests.
55
+ ActiveRecord::Base.configurations = db_config
56
+ ActiveRecord::Base.establish_connection(:primary)
35
57
 
36
58
  db_adapter = ActiveRecord::Base.connection.adapter_name
37
59
  puts "Running with database adapter '#{db_adapter}'"
@@ -40,7 +62,9 @@ RSpec.configure do |config|
40
62
  ActiveRecord::Migration.verbose = false
41
63
  end
42
64
 
43
- config.before(:each, active_record: true) do
65
+ # Since our primary and secondary connections point to the same database, we don't
66
+ # need to worry about applying these actions to both.
67
+ config.before(:each, :active_record) do
44
68
  tables = %w[
45
69
  my_active_record_models
46
70
  my_active_record_model_transitions
@@ -53,6 +77,7 @@ RSpec.configure do |config|
53
77
  ]
54
78
  tables.each do |table_name|
55
79
  sql = "DROP TABLE IF EXISTS #{table_name};"
80
+
56
81
  ActiveRecord::Base.connection.execute(sql)
57
82
  end
58
83
 
@@ -82,7 +107,8 @@ RSpec.configure do |config|
82
107
  CreateStiActiveRecordModelTransitionMigration.migrate(:up)
83
108
  StiActiveRecordModelTransition.reset_column_information
84
109
  end
85
-
86
- MyNamespace::MyActiveRecordModelTransition.serialize(:metadata, JSON)
87
110
  end
88
111
  end
112
+
113
+ # We have to require this after the databases are configured.
114
+ require "support/active_record"
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
-
5
- describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
3
+ describe Statesman::Adapters::ActiveRecordQueries, :active_record do
6
4
  def configure_old(klass, transition_class)
7
5
  klass.define_singleton_method(:transition_class) { transition_class }
8
6
  klass.define_singleton_method(:initial_state) { :initial }
@@ -1,17 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
3
  require "timecop"
5
4
  require "statesman/adapters/shared_examples"
6
5
  require "statesman/exceptions"
7
6
 
8
- describe Statesman::Adapters::ActiveRecord, active_record: true do
7
+ describe Statesman::Adapters::ActiveRecord, :active_record do
9
8
  before do
10
9
  prepare_model_table
11
10
  prepare_transitions_table
12
11
 
13
- MyActiveRecordModelTransition.serialize(:metadata, JSON)
14
-
15
12
  prepare_sti_model_table
16
13
  prepare_sti_transitions_table
17
14
 
@@ -26,8 +23,10 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
26
23
 
27
24
  after { Statesman.configure { storage_adapter(Statesman::Adapters::Memory) } }
28
25
 
26
+ let(:model_class) { MyActiveRecordModel }
27
+ let(:transition_class) { MyActiveRecordModelTransition }
29
28
  let(:observer) { double(Statesman::Machine, execute: nil) }
30
- let(:model) { MyActiveRecordModel.create(current_state: :pending) }
29
+ let(:model) { model_class.create(current_state: :pending) }
31
30
 
32
31
  it_behaves_like "an adapter", described_class, MyActiveRecordModelTransition
33
32
 
@@ -36,17 +35,11 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
36
35
  before do
37
36
  metadata_column = double
38
37
  allow(metadata_column).to receive_messages(sql_type: "")
39
- allow(MyActiveRecordModelTransition).to receive_messages(columns_hash:
40
- { "metadata" => metadata_column })
41
- if ActiveRecord.respond_to?(:gem_version) &&
42
- ActiveRecord.gem_version >= Gem::Version.new("4.2.0.a")
43
- expect(MyActiveRecordModelTransition).
44
- to receive(:type_for_attribute).with("metadata").
45
- and_return(ActiveRecord::Type::Value.new)
46
- else
47
- expect(MyActiveRecordModelTransition).
48
- to receive_messages(serialized_attributes: {})
49
- end
38
+ allow(MyActiveRecordModelTransition).
39
+ to receive_messages(columns_hash: { "metadata" => metadata_column })
40
+ expect(MyActiveRecordModelTransition).
41
+ to receive(:type_for_attribute).with("metadata").
42
+ and_return(ActiveRecord::Type::Value.new)
50
43
  end
51
44
 
52
45
  it "raises an exception" do
@@ -91,18 +84,12 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
91
84
  allow(metadata_column).to receive_messages(sql_type: "jsonb")
92
85
  allow(MyActiveRecordModelTransition).to receive_messages(columns_hash:
93
86
  { "metadata" => metadata_column })
94
- if ActiveRecord.respond_to?(:gem_version) &&
95
- ActiveRecord.gem_version >= Gem::Version.new("4.2.0.a")
96
- serialized_type = ActiveRecord::Type::Serialized.new(
97
- "", ActiveRecord::Coders::JSON
98
- )
99
- expect(MyActiveRecordModelTransition).
100
- to receive(:type_for_attribute).with("metadata").
101
- and_return(serialized_type)
102
- else
103
- expect(MyActiveRecordModelTransition).
104
- to receive_messages(serialized_attributes: { "metadata" => "" })
105
- end
87
+ serialized_type = ActiveRecord::Type::Serialized.new(
88
+ "", ActiveRecord::Coders::JSON
89
+ )
90
+ expect(MyActiveRecordModelTransition).
91
+ to receive(:type_for_attribute).with("metadata").
92
+ and_return(serialized_type)
106
93
  end
107
94
 
108
95
  it "raises an exception" do
@@ -117,13 +104,15 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
117
104
  describe "#create" do
118
105
  subject(:transition) { create }
119
106
 
120
- let!(:adapter) do
121
- described_class.new(MyActiveRecordModelTransition, model, observer)
122
- end
107
+ let!(:adapter) { described_class.new(transition_class, model, observer) }
123
108
  let(:from) { :x }
124
109
  let(:to) { :y }
125
110
  let(:create) { adapter.create(from, to) }
126
111
 
112
+ it "only connects to the primary database" do
113
+ expect { create }.to exactly_query_databases({ primary: [:writing] })
114
+ end
115
+
127
116
  context "when there is a race" do
128
117
  it "raises a TransitionConflictError" do
129
118
  adapter2 = adapter.dup
@@ -131,7 +120,8 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
131
120
  adapter.last
132
121
  adapter2.create(:y, :z)
133
122
  expect { adapter.create(:y, :z) }.
134
- to raise_exception(Statesman::TransitionConflictError)
123
+ to raise_exception(Statesman::TransitionConflictError).
124
+ and exactly_query_databases({ primary: [:writing] })
135
125
  end
136
126
 
137
127
  it "does not pollute the state when the transition fails" do
@@ -355,12 +345,34 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
355
345
  end
356
346
  end
357
347
  end
348
+
349
+ context "when using the secondary database" do
350
+ let(:model_class) { SecondaryActiveRecordModel }
351
+ let(:transition_class) { SecondaryActiveRecordModelTransition }
352
+
353
+ it "doesn't connect to the primary database" do
354
+ expect { create }.to exactly_query_databases({ secondary: [:writing] })
355
+ expect(adapter.last.to_state).to eq("y")
356
+ end
357
+
358
+ context "when there is a race" do
359
+ it "raises a TransitionConflictError and uses the correct database" do
360
+ adapter2 = adapter.dup
361
+ adapter2.create(:x, :y)
362
+ adapter.last
363
+ adapter2.create(:y, :z)
364
+
365
+ expect { adapter.create(:y, :z) }.
366
+ to raise_exception(Statesman::TransitionConflictError).
367
+ and exactly_query_databases({ secondary: [:writing] })
368
+ end
369
+ end
370
+ end
358
371
  end
359
372
 
360
373
  describe "#last" do
361
- let(:adapter) do
362
- described_class.new(MyActiveRecordModelTransition, model, observer)
363
- end
374
+ let(:transition_class) { MyActiveRecordModelTransition }
375
+ let(:adapter) { described_class.new(transition_class, model, observer) }
364
376
 
365
377
  context "with a previously looked up transition" do
366
378
  before { adapter.create(:x, :y) }
@@ -377,8 +389,19 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
377
389
  before { adapter.create(:y, :z, []) }
378
390
 
379
391
  it "retrieves the new transition from the database" do
392
+ expect { adapter.last.to_state }.to exactly_query_databases({ primary: [:writing] })
380
393
  expect(adapter.last.to_state).to eq("z")
381
394
  end
395
+
396
+ context "when using the secondary database" do
397
+ let(:model_class) { SecondaryActiveRecordModel }
398
+ let(:transition_class) { SecondaryActiveRecordModelTransition }
399
+
400
+ it "retrieves the new transition from the database" do
401
+ expect { adapter.last.to_state }.to exactly_query_databases({ secondary: [:writing] })
402
+ expect(adapter.last.to_state).to eq("z")
403
+ end
404
+ end
382
405
  end
383
406
 
384
407
  context "when a new transition has been created elsewhere" do
@@ -467,10 +490,6 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
467
490
  CreateNamespacedARModelTransitionMigration.migrate(:up)
468
491
  end
469
492
 
470
- before do
471
- MyNamespace::MyActiveRecordModelTransition.serialize(:metadata, JSON)
472
- end
473
-
474
493
  let(:observer) { double(Statesman::Machine, execute: nil) }
475
494
  let(:model) do
476
495
  MyNamespace::MyActiveRecordModel.create(current_state: :pending)
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
3
  require "json"
5
4
 
6
5
  describe Statesman::Adapters::ActiveRecordTransition do
@@ -8,7 +7,11 @@ describe Statesman::Adapters::ActiveRecordTransition do
8
7
 
9
8
  describe "including behaviour" do
10
9
  it "calls Class.serialize" do
11
- expect(transition_class).to receive(:serialize).with(:metadata, JSON).once
10
+ if Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("7.1")
11
+ expect(transition_class).to receive(:serialize).with(:metadata, coder: JSON).once
12
+ else
13
+ expect(transition_class).to receive(:serialize).with(:metadata, JSON).once
14
+ end
12
15
  transition_class.send(:include, described_class)
13
16
  end
14
17
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
3
  require "statesman/adapters/shared_examples"
5
4
  require "statesman/adapters/memory_transition"
6
5
 
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
3
  require "statesman/adapters/memory_transition"
5
4
 
6
5
  describe Statesman::Adapters::MemoryTransition do
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
-
5
3
  # All adpators must define seven methods:
6
4
  # initialize: Accepts a transition class, parent model and state_attr.
7
5
  # transition_class: Returns the transition class object passed to initialize.
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
-
5
- describe Statesman::Adapters::TypeSafeActiveRecordQueries, active_record: true do
3
+ describe Statesman::Adapters::TypeSafeActiveRecordQueries, :active_record do
6
4
  def configure(klass, transition_class)
7
5
  klass.send(:extend, described_class)
8
6
  klass.configure_state_machine(
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
-
5
3
  describe Statesman::Callback do
6
4
  let(:cb_lambda) { -> {} }
7
5
  let(:callback) do