analysand 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README +41 -0
- data/Rakefile +8 -0
- data/analysand.gemspec +30 -0
- data/bin/analysand +28 -0
- data/lib/analysand.rb +5 -0
- data/lib/analysand/bulk_response.rb +14 -0
- data/lib/analysand/change_watcher.rb +276 -0
- data/lib/analysand/connection_testing.rb +46 -0
- data/lib/analysand/database.rb +492 -0
- data/lib/analysand/errors.rb +26 -0
- data/lib/analysand/instance.rb +156 -0
- data/lib/analysand/response.rb +45 -0
- data/lib/analysand/version.rb +3 -0
- data/lib/analysand/view_response.rb +24 -0
- data/script/setup_database.rb +45 -0
- data/spec/analysand/a_session_grantor.rb +40 -0
- data/spec/analysand/change_watcher_spec.rb +84 -0
- data/spec/analysand/database_spec.rb +228 -0
- data/spec/analysand/database_writing_spec.rb +478 -0
- data/spec/analysand/instance_spec.rb +86 -0
- data/spec/analysand/response_spec.rb +22 -0
- data/spec/analysand/view_response_spec.rb +33 -0
- data/spec/fixtures/vcr_cassettes/get_session_does_not_refresh_cookie.yml +73 -0
- data/spec/fixtures/vcr_cassettes/get_session_refreshes_cookie.yml +75 -0
- data/spec/fixtures/vcr_cassettes/head_request_with_etag.yml +40 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/support/database_access.rb +40 -0
- data/spec/support/example_isolation.rb +86 -0
- data/spec/support/test_parameters.rb +39 -0
- metadata +276 -0
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'analysand/errors'
|
4
|
+
require 'analysand/instance'
|
5
|
+
require 'uri'
|
6
|
+
require 'vcr'
|
7
|
+
|
8
|
+
require File.expand_path('../a_session_grantor', __FILE__)
|
9
|
+
|
10
|
+
module Analysand
|
11
|
+
describe Instance do
|
12
|
+
describe '#initialize' do
|
13
|
+
it 'requires an absolute URI' do
|
14
|
+
lambda { Instance.new(URI("/abc")) }.should raise_error(InvalidURIError)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
let(:instance) { Instance.new(instance_uri) }
|
19
|
+
|
20
|
+
describe '#establish_session' do
|
21
|
+
describe 'given admin credentials' do
|
22
|
+
let(:credentials) { admin_credentials }
|
23
|
+
|
24
|
+
it_should_behave_like 'a session grantor'
|
25
|
+
end
|
26
|
+
|
27
|
+
describe 'given member credentials' do
|
28
|
+
let(:credentials) { member1_credentials }
|
29
|
+
|
30
|
+
it_should_behave_like 'a session grantor'
|
31
|
+
end
|
32
|
+
|
33
|
+
describe 'given incorrect credentials' do
|
34
|
+
it 'returns [nil, response]' do
|
35
|
+
session, resp = instance.establish_session('wrong', 'wrong')
|
36
|
+
|
37
|
+
session.should be_nil
|
38
|
+
resp.code.should == '401'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '#renew_session' do
|
44
|
+
let(:credentials) { admin_credentials }
|
45
|
+
|
46
|
+
before do
|
47
|
+
@session, _ = instance.establish_session(credentials[:username], credentials[:password])
|
48
|
+
end
|
49
|
+
|
50
|
+
describe 'if CouchDB refreshes the session cookie' do
|
51
|
+
around do |example|
|
52
|
+
VCR.use_cassette('get_session_refreshes_cookie') { example.call }
|
53
|
+
end
|
54
|
+
|
55
|
+
it_should_behave_like 'a session grantor' do
|
56
|
+
let(:result) { instance.renew_session(@session) }
|
57
|
+
let(:role_locator) do
|
58
|
+
lambda { |resp| resp['userCtx']['roles'] }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe 'if CouchDB does not refresh the session cookie' do
|
64
|
+
around do |example|
|
65
|
+
VCR.use_cassette('get_session_does_not_refresh_cookie') { example.call }
|
66
|
+
end
|
67
|
+
|
68
|
+
it_should_behave_like 'a session grantor' do
|
69
|
+
let(:result) { instance.renew_session(@session) }
|
70
|
+
let(:role_locator) do
|
71
|
+
lambda { |resp| resp['userCtx']['roles'] }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe 'given an invalid session' do
|
77
|
+
it 'returns [nil, response]' do
|
78
|
+
session, resp = instance.renew_session({ :token => 'AuthSession=wrong' })
|
79
|
+
|
80
|
+
session.should be_nil
|
81
|
+
resp.code.should == '400'
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'analysand/database'
|
4
|
+
require 'analysand/response'
|
5
|
+
|
6
|
+
module Analysand
|
7
|
+
describe Response do
|
8
|
+
let(:db) { Database.new(database_uri) }
|
9
|
+
|
10
|
+
describe '#etag' do
|
11
|
+
let(:response) do
|
12
|
+
VCR.use_cassette('head_request_with_etag') do
|
13
|
+
db.head('abc123', admin_credentials)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'removes quotes from ETags' do
|
18
|
+
response.etag.should == '1-967a00dff5e02add41819138abb3284d'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'analysand/view_response'
|
4
|
+
|
5
|
+
module Analysand
|
6
|
+
describe ViewResponse do
|
7
|
+
describe '#docs' do
|
8
|
+
let(:resp_with_docs) do
|
9
|
+
'{"rows": [{"id":"foo","key":"foo","value":{},"doc":{"foo":"bar"}}]}'
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:resp_without_docs) do
|
13
|
+
'{"rows": [{"id":"foo","key":"foo","value":{}}]}'
|
14
|
+
end
|
15
|
+
|
16
|
+
describe 'if the view includes docs' do
|
17
|
+
subject { ViewResponse.new(stub(:body => resp_with_docs)) }
|
18
|
+
|
19
|
+
it 'returns the value of the "doc" key in each row' do
|
20
|
+
subject.docs.should == [{'foo' => 'bar'}]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe 'if the view does not include docs' do
|
25
|
+
subject { ViewResponse.new(stub(:body => resp_without_docs)) }
|
26
|
+
|
27
|
+
it 'returns []' do
|
28
|
+
subject.docs.should == []
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
---
|
2
|
+
http_interactions:
|
3
|
+
- request:
|
4
|
+
method: post
|
5
|
+
uri: http://localhost:5984/_session
|
6
|
+
body:
|
7
|
+
string: name=admin&password=admin
|
8
|
+
headers:
|
9
|
+
Connection:
|
10
|
+
- keep-alive
|
11
|
+
Accept:
|
12
|
+
- "*/*"
|
13
|
+
Keep-Alive:
|
14
|
+
- 30
|
15
|
+
Content-Type:
|
16
|
+
- application/x-www-form-urlencoded
|
17
|
+
response:
|
18
|
+
status:
|
19
|
+
code: 200
|
20
|
+
message: OK
|
21
|
+
headers:
|
22
|
+
Date:
|
23
|
+
- Sat, 31 Mar 2012 22:35:18 GMT
|
24
|
+
Set-Cookie:
|
25
|
+
- AuthSession=YWRtaW46NEY3Nzg2QTY6WVqDNxuHpu4OPp07rib-n9WQLt8; Version=1; Path=/; HttpOnly
|
26
|
+
Server:
|
27
|
+
- CouchDB/1.3.0a-d0a5bf0-git (Erlang OTP/R14B02)
|
28
|
+
Content-Length:
|
29
|
+
- "56"
|
30
|
+
Content-Type:
|
31
|
+
- text/plain; charset=utf-8
|
32
|
+
Cache-Control:
|
33
|
+
- must-revalidate
|
34
|
+
body:
|
35
|
+
string: |
|
36
|
+
{"ok":true,"name":null,"roles":["_admin","site_admin"]}
|
37
|
+
recorded_at: Sat, 31 Mar 2012 22:35:18 GMT
|
38
|
+
- request:
|
39
|
+
method: get
|
40
|
+
uri: http://localhost:5984/_session
|
41
|
+
headers:
|
42
|
+
Cookie:
|
43
|
+
- AuthSession=YWRtaW46NEY3Nzg2QTY6WVqDNxuHpu4OPp07rib-n9WQLt8; Version=1; Path=/; HttpOnly
|
44
|
+
Connection:
|
45
|
+
- keep-alive
|
46
|
+
Accept:
|
47
|
+
- "*/*"
|
48
|
+
Keep-Alive:
|
49
|
+
- 30
|
50
|
+
Content-Type:
|
51
|
+
- application/x-www-form-urlencoded
|
52
|
+
response:
|
53
|
+
status:
|
54
|
+
code: 200
|
55
|
+
message: OK
|
56
|
+
headers:
|
57
|
+
Date:
|
58
|
+
- Sat, 31 Mar 2012 22:35:18 GMT
|
59
|
+
Server:
|
60
|
+
- CouchDB/1.3.0a-d0a5bf0-git (Erlang OTP/R14B02)
|
61
|
+
Content-Length:
|
62
|
+
- "56"
|
63
|
+
Content-Type:
|
64
|
+
- text/plain; charset=utf-8
|
65
|
+
Cache-Control:
|
66
|
+
- must-revalidate
|
67
|
+
body:
|
68
|
+
string: |
|
69
|
+
{"ok":true,"userCtx":{"name":null,"roles":["_admin","site_admin"]},"info":{"authentication_db":"_users","authentication_handlers":["oauth","cookie","default"]}}
|
70
|
+
|
71
|
+
http_version:
|
72
|
+
recorded_at: Sat, 31 Mar 2012 22:35:18 GMT
|
73
|
+
recorded_with: VCR 2.0.1
|
@@ -0,0 +1,75 @@
|
|
1
|
+
---
|
2
|
+
http_interactions:
|
3
|
+
- request:
|
4
|
+
method: post
|
5
|
+
uri: http://localhost:5984/_session
|
6
|
+
body:
|
7
|
+
string: name=admin&password=admin
|
8
|
+
headers:
|
9
|
+
Connection:
|
10
|
+
- keep-alive
|
11
|
+
Accept:
|
12
|
+
- "*/*"
|
13
|
+
Keep-Alive:
|
14
|
+
- 30
|
15
|
+
Content-Type:
|
16
|
+
- application/x-www-form-urlencoded
|
17
|
+
response:
|
18
|
+
status:
|
19
|
+
code: 200
|
20
|
+
message: OK
|
21
|
+
headers:
|
22
|
+
Date:
|
23
|
+
- Sat, 31 Mar 2012 22:35:18 GMT
|
24
|
+
Set-Cookie:
|
25
|
+
- AuthSession=YWRtaW46NEY3Nzg2QTY6WVqDNxuHpu4OPp07rib-n9WQLt8; Version=1; Path=/; HttpOnly
|
26
|
+
Server:
|
27
|
+
- CouchDB/1.3.0a-d0a5bf0-git (Erlang OTP/R14B02)
|
28
|
+
Content-Length:
|
29
|
+
- "56"
|
30
|
+
Content-Type:
|
31
|
+
- text/plain; charset=utf-8
|
32
|
+
Cache-Control:
|
33
|
+
- must-revalidate
|
34
|
+
body:
|
35
|
+
string: |
|
36
|
+
{"ok":true,"name":null,"roles":["_admin","site_admin"]}
|
37
|
+
recorded_at: Sat, 31 Mar 2012 22:35:18 GMT
|
38
|
+
- request:
|
39
|
+
method: get
|
40
|
+
uri: http://localhost:5984/_session
|
41
|
+
headers:
|
42
|
+
Cookie:
|
43
|
+
- AuthSession=YWRtaW46NEY3Nzg2QTY6WVqDNxuHpu4OPp07rib-n9WQLt8; Version=1; Path=/; HttpOnly
|
44
|
+
Connection:
|
45
|
+
- keep-alive
|
46
|
+
Accept:
|
47
|
+
- "*/*"
|
48
|
+
Keep-Alive:
|
49
|
+
- 30
|
50
|
+
Content-Type:
|
51
|
+
- application/x-www-form-urlencoded
|
52
|
+
response:
|
53
|
+
status:
|
54
|
+
code: 200
|
55
|
+
message: OK
|
56
|
+
headers:
|
57
|
+
Date:
|
58
|
+
- Sat, 31 Mar 2012 22:35:18 GMT
|
59
|
+
Set-Cookie:
|
60
|
+
- AuthSession=YWRtaW46NEY3Nzg3RTk6Lj1LSc4dWHBMZoFKm9ZMxq0L3Ec; Version=1; Path=/; HttpOnly
|
61
|
+
Server:
|
62
|
+
- CouchDB/1.3.0a-d0a5bf0-git (Erlang OTP/R14B02)
|
63
|
+
Content-Length:
|
64
|
+
- "56"
|
65
|
+
Content-Type:
|
66
|
+
- text/plain; charset=utf-8
|
67
|
+
Cache-Control:
|
68
|
+
- must-revalidate
|
69
|
+
body:
|
70
|
+
string: |
|
71
|
+
{"ok":true,"userCtx":{"name":null,"roles":["_admin","site_admin"]},"info":{"authentication_db":"_users","authentication_handlers":["oauth","cookie","default"]}}
|
72
|
+
|
73
|
+
http_version:
|
74
|
+
recorded_at: Sat, 31 Mar 2012 22:35:18 GMT
|
75
|
+
recorded_with: VCR 2.0.1
|
@@ -0,0 +1,40 @@
|
|
1
|
+
---
|
2
|
+
http_interactions:
|
3
|
+
- request:
|
4
|
+
method: head
|
5
|
+
uri: http://admin:admin@localhost:5984/analysand_test/abc123
|
6
|
+
body:
|
7
|
+
encoding: US-ASCII
|
8
|
+
string: ''
|
9
|
+
headers:
|
10
|
+
Accept:
|
11
|
+
- ! '*/*'
|
12
|
+
User-Agent:
|
13
|
+
- Ruby
|
14
|
+
Connection:
|
15
|
+
- keep-alive
|
16
|
+
Keep-Alive:
|
17
|
+
- 30
|
18
|
+
response:
|
19
|
+
status:
|
20
|
+
code: 200
|
21
|
+
message: OK
|
22
|
+
headers:
|
23
|
+
Server:
|
24
|
+
- CouchDB/1.3.0a-d6ab08d-git (Erlang OTP/R15B)
|
25
|
+
Etag:
|
26
|
+
- ! '"1-967a00dff5e02add41819138abb3284d"'
|
27
|
+
Date:
|
28
|
+
- Tue, 22 May 2012 14:21:26 GMT
|
29
|
+
Content-Type:
|
30
|
+
- text/plain; charset=utf-8
|
31
|
+
Content-Length:
|
32
|
+
- '61'
|
33
|
+
Cache-Control:
|
34
|
+
- must-revalidate
|
35
|
+
body:
|
36
|
+
encoding: US-ASCII
|
37
|
+
string: ''
|
38
|
+
http_version:
|
39
|
+
recorded_at: Tue, 22 May 2012 14:21:26 GMT
|
40
|
+
recorded_with: VCR 2.0.1
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
|
2
|
+
|
3
|
+
require File.expand_path('../support/database_access', __FILE__)
|
4
|
+
require File.expand_path('../support/example_isolation', __FILE__)
|
5
|
+
require File.expand_path('../support/test_parameters', __FILE__)
|
6
|
+
|
7
|
+
require 'vcr'
|
8
|
+
|
9
|
+
RSpec.configure do |config|
|
10
|
+
config.include DatabaseAccess
|
11
|
+
config.include ExampleIsolation
|
12
|
+
config.include TestParameters
|
13
|
+
end
|
14
|
+
|
15
|
+
VCR.configure do |c|
|
16
|
+
c.allow_http_connections_when_no_cassette = true
|
17
|
+
c.cassette_library_dir = 'spec/fixtures/vcr_cassettes'
|
18
|
+
c.hook_into :webmock
|
19
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
module DatabaseAccess
|
4
|
+
##
|
5
|
+
# Sets members and admins for the database.
|
6
|
+
#
|
7
|
+
# Assumes #admin_credentials returns a Hash containing the keys :username and
|
8
|
+
# :password, where those keys' values are the username and password of a
|
9
|
+
# CouchDB admin user.
|
10
|
+
def set_security(members, admins = {})
|
11
|
+
doc = {
|
12
|
+
'members' => members,
|
13
|
+
'admins' => admins
|
14
|
+
}
|
15
|
+
|
16
|
+
credentials = admin_credentials
|
17
|
+
uri = instance_uri
|
18
|
+
|
19
|
+
Net::HTTP.start(uri.host, uri.port) do |h|
|
20
|
+
req = Net::HTTP::Put.new("/#{database_name}/_security")
|
21
|
+
req.basic_auth(credentials[:username], credentials[:password])
|
22
|
+
req.body = doc.to_json
|
23
|
+
|
24
|
+
resp = h.request(req)
|
25
|
+
|
26
|
+
unless Net::HTTPSuccess === resp
|
27
|
+
raise "Unable to set security parameters on #{database_name}: response code = #{resp.code}, body = #{resp.body}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# Resets member and admin lists for the test database to [].
|
34
|
+
def clear_security
|
35
|
+
set_security({ 'users' => [], 'roles' => [] },
|
36
|
+
{ 'users' => [], 'roles' => [] })
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# vim:ts=2:sw=2:et:tw=78
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require File.expand_path('../test_parameters', __FILE__)
|
2
|
+
|
3
|
+
##
|
4
|
+
# CouchDB doesn't implement a commit/rollback scheme like other databases, but
|
5
|
+
# it would nevertheless still sometimes be nice to be able to isolate
|
6
|
+
# datastore changes made between tests.
|
7
|
+
#
|
8
|
+
# This module provides methods to drop and create databases, which provides a
|
9
|
+
# slow-yet-effective way of achieving said isolation. Examples:
|
10
|
+
#
|
11
|
+
# describe Something do
|
12
|
+
# let(:database_uri) { 'http://localhost:5984' }
|
13
|
+
# let(:database_name) { 'something' }
|
14
|
+
#
|
15
|
+
# before do
|
16
|
+
# clean_databases!
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# describe AnotherThing do
|
21
|
+
# let(:database_uri) { 'http://localhost:5984' }
|
22
|
+
# let(:database_names) { ['another', 'database'] }
|
23
|
+
#
|
24
|
+
# before do
|
25
|
+
# clean_databases!
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# If both database_name and database_names are specified in the same example
|
30
|
+
# group, the latter will have precedence.
|
31
|
+
#
|
32
|
+
#
|
33
|
+
# Clean databases come with a performance cost
|
34
|
+
# ============================================
|
35
|
+
#
|
36
|
+
# Dropping and creating databases has a real performance cost, so it is
|
37
|
+
# advised that you only use this when absolutely necessary. Databases are
|
38
|
+
# only cleaned when
|
39
|
+
#
|
40
|
+
# clean_databases!
|
41
|
+
#
|
42
|
+
# is included in an example group's before block.
|
43
|
+
module ExampleIsolation
|
44
|
+
def clean_databases!
|
45
|
+
drop_databases!
|
46
|
+
create_databases!
|
47
|
+
end
|
48
|
+
|
49
|
+
def drop_databases!
|
50
|
+
affected_databases.each do |db|
|
51
|
+
uri = instance_uri + "/#{db}"
|
52
|
+
credentials = admin_credentials
|
53
|
+
|
54
|
+
Net::HTTP.start(uri.host, uri.port) do |http|
|
55
|
+
req = Net::HTTP::Delete.new(uri.path)
|
56
|
+
req.basic_auth(admin_username, admin_password)
|
57
|
+
|
58
|
+
http.request(req)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def create_databases!
|
64
|
+
affected_databases.each do |db|
|
65
|
+
uri = instance_uri + "/#{db}"
|
66
|
+
credentials = admin_credentials
|
67
|
+
|
68
|
+
Net::HTTP.start(uri.host, uri.port) do |http|
|
69
|
+
req = Net::HTTP::Put.new(uri.path)
|
70
|
+
req.basic_auth(admin_username, admin_password)
|
71
|
+
|
72
|
+
http.request(req)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def affected_databases
|
78
|
+
if respond_to?(:database_name)
|
79
|
+
[database_name]
|
80
|
+
elsif respond_to?(:database_names)
|
81
|
+
database_names
|
82
|
+
end.flatten
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# vim:ts=2:sw=2:et:tw=78
|