motel-activerecord 1.0.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.
- checksums.yaml +7 -0
- data/README.md +225 -0
- data/VERSION +1 -0
- data/lib/motel-activerecord.rb +10 -0
- data/lib/motel/connection_adapters.rb +3 -0
- data/lib/motel/connection_adapters/connection_handler.rb +96 -0
- data/lib/motel/connection_adapters/connection_specification.rb +2 -0
- data/lib/motel/connection_adapters/connection_specification/resolver.rb +92 -0
- data/lib/motel/errors.rb +28 -0
- data/lib/motel/lobby.rb +47 -0
- data/lib/motel/manager.rb +69 -0
- data/lib/motel/multi_tenant.rb +53 -0
- data/lib/motel/railtie.rb +63 -0
- data/lib/motel/sources.rb +3 -0
- data/lib/motel/sources/database.rb +120 -0
- data/lib/motel/sources/default.rb +54 -0
- data/lib/motel/sources/redis.rb +80 -0
- data/lib/motel/version.rb +6 -0
- data/spec/lib/motel/connection_adapters/connection_handler_spec.rb +184 -0
- data/spec/lib/motel/connection_adapters/connection_specification/resolver_spec.rb +120 -0
- data/spec/lib/motel/lobby_spec.rb +155 -0
- data/spec/lib/motel/manager_spec.rb +238 -0
- data/spec/lib/motel/multi_tenant_spec.rb +169 -0
- data/spec/lib/motel/sources/database_spec.rb +246 -0
- data/spec/lib/motel/sources/default_spec.rb +167 -0
- data/spec/lib/motel/sources/redis_spec.rb +188 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/tmp/foo.sqlite3 +0 -0
- data/spec/tmp/tenants.sqlite3 +0 -0
- metadata +144 -0
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
module Motel
|
4
|
+
module Sources
|
5
|
+
|
6
|
+
class Redis
|
7
|
+
|
8
|
+
attr_accessor :host, :port, :password, :path, :prefix_tenant_alias
|
9
|
+
|
10
|
+
def initialize(config = {})
|
11
|
+
@host = config[:host]
|
12
|
+
@port = config[:port]
|
13
|
+
@password = config[:password]
|
14
|
+
@path = config[:path]
|
15
|
+
@prefix_tenant_alias = (config[:prefix_tenant_alias] || 'tenant:')
|
16
|
+
end
|
17
|
+
|
18
|
+
def tenants
|
19
|
+
redis.keys.inject({}) do |hash, tenant_als|
|
20
|
+
if tenant_als.match("^#{prefix_tenant_alias}")
|
21
|
+
hash[tenant_name(tenant_als)] = tenant(tenant_name(tenant_als))
|
22
|
+
end
|
23
|
+
hash
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def tenant(name)
|
28
|
+
spec = redis.hgetall(tenant_alias(name))
|
29
|
+
spec if spec.any?
|
30
|
+
end
|
31
|
+
|
32
|
+
def tenant?(name)
|
33
|
+
!tenant(name).nil?
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_tenant(name, spec)
|
37
|
+
raise ExistingTenantError if tenant?(name)
|
38
|
+
|
39
|
+
spec.each do |field, value|
|
40
|
+
redis.hset(tenant_alias(name), field, value)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def update_tenant(name, spec)
|
45
|
+
raise NonexistentTenantError unless tenant?(name)
|
46
|
+
|
47
|
+
spec.each do |field, value|
|
48
|
+
redis.hset(tenant_alias(name), field, value)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def delete_tenant(name)
|
53
|
+
if tenant?(name)
|
54
|
+
fields = redis.hkeys tenant_alias(name)
|
55
|
+
redis.hdel(tenant_alias(name), [*fields])
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def redis
|
62
|
+
@redis ||= begin
|
63
|
+
::Redis.new(host: host, port: port, password: password, path: path)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def tenant_alias(name)
|
68
|
+
"#{prefix_tenant_alias}#{name}"
|
69
|
+
end
|
70
|
+
|
71
|
+
def tenant_name(tenant_alias)
|
72
|
+
name = tenant_alias.match("#{prefix_tenant_alias}(.*)")
|
73
|
+
name[1] if name
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
@@ -0,0 +1,184 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Motel::ConnectionAdapters::ConnectionHandler do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@handler = Motel::ConnectionAdapters::ConnectionHandler.new
|
7
|
+
@handler.tenants_source.add_tenant('foo', FOO_SPEC)
|
8
|
+
@handler.tenants_source.add_tenant('bar', BAR_SPEC)
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '#establish_connection' do
|
12
|
+
|
13
|
+
context 'specifying only the tenant name' do
|
14
|
+
|
15
|
+
context 'existing tenant' do
|
16
|
+
|
17
|
+
it 'returns an instance of ConnectionPool' do
|
18
|
+
expect(
|
19
|
+
@handler.establish_connection('foo')
|
20
|
+
).to be_an_instance_of ActiveRecord::ConnectionAdapters::ConnectionPool
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'nonexistent tenant' do
|
26
|
+
|
27
|
+
it 'raise an error' do
|
28
|
+
expect{
|
29
|
+
@handler.establish_connection('baz')
|
30
|
+
}.to raise_error Motel::NonexistentTenantError
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'specifying tenant name and the spec' do
|
38
|
+
|
39
|
+
it 'returns an instance if ConnectionPool' do
|
40
|
+
resolver = Motel::ConnectionAdapters::ConnectionSpecification::Resolver.new
|
41
|
+
spec = resolver.spec(BAZ_SPEC)
|
42
|
+
expect(
|
43
|
+
@handler.establish_connection('baz', spec)
|
44
|
+
).to be_an_instance_of ActiveRecord::ConnectionAdapters::ConnectionPool
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
describe '#retrieve_connection' do
|
52
|
+
|
53
|
+
context 'existing tenant' do
|
54
|
+
|
55
|
+
it 'initializes and returns a connection' do
|
56
|
+
expect(@handler.retrieve_connection('foo')).to be_true
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'returns a connection' do
|
60
|
+
@pool = @handler.establish_connection('foo')
|
61
|
+
expect(@handler.retrieve_connection('foo')).to eq @pool.connection
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'nonexistent tenant' do
|
67
|
+
|
68
|
+
it 'raise an error' do
|
69
|
+
expect{
|
70
|
+
@handler.retrieve_connection('baz')
|
71
|
+
}.to raise_error Motel::NonexistentTenantError
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
describe '#retrieve_connection_pool' do
|
79
|
+
|
80
|
+
context 'existing tenant' do
|
81
|
+
|
82
|
+
it 'initializes and returns a connection pool' do
|
83
|
+
expect(@handler.retrieve_connection_pool('foo')).to be_true
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'returns a connection pool' do
|
87
|
+
@pool = @handler.establish_connection('foo')
|
88
|
+
expect(@handler.retrieve_connection_pool('foo')).to eq @pool
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
context 'nonexistent tenant' do
|
94
|
+
|
95
|
+
it 'raise an error' do
|
96
|
+
expect{
|
97
|
+
@handler.retrieve_connection('baz')
|
98
|
+
}.to raise_error Motel::NonexistentTenantError
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
describe '#connected?' do
|
106
|
+
|
107
|
+
it 'returns true' do
|
108
|
+
@handler.retrieve_connection('foo')
|
109
|
+
expect(@handler.connected?('foo')).to be_true
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'returns false' do
|
113
|
+
expect(@handler.connected?('foo')).to be_false
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
describe '#active_connections?' do
|
119
|
+
|
120
|
+
it 'has not active connections' do
|
121
|
+
expect(@handler.active_connections?).to be_false
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'has active connections' do
|
125
|
+
@handler.retrieve_connection('foo')
|
126
|
+
expect(@handler.active_connections?).to be_true
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
describe '#remove_connection' do
|
132
|
+
|
133
|
+
it 'removes the connection' do
|
134
|
+
@handler.retrieve_connection('foo')
|
135
|
+
expect(@handler.active_connections?).to be_true
|
136
|
+
|
137
|
+
@handler.remove_connection('foo')
|
138
|
+
expect(@handler.active_connections?).to be_false
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
describe '#active_tenants' do
|
144
|
+
|
145
|
+
context 'no active tenants' do
|
146
|
+
|
147
|
+
it 'returns empty array' do
|
148
|
+
expect(@handler.active_tenants).to be_empty
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
|
153
|
+
context 'are active tenants' do
|
154
|
+
|
155
|
+
before(:each) do
|
156
|
+
@handler.establish_connection('foo')
|
157
|
+
@handler.establish_connection('bar')
|
158
|
+
end
|
159
|
+
|
160
|
+
|
161
|
+
it 'returns exactly two names' do
|
162
|
+
expect(@handler.active_tenants.count).to eq 2
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'returns the names' do
|
166
|
+
expect(@handler.active_tenants).to include('foo', 'bar')
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
|
173
|
+
describe '#tenants_sources=' do
|
174
|
+
|
175
|
+
it 'sets a tenats source for teh resolver' do
|
176
|
+
@handler.tenants_source = Motel::Sources::Redis.new
|
177
|
+
|
178
|
+
expect(@handler.tenants_source).to be_an_instance_of Motel::Sources::Redis
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
|
183
|
+
end
|
184
|
+
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Motel::ConnectionAdapters::ConnectionSpecification::Resolver do
|
4
|
+
|
5
|
+
describe '#spec' do
|
6
|
+
|
7
|
+
context 'from a hash' do
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
@resolver = Motel::ConnectionAdapters::ConnectionSpecification::Resolver.new
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'specifying adapter' do
|
14
|
+
|
15
|
+
context 'existing adapter' do
|
16
|
+
|
17
|
+
it 'returns an instance of ConnectionSpecification' do
|
18
|
+
expect(
|
19
|
+
@resolver.spec(BAZ_SPEC)
|
20
|
+
).to be_an_instance_of ActiveRecord::ConnectionAdapters::ConnectionSpecification
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'nonexistent adapter' do
|
26
|
+
|
27
|
+
it 'rises an error' do
|
28
|
+
expect{@resolver.spec({adapter: 'nonexistent_adapter'})}.to raise_error LoadError
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'adapter unspecified' do
|
36
|
+
|
37
|
+
it 'rises an error' do
|
38
|
+
expect{
|
39
|
+
@resolver.spec({database: BAZ_SPEC['database']})
|
40
|
+
}.to raise_error ActiveRecord::AdapterNotSpecified
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'from a string' do
|
48
|
+
|
49
|
+
before(:each) do
|
50
|
+
@resolver = Motel::ConnectionAdapters::ConnectionSpecification::Resolver.new(
|
51
|
+
{'foo' => FOO_SPEC}
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'string as a key' do
|
56
|
+
|
57
|
+
context 'existing configuration' do
|
58
|
+
|
59
|
+
it 'returns an instance of ConnectionSpecification' do
|
60
|
+
expect(
|
61
|
+
@resolver.spec('foo')
|
62
|
+
).to be_an_instance_of ActiveRecord::ConnectionAdapters::ConnectionSpecification
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'nonexistent configuration' do
|
68
|
+
|
69
|
+
it 'rises an error' do
|
70
|
+
expect{@resolver.spec('baz')}.to raise_error ActiveRecord::AdapterNotSpecified
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'string as a url' do
|
78
|
+
|
79
|
+
before(:all) do
|
80
|
+
@url = 'mysql2://foo:foobar_password@localhost:3306/foobar'
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'returns an instance of ConnectionSpecification' do
|
84
|
+
expect(
|
85
|
+
@resolver.spec(@url)
|
86
|
+
).to be_an_instance_of ActiveRecord::ConnectionAdapters::ConnectionSpecification
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'spec of connection specifications contains a correct adapter' do
|
90
|
+
expect(@resolver.spec(@url).config[:adapter]).to eq 'mysql2'
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'spec of connection specifications contains a correct username' do
|
94
|
+
expect(@resolver.spec(@url).config[:username]).to eq 'foo'
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'spec of connection specifications contains a correct password' do
|
98
|
+
expect(@resolver.spec(@url).config[:password]).to eq 'foobar_password'
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'spec of connection specifications contains a correct host' do
|
102
|
+
expect(@resolver.spec(@url).config[:host]).to eq 'localhost'
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'spec of connection specifications contains a correct port' do
|
106
|
+
expect(@resolver.spec(@url).config[:port]).to eq 3306
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'spec of connection specifications contains a correct database' do
|
110
|
+
expect(@resolver.spec(@url).config[:database]).to eq 'foobar'
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
@@ -0,0 +1,155 @@
|
|
1
|
+
ENV['RACK_ENV'] = 'test'
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'rack/test'
|
5
|
+
require 'tempfile'
|
6
|
+
|
7
|
+
describe Motel::Lobby do
|
8
|
+
include Rack::Test::Methods
|
9
|
+
|
10
|
+
def app
|
11
|
+
app = lambda{ |env| [200, {"Content-Type" => "text/html"}, "Test"] }
|
12
|
+
Motel::Lobby.new(app)
|
13
|
+
end
|
14
|
+
|
15
|
+
before(:all) do
|
16
|
+
ActiveRecord::Base.motel.add_tenant('foo', FOO_SPEC)
|
17
|
+
end
|
18
|
+
|
19
|
+
after(:all) do
|
20
|
+
ActiveRecord::Base.motel.delete_tenant('foo')
|
21
|
+
ActiveRecord::Base.motel.admission_criteria = nil #sets default
|
22
|
+
ActiveRecord::Base.motel.nonexistent_tenant_page = nil #sets default
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '#call' do
|
26
|
+
|
27
|
+
context 'default admission criteria' do
|
28
|
+
|
29
|
+
before(:all) do
|
30
|
+
ActiveRecord::Base.motel.admission_criteria = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'url match' do
|
34
|
+
|
35
|
+
context 'existing tenant' do
|
36
|
+
|
37
|
+
before(:each) do
|
38
|
+
@url = 'http://foo.test.com'
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'sets the current tenant' do
|
42
|
+
request @url
|
43
|
+
|
44
|
+
expect(ActiveRecord::Base.motel.current_tenant).to eq 'foo'
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'response is ok' do
|
48
|
+
request @url
|
49
|
+
|
50
|
+
expect(last_response).to be_ok
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'nonexistent tenant' do
|
56
|
+
|
57
|
+
before(:each) do
|
58
|
+
@url = 'http://bar.test.com'
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'returns 404 code' do
|
62
|
+
request @url
|
63
|
+
|
64
|
+
expect(last_response.status).to eq 404
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'with default nonexistent tenant message' do
|
68
|
+
|
69
|
+
it 'returns default message on body' do
|
70
|
+
ActiveRecord::Base.motel.nonexistent_tenant_page = nil
|
71
|
+
request @url
|
72
|
+
|
73
|
+
expect(last_response.body).to eq "Nonexistent bar tenant"
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
context 'specifying the path of the page with the nonexistent tenant message' do
|
79
|
+
|
80
|
+
it 'returns default message on body' do
|
81
|
+
message = '<h1>Noexistent tenant<h1>'
|
82
|
+
file = Tempfile.new(['status_404', '.html'], TEMP_DIR)
|
83
|
+
file.write(message)
|
84
|
+
file.close
|
85
|
+
|
86
|
+
ActiveRecord::Base.motel.nonexistent_tenant_page = file.path
|
87
|
+
request @url
|
88
|
+
|
89
|
+
expect(last_response.body).to eq message
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
context 'specifying admission criteria' do
|
103
|
+
|
104
|
+
before(:all) do
|
105
|
+
ActiveRecord::Base.motel.admission_criteria = 'tenants\/(\w*)'
|
106
|
+
end
|
107
|
+
|
108
|
+
context 'url match' do
|
109
|
+
|
110
|
+
context 'existing tenant' do
|
111
|
+
|
112
|
+
before(:each) do
|
113
|
+
@url = 'http://www.example.com/tenants/foo'
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'sets the current tenant' do
|
117
|
+
request @url
|
118
|
+
|
119
|
+
expect(ActiveRecord::Base.motel.current_tenant).to eq 'foo'
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'response is ok' do
|
123
|
+
request @url
|
124
|
+
|
125
|
+
expect(last_response).to be_ok
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
context 'url does not match' do
|
133
|
+
|
134
|
+
before(:each) do
|
135
|
+
@url = 'http://example.com'
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'sets null the current tenant' do
|
139
|
+
request @url
|
140
|
+
|
141
|
+
expect(ActiveRecord::Base.motel.current_tenant).to be_nil
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'response is ok' do
|
145
|
+
request @url
|
146
|
+
|
147
|
+
expect(last_response).to be_ok
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
|