Skip to content

Commit

Permalink
First bash at a multi-db spec.
Browse files Browse the repository at this point in the history
  • Loading branch information
benk-gc committed Nov 27, 2023
1 parent 0e355a5 commit caed2d5
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 11 deletions.
4 changes: 4 additions & 0 deletions spec/generators/statesman/migration_generator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,9 @@
expect(migration).
to contain("name: \"index_bacon_transitions_parent_most_recent\"")
end

it "doesn't query the database" do
expect { migration }.to exactly_query_databases({})
end
end
end
32 changes: 26 additions & 6 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
require "mysql2"
require "pg"
require "active_record"
require "active_record/database_configurations"
# We have to include all of Rails to make rspec-rails work
require "rails"
require "action_view"
require "action_dispatch"
require "action_controller"
require "rspec/rails"
require "support/active_record"
require "support/exactly_query_databases"
require "rspec/its"
require "pry"

Expand All @@ -30,17 +31,24 @@ def connection_failure
else
current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call

# We have to parse this to a hash since ActiveRecord::Base.configurations
# will only consider a single URL config.
url_config = if ENV["DATABASE_URL"]
ActiveRecord::DatabaseConfigurations::ConnectionUrlResolver.
new(ENV["DATABASE_URL"]).to_hash.merge({ sslmode: "disable" })
end

db_config = {
current_env => {
primary: ENV["DATABASE_URL"] || {
primary: url_config || {
adapter: "sqlite3",
database: ":memory:",
database: "/tmp/statesman_primary.db",
},
secondary: ENV["DATABASE_URL"] || {
secondary: url_config || {
adapter: "sqlite3",
database: ":memory:",
database: "/tmp/statesman_secondary.db",
},
}
},
}

# Connect to the primary database for activerecord tests.
Expand Down Expand Up @@ -100,4 +108,16 @@ def prepare_sti_transitions_table
CreateStiActiveRecordModelTransitionMigration.migrate(:up)
StiActiveRecordModelTransition.reset_column_information
end

def with_db(name)
original_config = ActiveRecord::Base.connection_db_config
db_config = ActiveRecord::Base.configurations.find_db_config(name)
pool = ActiveRecord::Base.connection_handler.establish_connection(db_config)
yield pool.connection
ensure
ActiveRecord::Base.connection_handler.establish_connection(original_config)
end
end

# We have to require this after the databases are configured.
require "support/active_record"
25 changes: 20 additions & 5 deletions spec/statesman/adapters/active_record_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
prepare_model_table
prepare_transitions_table

# MyActiveRecordModelTransition.serialize(:metadata, JSON)
# @todo We only really need this because we actually use two databases for SQLite.
# with_db(:secondary) do
# prepare_model_table
# prepare_transitions_table
# end

prepare_sti_model_table
prepare_sti_transitions_table
Expand All @@ -26,8 +30,9 @@

after { Statesman.configure { storage_adapter(Statesman::Adapters::Memory) } }

let(:model_class) { MyActiveRecordModel }
let(:observer) { double(Statesman::Machine, execute: nil) }
let(:model) { MyActiveRecordModel.create(current_state: :pending) }
let(:model) { model_class.create(current_state: :pending) }

it_behaves_like "an adapter", described_class, MyActiveRecordModelTransition

Expand Down Expand Up @@ -346,9 +351,8 @@
end

describe "#last" do
let(:adapter) do
described_class.new(MyActiveRecordModelTransition, model, observer)
end
let(:transition_class) { MyActiveRecordModelTransition }
let(:adapter) { described_class.new(transition_class, model, observer) }

context "with a previously looked up transition" do
before { adapter.create(:x, :y) }
Expand All @@ -367,6 +371,17 @@
it "retrieves the new transition from the database" do
expect(adapter.last.to_state).to eq("z")
end

context "when using the secondary database" do
let(:model_class) { SecondaryActiveRecordModel }
let(:transition_class) { SecondaryActiveRecordModelTransition }

it "retrieves the new transition from the database" do
expect { adapter.last.to_state }.to exactly_query_databases({ secondary: [:writing] })

expect(adapter.last.to_state).to eq("z")
end
end
end

context "when a new transition has been created elsewhere" do
Expand Down
37 changes: 37 additions & 0 deletions spec/support/active_record.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require "json"
require "support/secondary_record"

MIGRATION_CLASS = if Rails.version.split(".").map(&:to_i).first >= 5
migration_version = ActiveRecord::Migration.current_version
Expand Down Expand Up @@ -134,6 +135,42 @@ class OtherActiveRecordModelTransition < ActiveRecord::Base
belongs_to :other_active_record_model
end

class SecondaryActiveRecordModelTransition < SecondaryRecord
self.table_name = "my_active_record_model_transitions"

include Statesman::Adapters::ActiveRecordTransition

belongs_to :my_active_record_model,
class_name: "SecondaryActiveRecordModel",
foreign_key: "my_active_record_model_transition_id"
end

class SecondaryActiveRecordModel < SecondaryRecord
self.table_name = "my_active_record_models"

has_many :my_active_record_model_transitions,
class_name: "SecondaryActiveRecordModelTransition",
foreign_key: "my_active_record_model_id",
autosave: false

alias_method :transitions, :my_active_record_model_transitions

include Statesman::Adapters::ActiveRecordQueries[
transition_class: SecondaryActiveRecordModelTransition,
initial_state: :initial
]

def state_machine
@state_machine ||= MyStateMachine.new(
self, transition_class: SecondaryActiveRecordModelTransition
)
end

def metadata
super || {}
end
end

class CreateOtherActiveRecordModelMigration < MIGRATION_CLASS
def change
create_table :other_active_record_models do |t|
Expand Down
35 changes: 35 additions & 0 deletions spec/support/exactly_query_databases.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

# `expected_dbs` should be a Hash of the form:
# {
# primary: [:writing, :reading],
# replica: [:reading],
# }
RSpec::Matchers.define :exactly_query_databases do |expected_dbs|
match do |block|
@expected_dbs = expected_dbs.transform_values(&:to_set).with_indifferent_access
@actual_dbs = Hash.new { |h, k| h[k] = Set.new }.with_indifferent_access

ActiveSupport::Notifications.
subscribe("sql.active_record") do |_name, _start, _finish, _id, payload|
pool = payload.fetch(:connection).pool

next if pool.is_a?(ActiveRecord::ConnectionAdapters::NullPool)

name = pool.db_config.name
role = pool.role

@actual_dbs[name] << role
end

block.call

@actual_dbs == @expected_dbs
end

failure_message do |_block|
"expected to query exactly #{@expected_dbs}, but queried #{@actual_dbs}"
end

supports_block_expectations
end
7 changes: 7 additions & 0 deletions spec/support/secondary_record.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class SecondaryRecord < ActiveRecord::Base
self.abstract_class = true

connects_to database: { writing: :secondary, reading: :secondary }
end

0 comments on commit caed2d5

Please sign in to comment.