Skip to content

Commit

Permalink
Add database tasks test coverage (#5)
Browse files Browse the repository at this point in the history
* Move TestHelper to separate file

* Add tests for PostgreSQLProxy database tasks
  • Loading branch information
mateuscruz authored Dec 24, 2024
1 parent c6e6aee commit f4d8188
Show file tree
Hide file tree
Showing 9 changed files with 414 additions and 138 deletions.
7 changes: 6 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ jobs:
- name: Run RSpec tests
env:
RAILS_ENV: test
PG_PRIMARY_USER: postgres_primary_test
PG_PRIMARY_PASSWORD: postgres_primary_test
PG_PRIMARY_HOST: localhost
Expand All @@ -144,7 +145,11 @@ jobs:
PG_REPLICA_HOST: localhost
PG_REPLICA_PORT: 5433

run: RUBY_VERSION="${{ matrix.ruby }}" RAILS_VERSION="${{ matrix.rails }}" bundle exec rspec --format progress
run: |
export RUBY_VERSION="${{ matrix.ruby }}"
export RAILS_VERSION="${{ matrix.rails }}"
bundle exec rake db:setup
bundle exec rspec --format progress
- name: Upload Coverage Report
uses: actions/upload-artifact@v4
Expand Down
56 changes: 56 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,62 @@ RuboCop::RakeTask.new

task default: %i[spec rubocop]

desc "Prepares the database environment for use"
task :environment do
$LOAD_PATH << File.expand_path("lib", __dir__)
require "active_record_proxy_adapters"
require "active_record_proxy_adapters/connection_handling"
ActiveRecord::Base.extend ActiveRecordProxyAdapters::ConnectionHandling
require_relative "spec/test_helper"

TestHelper.setup_active_record_config

$stdout.puts "Environment loaded: #{TestHelper.env_name}"
end

namespace :db do # rubocop:disable Metrics/BlockLength
desc "Drops all databases"
task drop: %i[drop:postgresql]

namespace :drop do
desc "Drops the postgresql database"
task postgresql: :environment do
TestHelper.drop_database
end
end

desc "Creates all databases"
task create: %i[create:postgresql]

namespace :create do
desc "Creates the postgresql database"
task postgresql: :environment do
TestHelper.create_database
end
end

namespace :schema do
desc "Loads all schemas onto their respective databases"
task load: %i[load:postgresql]

namespace :load do
desc "Loads the schema into the postgresql database from schema_path. Default is db/postgresql_structure.sql"
task :postgresql, [:schema_path] => :environment do |_task, args|
args.with_defaults(schema_path: "db/postgresql_structure.sql")
TestHelper.load_schema(args.schema_path)
end
end
end

desc "Creates a all databases and loads their schemas"
task setup: %i[create schema:load]

namespace :setup do
desc "Creates the postgresql database and loads the schema"
task postgresql: %i[create:postgresql schema:load:postgresql]
end
end

namespace :coverage do
desc "Collates all result sets generated by the different test runners"
task :report do
Expand Down
76 changes: 76 additions & 0 deletions db/postgresql_structure.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;

--
-- Name: public; Type: SCHEMA; Schema: -; Owner: -
--

-- *not* creating schema, since initdb creates it


SET default_tablespace = '';

SET default_table_access_method = heap;

--
-- Name: users; Type: TABLE; Schema: public; Owner: -
--

CREATE TABLE public.users (
id integer NOT NULL,
name text NOT NULL,
email text NOT NULL,
created_at timestamp without time zone DEFAULT now() NOT NULL,
updated_at timestamp without time zone DEFAULT now() NOT NULL
);


--
-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--

CREATE SEQUENCE public.users_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;


--
-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--

ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id;


--
-- Name: users id; Type: DEFAULT; Schema: public; Owner: -
--

ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass);


--
-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--

ALTER TABLE ONLY public.users
ADD CONSTRAINT users_pkey PRIMARY KEY (id);


--
-- PostgreSQL database dump complete
--

SET search_path TO "$user", public;

129 changes: 129 additions & 0 deletions spec/active_record/tasks/postgresql_proxy_database_tasks_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# frozen_string_literal: true

# rubocop:disable RSpec/MultipleMemoizedHelpers
RSpec.describe ActiveRecord::Tasks::PostgreSQLProxyDatabaseTasks do # rubocop:disable RSpec/SpecFilePathFormat
let(:public_schema_config) do
configuration.configuration_hash.merge(adapter: "postgresql", database: "postgres", schema_search_path: "public")
end
let(:configuration) do
TestHelper.postgresql_database_tasks_configuration
end

def database_exists?
proc do
with_master_connection do |conn|
conn.select_value <<~SQL.squish
SELECT COUNT(*)::int::boolean
FROM pg_database WHERE datname = '#{configuration.database}';
SQL
end
end
end

def schema_loaded?
proc do
any_tables = TestHelper::PostgreSQLDatabaseTaskRecord.connection.tables.any?
TestHelper::PostgreSQLDatabaseTaskRecord.connection_pool.disconnect!

any_tables
end
end

def with_master_connection(&)
pool = ActiveRecord::Base.connection_handler.establish_connection(public_schema_config,
role: :admin)
pool.with_connection(&)
ensure
pool.disconnect
end

describe "#drop" do
subject(:drop) { ActiveRecord::Tasks::DatabaseTasks.drop(configuration) }

before do
ActiveRecord::Tasks::DatabaseTasks.create(configuration)
end

it "drops the database" do
expect { drop }.to change(&database_exists?).from(true).to(false)
end
end

describe "#create" do
subject(:create) { ActiveRecord::Tasks::DatabaseTasks.create(configuration) }

before do
ActiveRecord::Tasks::DatabaseTasks.drop(configuration)
end

after do
ActiveRecord::Tasks::DatabaseTasks.drop(configuration)
end

it "creates the database" do
expect { create }.to change(&database_exists?).from(false).to(true)
end
end

describe "#structure_load" do
subject(:structure_load) do
ActiveRecord::Tasks::DatabaseTasks.structure_load(configuration, "db/postgresql_structure.sql")
end

before do
ActiveRecord::Tasks::DatabaseTasks.create(configuration)
end

after do
ActiveRecord::Tasks::DatabaseTasks.drop(configuration)
end

it "loads the schema" do
expect { structure_load }.to change(&schema_loaded?).from(false).to(true)
end
end

describe "#structure_dump" do
subject(:structure_dump) { ActiveRecord::Tasks::DatabaseTasks.structure_dump(configuration, dump_out) }

let(:dump_out) { temp_file.path }
let(:dump_in) { "db/postgresql_structure.sql" }
let(:temp_file) { Tempfile.create("postgresql_structure.sql") }
let(:schema) { File.read(dump_in) }

before do
ActiveRecord::Tasks::DatabaseTasks.create(configuration)
ActiveRecord::Tasks::DatabaseTasks.structure_load(configuration, dump_in)
end

after do
ActiveRecord::Tasks::DatabaseTasks.drop(configuration)
end

it "dumps the schema onto the given path" do
schema_matches = proc { temp_file.read == schema }

expect { structure_dump }.to change(&schema_matches).from(false).to(true)
end
end

describe "#purge" do
subject(:purge) do
ActiveRecord::Tasks::DatabaseTasks.purge(configuration)
end

before do
ActiveRecord::Tasks::DatabaseTasks.create(configuration)
ActiveRecord::Tasks::DatabaseTasks.structure_load(configuration, "db/postgresql_structure.sql")
end

after do
ActiveRecord::Tasks::DatabaseTasks.drop(configuration)
end

it "recreates the database with an empty schema" do
expect { purge }.to change(&schema_loaded?).from(true).to(false)
end
end
end
# rubocop:enable RSpec/MultipleMemoizedHelpers
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

RSpec.describe ActiveRecordProxyAdapters::PostgreSQLProxy do
RSpec.describe ActiveRecordProxyAdapters::PostgreSQLProxy do # rubocop:disable RSpec/SpecFilePathFormat
attr_reader :primary_adapter

let(:replica_pool) { TestHelper.replica_pool }
Expand All @@ -14,6 +14,8 @@

@primary_adapter = nil
end

TestHelper.truncate_database
end

def create_dummy_user
Expand Down Expand Up @@ -54,15 +56,15 @@ def create_dummy_user
it "reroutes query to the primary" do
allow(primary_adapter).to receive(:"#{method_name}_unproxied").and_call_original

ActiveRecord::Base.transaction { run_test }
primary_adapter.transaction { run_test }

expect(primary_adapter).to have_received(:"#{method_name}_unproxied").with(sql, any_args).once
end

it "does not checkout a connection from the replica pool" do
allow(replica_pool).to receive(:checkout).and_call_original

ActiveRecord::Base.transaction { run_test }
primary_adapter.transaction { run_test }

expect(replica_pool).not_to have_received(:checkout)
end
Expand Down
Empty file added spec/config/.keep
Empty file.
21 changes: 21 additions & 0 deletions spec/config/database.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
test:
postgresql_primary: &postgresql_primary
adapter: postgresql_proxy
username: <%= ENV.fetch("PG_PRIMARY_USER", "postgres") %>
password: <%= ENV.fetch("PG_PRIMARY_PASSWORD", "postgres") %>
host: <%= ENV.fetch("PG_PRIMARY_HOST", "localhost") %>
port: <%= Integer(ENV.fetch("PG_PRIMARY_PORT", 5432)) %>
database: primary_replica_proxy_test

postgresql_replica:
adapter: postgresql
username: <%= ENV.fetch("PG_REPLICA_USER", "postgres") %>
password: <%= ENV.fetch("PG_REPLICA_PASSWORD", "postgres") %>
host: <%= ENV.fetch("PG_REPLICA_HOST", "postgres_replica") %>
port: <%= Integer(ENV.fetch("PG_REPLICA_PORT", 5433)) %>
database: primary_replica_proxy_test
replica: true

postgresql_database_tasks:
<<: *postgresql_primary
database: primary_replica_proxy_database_tasks_test
Loading

0 comments on commit f4d8188

Please sign in to comment.