From caed2d540a331059035d0a35e31bb3920ceaff4b Mon Sep 17 00:00:00 2001 From: benk-gc Date: Mon, 27 Nov 2023 16:46:37 +0000 Subject: [PATCH] First bash at a multi-db spec. --- .../statesman/migration_generator_spec.rb | 4 ++ spec/spec_helper.rb | 32 +++++++++++++--- spec/statesman/adapters/active_record_spec.rb | 25 ++++++++++--- spec/support/active_record.rb | 37 +++++++++++++++++++ spec/support/exactly_query_databases.rb | 35 ++++++++++++++++++ spec/support/secondary_record.rb | 7 ++++ 6 files changed, 129 insertions(+), 11 deletions(-) create mode 100644 spec/support/exactly_query_databases.rb create mode 100644 spec/support/secondary_record.rb diff --git a/spec/generators/statesman/migration_generator_spec.rb b/spec/generators/statesman/migration_generator_spec.rb index c62d1dc7..547f1e4c 100644 --- a/spec/generators/statesman/migration_generator_spec.rb +++ b/spec/generators/statesman/migration_generator_spec.rb @@ -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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1aa0e843..35720ad3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -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" @@ -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. @@ -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" diff --git a/spec/statesman/adapters/active_record_spec.rb b/spec/statesman/adapters/active_record_spec.rb index a0950b4b..5dd76696 100644 --- a/spec/statesman/adapters/active_record_spec.rb +++ b/spec/statesman/adapters/active_record_spec.rb @@ -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 @@ -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 @@ -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) } @@ -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 diff --git a/spec/support/active_record.rb b/spec/support/active_record.rb index f8b0d8a3..7b9535aa 100644 --- a/spec/support/active_record.rb +++ b/spec/support/active_record.rb @@ -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 @@ -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| diff --git a/spec/support/exactly_query_databases.rb b/spec/support/exactly_query_databases.rb new file mode 100644 index 00000000..209b6231 --- /dev/null +++ b/spec/support/exactly_query_databases.rb @@ -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 diff --git a/spec/support/secondary_record.rb b/spec/support/secondary_record.rb new file mode 100644 index 00000000..b44061e2 --- /dev/null +++ b/spec/support/secondary_record.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class SecondaryRecord < ActiveRecord::Base + self.abstract_class = true + + connects_to database: { writing: :secondary, reading: :secondary } +end