innkeeper 0.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 (87) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.pryrc +3 -0
  4. data/.travis.yml +15 -0
  5. data/Appraisals +20 -0
  6. data/Gemfile +10 -0
  7. data/Guardfile +24 -0
  8. data/HISTORY.md +337 -0
  9. data/README.md +485 -0
  10. data/Rakefile +92 -0
  11. data/TODO.md +51 -0
  12. data/gemfiles/rails_5_1.gemfile +12 -0
  13. data/innkeeper.gemspec +42 -0
  14. data/lib/generators/innkeeper/install/USAGE +5 -0
  15. data/lib/generators/innkeeper/install/install_generator.rb +10 -0
  16. data/lib/generators/innkeeper/install/templates/innkeeper.rb +76 -0
  17. data/lib/innkeeper.rb +110 -0
  18. data/lib/innkeeper/adapters/abstract_adapter.rb +172 -0
  19. data/lib/innkeeper/adapters/mysql2_adapter.rb +47 -0
  20. data/lib/innkeeper/adapters/postgresql_adapter.rb +112 -0
  21. data/lib/innkeeper/console.rb +12 -0
  22. data/lib/innkeeper/deprecation.rb +10 -0
  23. data/lib/innkeeper/elevators/domain.rb +20 -0
  24. data/lib/innkeeper/elevators/generic.rb +32 -0
  25. data/lib/innkeeper/elevators/host_hash.rb +22 -0
  26. data/lib/innkeeper/elevators/subdomain.rb +62 -0
  27. data/lib/innkeeper/migrator.rb +33 -0
  28. data/lib/innkeeper/railtie.rb +56 -0
  29. data/lib/innkeeper/resolvers/abstract.rb +15 -0
  30. data/lib/innkeeper/resolvers/database.rb +11 -0
  31. data/lib/innkeeper/resolvers/schema.rb +14 -0
  32. data/lib/innkeeper/tasks/enhancements.rb +36 -0
  33. data/lib/innkeeper/tenant.rb +47 -0
  34. data/lib/innkeeper/version.rb +3 -0
  35. data/lib/tasks/innkeeper.rake +128 -0
  36. data/notes.md +31 -0
  37. data/test/config_test.rb +52 -0
  38. data/test/databases.yml.sample +37 -0
  39. data/test/decorator_test.rb +21 -0
  40. data/test/domain_elevator_test.rb +38 -0
  41. data/test/dummy/Rakefile +7 -0
  42. data/test/dummy/app/controllers/application_controller.rb +6 -0
  43. data/test/dummy/app/helpers/application_helper.rb +2 -0
  44. data/test/dummy/app/models/company.rb +3 -0
  45. data/test/dummy/app/models/user.rb +3 -0
  46. data/test/dummy/app/views/application/index.html.erb +1 -0
  47. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  48. data/test/dummy/config.ru +4 -0
  49. data/test/dummy/config/application.rb +49 -0
  50. data/test/dummy/config/boot.rb +11 -0
  51. data/test/dummy/config/database.yml.sample +38 -0
  52. data/test/dummy/config/environment.rb +5 -0
  53. data/test/dummy/config/environments/development.rb +27 -0
  54. data/test/dummy/config/environments/production.rb +51 -0
  55. data/test/dummy/config/environments/test.rb +34 -0
  56. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  57. data/test/dummy/config/initializers/inflections.rb +10 -0
  58. data/test/dummy/config/initializers/innkeeper.rb +4 -0
  59. data/test/dummy/config/initializers/mime_types.rb +5 -0
  60. data/test/dummy/config/initializers/secret_token.rb +7 -0
  61. data/test/dummy/config/initializers/session_store.rb +8 -0
  62. data/test/dummy/config/locales/en.yml +5 -0
  63. data/test/dummy/config/routes.rb +3 -0
  64. data/test/dummy/db/schema.rb +19 -0
  65. data/test/dummy/db/seeds.rb +5 -0
  66. data/test/dummy/db/seeds/import.rb +5 -0
  67. data/test/dummy/public/404.html +26 -0
  68. data/test/dummy/public/422.html +26 -0
  69. data/test/dummy/public/500.html +26 -0
  70. data/test/dummy/public/favicon.ico +0 -0
  71. data/test/dummy/public/stylesheets/.gitkeep +0 -0
  72. data/test/dummy/script/rails +6 -0
  73. data/test/excluded_models_test.rb +32 -0
  74. data/test/generic_elevator_test.rb +63 -0
  75. data/test/host_hash_elevator_test.rb +42 -0
  76. data/test/innkeeper_test.rb +96 -0
  77. data/test/mocks/adapter_mock.rb +11 -0
  78. data/test/multithreading_test.rb +37 -0
  79. data/test/mysql2_adapter_test.rb +17 -0
  80. data/test/postgresql_adapter_test.rb +39 -0
  81. data/test/railtie_test.rb +31 -0
  82. data/test/rake_task_test.rb +57 -0
  83. data/test/resolver_test.rb +21 -0
  84. data/test/shared/shared_adapter_tests.rb +95 -0
  85. data/test/subdomain_elevator_test.rb +75 -0
  86. data/test/test_helper.rb +24 -0
  87. metadata +325 -0
@@ -0,0 +1,63 @@
1
+ require_relative 'test_helper'
2
+ require_relative 'mocks/adapter_mock'
3
+ require 'innkeeper/elevators/generic'
4
+
5
+ class GenericElevatorTest < Minitest::Test
6
+ include AdapterMock
7
+
8
+ class MyElevator < Innkeeper::Elevators::Generic
9
+ def parse_tenant_name(*)
10
+ 'tenant2'
11
+ end
12
+ end
13
+
14
+ def setup
15
+ @elevator = Innkeeper::Elevators::Generic.new(Proc.new{})
16
+
17
+ super
18
+ end
19
+
20
+ def test_processor_is_called_if_given
21
+ elevator = Innkeeper::Elevators::Generic.new(Proc.new{}, Proc.new{'tenant1'})
22
+
23
+ with_adapter_mocked do |adapter|
24
+ adapter.expect :switch, true, ['tenant1']
25
+
26
+ elevator.call('HTTP_HOST' => 'foo.bar.com')
27
+
28
+ assert adapter.verify
29
+ end
30
+ end
31
+
32
+ def test_raises_if_parse_tenant_name_not_implemented
33
+ assert_raises RuntimeError do
34
+ @elevator.call('HTTP_HOST' => 'foo.bar.com')
35
+ end
36
+ end
37
+
38
+ def test_switches_to_the_parsed_db_name
39
+ elevator = MyElevator.new(Proc.new{})
40
+
41
+ with_adapter_mocked do |adapter|
42
+ adapter.expect :switch, true, ['tenant2']
43
+
44
+ elevator.call('HTTP_HOST' => 'foo.bar.com')
45
+
46
+ assert adapter.verify
47
+ end
48
+ end
49
+
50
+ def test_does_not_call_switch_if_no_database_given
51
+ app_mock = Minitest::Mock.new
52
+ app_mock.expect :call, true, [{'HTTP_HOST' => 'foo.bar.com'}]
53
+ elevator = MyElevator.new(app_mock, Proc.new{})
54
+
55
+ with_adapter_mocked do |adapter|
56
+ elevator.call('HTTP_HOST' => 'foo.bar.com')
57
+
58
+ assert adapter.verify
59
+ end
60
+
61
+ assert app_mock.verify
62
+ end
63
+ end
@@ -0,0 +1,42 @@
1
+ require_relative 'test_helper'
2
+ require_relative 'mocks/adapter_mock'
3
+ require 'innkeeper/elevators/host_hash'
4
+
5
+ class HostHashElevatorTest < Minitest::Test
6
+ include AdapterMock
7
+
8
+ def setup
9
+ @elevator = Innkeeper::Elevators::HostHash.new(Proc.new{}, 'example.com' => 'example_tenant')
10
+
11
+ super
12
+ end
13
+
14
+ def test_parses_host_from_domain_name
15
+ request = ActionDispatch::Request.new('HTTP_HOST' => 'example.com')
16
+ assert_equal 'example_tenant', @elevator.parse_tenant_name(request)
17
+ end
18
+
19
+ def test_raises_exception_if_no_host
20
+ request = ActionDispatch::Request.new('HTTP_HOST' => '')
21
+ assert_raises Innkeeper::TenantNotFound do
22
+ @elevator.parse_tenant_name(request)
23
+ end
24
+ end
25
+
26
+ def test_raises_exception_if_host_not_found
27
+ request = ActionDispatch::Request.new('HTTP_HOST' => 'example2.com')
28
+ assert_raises Innkeeper::TenantNotFound do
29
+ @elevator.parse_tenant_name(request)
30
+ end
31
+ end
32
+
33
+ def test_switches_to_proper_tenant
34
+ with_adapter_mocked do |adapter|
35
+ adapter.expect :switch, true, ['example_tenant']
36
+
37
+ @elevator.call('HTTP_HOST' => 'example.com')
38
+
39
+ assert adapter.verify
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,96 @@
1
+ require "minitest/autorun"
2
+
3
+ module Innkeeper
4
+ class Test < Minitest::Test
5
+ def setup_connection(db)
6
+ @config = Innkeeper::TestHelper.config['connections'][db].symbolize_keys
7
+ ActiveRecord::Base.establish_connection(@config)
8
+ # `establish_connection` sets @connection_specification_name on
9
+ # ActiveRecord::Base, this causes it to override our Thread local.
10
+ # `establish_connection` should never be used in a productiion app
11
+ # for this reason.
12
+ Innkeeper.connection_class.connection_specification_name = nil
13
+ Innkeeper.reset
14
+ end
15
+
16
+ def setup
17
+ Innkeeper::Tenant.reload!
18
+ @adapter = Innkeeper::Tenant.adapter
19
+ @tenant1 = self.class.next_db
20
+ @tenant2 = self.class.next_db
21
+ @adapter.create(@tenant1)
22
+ @adapter.create(@tenant2)
23
+ end
24
+
25
+ def teardown
26
+ @adapter.reset
27
+
28
+ tenants = [@tenant1, @tenant2]
29
+
30
+ if @adapter.class.name == "Innkeeper::Adapters::PostgresqlAdapter"
31
+ @postgres_dbs ? drop(tenants, :database) : drop(tenants, :schema)
32
+ else
33
+ drop(tenants)
34
+ end
35
+
36
+ Innkeeper.excluded_models.each do |excl|
37
+ excl.constantize.connection_specification_name = nil
38
+ end
39
+
40
+ Innkeeper.connection_class.clear_all_connections!
41
+ # unless we remove the connection pools, the connection pools from
42
+ # previous tests containing configs with deleted databases,
43
+ # persist and cause bugs for future tests using the same
44
+ # host/adapter (so the spec name is the same)
45
+ Innkeeper.connection_class.connection_handler.tap do |ch|
46
+ ch.send(:owner_to_pool).each_key do |k|
47
+ ch.remove_connection(k) if k =~ /^_innkeeper/
48
+ end
49
+ end
50
+ Innkeeper.reset
51
+ Innkeeper::Tenant.reload!
52
+ end
53
+
54
+ def drop(tenants, type = nil)
55
+ meth = "drop"
56
+ meth += "_#{type}" if type
57
+
58
+ tenants.each{ |t| @adapter.send(meth, t) }
59
+ end
60
+
61
+ def self.next_db
62
+ @@x ||= 0
63
+ "db%d" % @@x += 1
64
+ end
65
+
66
+ def tenant_is(tenant, for_model: Innkeeper.connection_class)
67
+ config = Innkeeper::Tenant.config_for(tenant)
68
+
69
+ if @adapter.class.name == "Innkeeper::Adapters::PostgresqlAdapter"
70
+ current_search_path = for_model.connection.schema_search_path
71
+ end
72
+
73
+ config[:database] == for_model.connection.current_database &&
74
+ (!current_search_path || (current_search_path == config[:schema_search_path]) || current_search_path == "\"$user\", public") &&
75
+ (for_model != Innkeeper.connection_class || Innkeeper::Tenant.current == tenant)
76
+ end
77
+
78
+ def assert_tenant_is(tenant, for_model: Innkeeper.connection_class)
79
+ res = tenant_is(tenant, for_model: for_model)
80
+
81
+ if !res && @adapter.class.name == "Innkeeper::Adapters::PostgresqlAdapter"
82
+ schema = for_model.connection.schema_search_path
83
+ end
84
+
85
+ assert res, "Expected: #{tenant}\nActual: #{{ db: for_model.connection.current_database, schema: schema }}"
86
+ end
87
+
88
+ def assert_received(klass, meth, count = 1)
89
+ migrator_mock = Minitest::Mock.new
90
+ count.times{ migrator_mock.expect meth, true }
91
+ klass.stub(meth, ->(*){ migrator_mock.send(meth) }){ yield }
92
+
93
+ assert migrator_mock.verify
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,11 @@
1
+ module AdapterMock
2
+ def with_adapter_mocked
3
+ adapter = Minitest::Mock.new
4
+ old_adapter = Thread.current[:innkeeper_adapter]
5
+ Thread.current[:innkeeper_adapter] = adapter
6
+
7
+ yield adapter
8
+ ensure
9
+ Thread.current[:innkeeper_adapter] = old_adapter
10
+ end
11
+ end
@@ -0,0 +1,37 @@
1
+ require_relative 'test_helper'
2
+ require 'innkeeper/resolvers/database'
3
+
4
+ class MultithreadingTest < Innkeeper::Test
5
+ def setup
6
+ setup_connection("mysql")
7
+
8
+ Innkeeper.configure do |config|
9
+ # to test in connection switching mode as if switching between hosts
10
+ config.force_reconnect_on_switch = true
11
+ config.pool_per_config = true
12
+ config.tenant_resolver = Innkeeper::Resolvers::Database
13
+ end
14
+
15
+ super
16
+ end
17
+
18
+ def test_thread_safety_of_switching
19
+ assert_tenant_is(Innkeeper.default_tenant)
20
+
21
+ threads = []
22
+ 100.times do
23
+ threads << Thread.new do
24
+ db = [@tenant1, @tenant2].sample
25
+ Innkeeper::Tenant.switch!(db)
26
+
27
+ assert_tenant_is(db)
28
+
29
+ Innkeeper.connection_class.clear_active_connections!
30
+ end
31
+ end
32
+
33
+ threads.each(&:join)
34
+
35
+ assert_tenant_is(Innkeeper.default_tenant)
36
+ end
37
+ end
@@ -0,0 +1,17 @@
1
+ require_relative 'test_helper'
2
+ require 'innkeeper/resolvers/database'
3
+ require_relative 'shared/shared_adapter_tests'
4
+
5
+ class Mysql2AdapterTest < Innkeeper::Test
6
+ include SharedAdapterTests
7
+
8
+ def setup
9
+ setup_connection("mysql")
10
+
11
+ Innkeeper.configure do |config|
12
+ config.tenant_resolver = Innkeeper::Resolvers::Database
13
+ end
14
+
15
+ super
16
+ end
17
+ end
@@ -0,0 +1,39 @@
1
+ require_relative 'test_helper'
2
+ require 'innkeeper/resolvers/schema'
3
+ require 'innkeeper/resolvers/database'
4
+ require_relative 'shared/shared_adapter_tests'
5
+
6
+ class PostgresqlAdapterTest < Innkeeper::Test
7
+ include SharedAdapterTests
8
+
9
+ def setup
10
+ setup_connection("postgresql")
11
+
12
+ Innkeeper.configure do |config|
13
+ config.tenant_resolver = Innkeeper::Resolvers::Schema
14
+ end
15
+
16
+ super
17
+ end
18
+
19
+ # idk why it broked :'(
20
+ # def test_postgres_database_resolver_reconnects
21
+ # Innkeeper.tenant_resolver = Innkeeper::Resolvers::Database
22
+
23
+ # @adapter.create("db_tenant")
24
+
25
+ # assert_tenant_is(Innkeeper.default_tenant)
26
+
27
+ # conn_id = Innkeeper.connection.object_id
28
+
29
+ # Innkeeper::Tenant.switch("db_tenant") do
30
+ # refute_equal conn_id, Innkeeper.connection.object_id
31
+ # assert_equal "db_tenant", Innkeeper.connection.current_database
32
+ # end
33
+
34
+ # assert_tenant_is(Innkeeper.default_tenant)
35
+ # ensure
36
+ # @adapter.drop_database("db_tenant")
37
+ # Innkeeper.tenant_resolver = Innkeeper::Resolvers::Schema
38
+ # end
39
+ end
@@ -0,0 +1,31 @@
1
+ require_relative 'test_helper'
2
+ require 'innkeeper/resolvers/database'
3
+
4
+ class RailtieTest < Minitest::Test
5
+ def test_railtie_does_not_hold_onto_connection
6
+ Innkeeper.configure do |config|
7
+ config.tenant_resolver = Innkeeper::Resolvers::Database
8
+ config.excluded_models = %w(Company)
9
+ end
10
+
11
+ Innkeeper.connection_class.connection_pool.disconnect!
12
+
13
+ before = Innkeeper.connection_class.connection_pool.stat.slice(:busy, :dead, :waiting)
14
+
15
+ Innkeeper::Railtie.prep
16
+ Innkeeper::Railtie.config.to_prepare_blocks.map(&:call)
17
+
18
+ after = Innkeeper.connection_class.connection_pool.stat.slice(:busy, :dead, :waiting)
19
+
20
+ assert_equal before, after
21
+ end
22
+
23
+ def test_railtie_sets_default_configuration
24
+ Innkeeper::Railtie.prep
25
+
26
+ assert_equal [], Innkeeper.excluded_models
27
+ assert_equal false, Innkeeper.force_reconnect_on_switch
28
+ assert_equal false, Innkeeper.seed_after_create
29
+ assert_instance_of Innkeeper::Resolvers::Database, Innkeeper.tenant_resolver
30
+ end
31
+ end
@@ -0,0 +1,57 @@
1
+ require_relative 'test_helper'
2
+ require 'innkeeper/resolvers/database'
3
+ require 'rake'
4
+
5
+ class RakeTaskTest < Innkeeper::Test
6
+ def setup
7
+ setup_connection("mysql")
8
+
9
+ Innkeeper.configure do |config|
10
+ config.excluded_models = ["Company"]
11
+ config.tenant_names = lambda{ Company.pluck(:database) }
12
+ config.tenant_resolver = Innkeeper::Resolvers::Database
13
+ end
14
+
15
+ super
16
+
17
+ @rake = Rake::Application.new
18
+ Rake.application = @rake
19
+ Dummy::Application.load_tasks
20
+
21
+ # rails tasks running F up the schema...
22
+ Rake::Task.define_task('db:migrate')
23
+ Rake::Task.define_task('db:seed')
24
+ Rake::Task.define_task('db:rollback')
25
+ Rake::Task.define_task('db:migrate:up')
26
+ Rake::Task.define_task('db:migrate:down')
27
+ Rake::Task.define_task('db:migrate:redo')
28
+
29
+ @tenants = [@tenant1, @tenant2]
30
+ @tenants.each{ |t| Company.create(database: t) }
31
+ end
32
+
33
+ def teardown
34
+ Rake.application = nil
35
+ Company.delete_all
36
+
37
+ super
38
+ end
39
+
40
+ def test_all_databases_get_migrated
41
+ assert_received(Innkeeper::Migrator, :migrate, @tenants.size) do
42
+ @rake['innkeeper:migrate'].invoke
43
+ end
44
+ end
45
+
46
+ def test_all_databases_get_rolled_back
47
+ assert_received(Innkeeper::Migrator, :rollback, @tenants.size) do
48
+ @rake['innkeeper:rollback'].invoke
49
+ end
50
+ end
51
+
52
+ def test_all_databases_get_seeded
53
+ assert_received(Innkeeper::Tenant, :seed, @tenants.size) do
54
+ @rake['innkeeper:seed'].invoke
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,21 @@
1
+ require_relative 'test_helper'
2
+ require 'innkeeper/resolvers/database'
3
+ require 'innkeeper/resolvers/schema'
4
+
5
+ class ResolverTest < Minitest::Test
6
+ def test_database_resolver
7
+ resolver = Innkeeper::Resolvers::Database.new(Innkeeper.connection_config)
8
+ new_config = resolver.resolve("foobar")
9
+
10
+ assert_equal "foobar", new_config[:database]
11
+ end
12
+
13
+ def test_schema_resolver
14
+ Innkeeper.configure{ |config| config.persistent_schemas = ['a', 'b', 'c'] }
15
+
16
+ resolver = Innkeeper::Resolvers::Schema.new(Innkeeper.connection_config)
17
+ new_config = resolver.resolve("foobar")
18
+
19
+ assert_equal '"foobar", "a", "b", "c"', new_config[:schema_search_path]
20
+ end
21
+ end
@@ -0,0 +1,95 @@
1
+ module SharedAdapterTests
2
+ def test_switch
3
+ assert_tenant_is(Innkeeper.default_tenant)
4
+
5
+ Innkeeper::Tenant.switch(@tenant1){
6
+ assert_tenant_is(@tenant1)
7
+ }
8
+
9
+ assert_tenant_is(Innkeeper.default_tenant)
10
+ end
11
+
12
+ def test_local_switch_doesnt_modify_connection
13
+ assert_tenant_is(Innkeeper.default_tenant)
14
+
15
+ conn_id = Innkeeper.connection.object_id
16
+
17
+ Innkeeper::Tenant.switch!(@tenant1)
18
+
19
+ assert_tenant_is(@tenant1)
20
+ assert_equal conn_id, Innkeeper.connection.object_id
21
+ end
22
+
23
+ def test_remote_switch_modifies_connection
24
+ assert_tenant_is(Innkeeper.default_tenant)
25
+
26
+ conn_id = Innkeeper.connection.object_id
27
+
28
+ Innkeeper::Tenant.switch!(@config.dup.tap{ |c| c[:host] = 'localhost' })
29
+
30
+ assert_equal @config[:database], Innkeeper.connection.current_database
31
+ refute_equal conn_id, Innkeeper.connection.object_id
32
+ end
33
+
34
+ def test_force_reconnect
35
+ Innkeeper.configure{ |config| config.force_reconnect_on_switch = true }
36
+
37
+ assert_tenant_is(Innkeeper.default_tenant)
38
+
39
+ conn_id = Innkeeper.connection.object_id
40
+
41
+ Innkeeper::Tenant.switch!(@tenant1)
42
+
43
+ assert_tenant_is(@tenant1)
44
+ refute_equal conn_id, Innkeeper.connection.object_id
45
+ end
46
+
47
+ def test_switch_raises_error_for_unknown_database
48
+ assert_raises Innkeeper::TenantNotFound do
49
+ Innkeeper::Tenant.switch!("invalid")
50
+ end
51
+ end
52
+
53
+ def test_drop_raises_error_for_unknown_database
54
+ assert_raises Innkeeper::TenantNotFound do
55
+ if Innkeeper::Tenant.adapter.respond_to?(:drop_schema)
56
+ Innkeeper::Tenant.drop_schema("invalid")
57
+ else
58
+ Innkeeper::Tenant.drop("invalid")
59
+ end
60
+ end
61
+ end
62
+
63
+ def test_default_tenant_configuration_is_used
64
+ prev_default = Innkeeper.default_tenant
65
+
66
+ Innkeeper.configure do |config|
67
+ config.default_tenant = @tenant1
68
+ end
69
+
70
+ assert_equal @tenant1, Innkeeper.default_tenant
71
+
72
+ @adapter.reset
73
+
74
+ assert_tenant_is(@tenant1)
75
+ ensure
76
+ Innkeeper.default_tenant = prev_default
77
+ end
78
+
79
+ def test_ActiveRecord_QueryCache_cleared_after_switching_databases
80
+ [@tenant1, @tenant2].each do |tenant|
81
+ Innkeeper::Tenant.switch(tenant) do
82
+ User.create!(name: tenant)
83
+ end
84
+ end
85
+ Innkeeper.connection.enable_query_cache!
86
+
87
+ Innkeeper::Tenant.switch(@tenant1) do
88
+ assert User.find_by(name: @tenant1)
89
+ end
90
+
91
+ Innkeeper::Tenant.switch(@tenant2) do
92
+ assert_nil User.find_by(name: @tenant1)
93
+ end
94
+ end
95
+ end