Skip to content

Commit

Permalink
Account for mysql, sqlite adapters in testing logic
Browse files Browse the repository at this point in the history
MySQL is largely the same as postgres here, but sqlite requires some
accomodation.

There are two ways to use sqlite:
1. `sqlite::memory` for an in-memory database
2. `sqlite:///app/config/bookshelf_dev.db` for a filename

We will ignore in-memory databases and apply the standard renaming logic
to the basename of the database filenames, preserving the extname.
  • Loading branch information
alassek committed Jun 1, 2024
1 parent 2932c33 commit baa22dd
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 20 deletions.
135 changes: 126 additions & 9 deletions lib/hanami/db/testing.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true

require "pathname"
require "uri"

module Hanami
Expand All @@ -16,18 +17,134 @@ module Testing
DATABASE_NAME_MATCHER = /_dev(elopment)?$/
private_constant :DATABASE_NAME_MATCHER

# @api private
# @since 2.2.0
def self.database_url(url)
url = URI(url.to_s)
class << self
# @api private
# @since 2.2.0
def database_url(url)
url = parse_url(url)

case deconstruct_url(url)
in { scheme: "sqlite", opaque: nil, path: } unless path.nil?
url.path = database_filename(path)
in { path: String => path } if path =~ DATABASE_NAME_MATCHER
url.path = path.sub(DATABASE_NAME_MATCHER, DATABASE_NAME_SUFFIX)
in { path: String => path } unless path.end_with?(DATABASE_NAME_SUFFIX)
url.path << DATABASE_NAME_SUFFIX
else
# do nothing
end

stringify_url(url)
end

private

def parse_url(url)
if url.is_a?(URI::Generic)
# URI#dup does not duplicate internal instance variables, making
# mutation dangerous.
URI(stringify_url(url))
else
URI(url.to_s)
end
end

# rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity

# Work around a bug in Ruby 3.0.x that erroneously omits the '//' prefix from
# hierarchical URLs.
#
# @api private
# @since 2.2.0
def stringify_url(url)
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.1.0")
return url.to_s
end

require "stringio"

uri = StringIO.new
uri << url.scheme
uri << ":"
uri << url.opaque || "//"

if url.path =~ DATABASE_NAME_MATCHER
url.path = url.path.sub(DATABASE_NAME_MATCHER, DATABASE_NAME_SUFFIX)
elsif !url.path.end_with?(DATABASE_NAME_SUFFIX)
url.path << DATABASE_NAME_SUFFIX
if url.user || url.password
uri << "#{url.user}:#{url.password}@"
end

if url.host
uri << url.host
end

if url.port
uri << ":"
uri << url.port
end

if url.path
uri << url.path
end

if url.query
uri << "?"
uri << url.query
end

if url.fragment
uri << "#"
uri << uri.fragment
end

uri.string
end

url.to_s
# rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity

# Deconstructs a URI::Generic for pattern-matching.
#
# scheme: the adapter name corresponding to database
#
# path: URI path corresponding to database name or filename
#
# opaque: A URI whose scheme is not followed by '/' is
# considered non-hierarchical by RFC 3986. In practice,
# this will contain ':memory' for in-memory sqlite.
#
# @param url [URI] Database URL parsed as URI::Generic
#
# @return [Hash]
#
# @api private
# @since 2.2.0
def deconstruct_url(url)
%i[fragment host opaque password path port query scheme user].each_with_object({}) do |part, hash|
hash[part] = url.public_send(part)
end
end

# Transform filename as with URI paths, but account for extname
#
# @param path [String] path component from URI
#
# @return [String]
#
# @api private
# @since 2.2.0
def database_filename(path)
path = Pathname(path)
ext = path.extname
database = path.basename(ext).to_s

if database =~ /^dev(elopment)?$/
database = "test"
elsif database =~ DATABASE_NAME_MATCHER
database.sub!(DATABASE_NAME_MATCHER, DATABASE_NAME_SUFFIX)
elsif !database.end_with?(DATABASE_NAME_SUFFIX)
database << DATABASE_NAME_SUFFIX
end

path.dirname.join(database + ext).to_s
end
end
end
end
Expand Down
75 changes: 64 additions & 11 deletions spec/unit/testing/database_url_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,77 @@
RSpec.describe Hanami::DB::Testing do
subject { described_class.method(:database_url) }

it "transforms _dev to _test" do
expect(subject.call("/bookshelf_dev")).to eq "/bookshelf_test"
shared_examples "URL Transforms" do |scheme|
define_method(:url) { |path| URI.join("#{scheme}://localhost", path).to_s }

it "transforms _dev to _test" do
expect(subject.call(url("/bookshelf_dev"))).to eq url("/bookshelf_test")
end

it "transforms _development to _test" do
expect(subject.call(url("/bookshelf_development"))).to eq url("/bookshelf_test")
end

it "does not transform _test" do
expect(subject.call(url("/bookshelf_test"))).to eq url("/bookshelf_test")
end

it "appends to non-conforming paths" do
expect(subject.call(url("/bookshelf_database"))).to eq url("/bookshelf_database_test")
end

it "accepts any #to_s object such as URI" do
url = URI("#{scheme}://localhost:5432/bookshelf_development")
expect(subject.call(url)).to eq "#{scheme}://localhost:5432/bookshelf_test"
end
end

it "transforms _development to _test" do
expect(subject.call("/bookshelf_development")).to eq "/bookshelf_test"
context "postgres scheme" do
include_examples "URL Transforms", :postgres

it "preserves query params" do
url = "postgres://user:pass@/bookshelf_development?host=/var/run/postgresql/.s.PGSQL.5432"
expect(subject.call(url)).to eq "postgres://user:pass@/bookshelf_test?host=/var/run/postgresql/.s.PGSQL.5432"
end
end

it "does not transform _test" do
expect(subject.call("/bookshelf_test")).to eq "/bookshelf_test"
context "postgresql scheme" do
include_examples "URL Transforms", :postgresql

it "preserves query params" do
url = "postgresql://user:pass@/bookshelf_dev?host=/var/run/postgresql/.s.PGSQL.5432"
expect(subject.call(url)).to eq "postgresql://user:pass@/bookshelf_test?host=/var/run/postgresql/.s.PGSQL.5432"
end
end

it "appends to non-conforming paths" do
expect(subject.call("/bookshelf_database")).to eq "/bookshelf_database_test"
context "mysql scheme" do
include_examples "URL Transforms", :mysql
end

it "accepts any #to_s object such as URI" do
url = URI("postgres://localhost:5432/bookshelf_development")
expect(subject.call(url)).to eq "postgres://localhost:5432/bookshelf_test"
context "sqlite scheme" do
it "transforms _dev.db to _test.db" do
url = "sqlite://./config/bookshelf_dev.db"
expect(subject.call(url)).to eq "sqlite://./config/bookshelf_test.db"
end

it "transforms _development.db to _test.db" do
url = "sqlite:///app/config/bookshelf_development.db"
expect(subject.call(url)).to eq "sqlite:///app/config/bookshelf_test.db"
end

it "does not transform _test.db" do
url = "sqlite://./config/bookshelf_test.db"
expect(subject.call(url)).to eq "sqlite://./config/bookshelf_test.db"
end

it "appends to non-conforming filenames" do
url = "sqlite:///app/config/bookshelf.db"
expect(subject.call(url)).to eq "sqlite:///app/config/bookshelf_test.db"
end

it "ignores non-hierarchical databases" do
url = "sqlite::memory"
expect(subject.call(url)).to eq "sqlite::memory"
end
end
end

0 comments on commit baa22dd

Please sign in to comment.