analysand 1.1.0 → 2.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.
- data/CHANGELOG +8 -1
- data/Rakefile +14 -0
- data/lib/analysand/change_watcher.rb +1 -1
- data/lib/analysand/config_response.rb +24 -0
- data/lib/analysand/database.rb +13 -74
- data/lib/analysand/errors.rb +16 -0
- data/lib/analysand/http.rb +80 -0
- data/lib/analysand/instance.rb +128 -83
- data/lib/analysand/response.rb +8 -18
- data/lib/analysand/response_headers.rb +18 -0
- data/lib/analysand/session_response.rb +16 -0
- data/lib/analysand/status_code_predicates.rb +17 -0
- data/lib/analysand/streaming_view_response.rb +6 -16
- data/lib/analysand/version.rb +1 -1
- data/lib/analysand/viewing.rb +4 -4
- data/spec/analysand/a_response.rb +45 -1
- data/spec/analysand/a_session_grantor.rb +1 -1
- data/spec/analysand/database_spec.rb +14 -0
- data/spec/analysand/instance_spec.rb +104 -37
- data/spec/analysand/view_streaming_spec.rb +1 -1
- data/spec/fixtures/vcr_cassettes/get_config.yml +40 -0
- data/spec/fixtures/vcr_cassettes/get_many_config.yml +40 -0
- data/spec/fixtures/vcr_cassettes/reload_config.yml +114 -0
- data/spec/fixtures/vcr_cassettes/set_config.yml +77 -0
- data/spec/fixtures/vcr_cassettes/unauthorized_set_config.yml +43 -0
- data/spec/fixtures/vcr_cassettes/view.yml +1 -1
- data/spec/smoke/database_thread_spec.rb +59 -0
- metadata +215 -123
- data/spec/fixtures/vcr_cassettes/get_session_does_not_refresh_cookie.yml +0 -73
- data/spec/fixtures/vcr_cassettes/get_session_refreshes_cookie.yml +0 -75
data/lib/analysand/response.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'analysand/response_headers'
|
2
|
+
require 'analysand/status_code_predicates'
|
1
3
|
require 'forwardable'
|
2
4
|
require 'json/ext'
|
3
5
|
|
@@ -6,12 +8,14 @@ module Analysand
|
|
6
8
|
# The response object is a wrapper around Net::HTTPResponse that provides a
|
7
9
|
# few amenities:
|
8
10
|
#
|
9
|
-
# 1. A #success? method
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
11
|
+
# 1. A #success? method, which checks if 200 <= response code <= 299.
|
12
|
+
# 2. A #conflict method, which checks if response code == 409.
|
13
|
+
# 3. Automatic JSON deserialization of all response bodies.
|
14
|
+
# 4. Delegates the [] property accessor to the body.
|
13
15
|
class Response
|
14
16
|
extend Forwardable
|
17
|
+
include ResponseHeaders
|
18
|
+
include StatusCodePredicates
|
15
19
|
|
16
20
|
attr_reader :response
|
17
21
|
attr_reader :body
|
@@ -25,20 +29,6 @@ module Analysand
|
|
25
29
|
@body = JSON.parse(@response.body)
|
26
30
|
end
|
27
31
|
end
|
28
|
-
|
29
|
-
def etag
|
30
|
-
response.get_fields('ETag').first.gsub('"', '')
|
31
|
-
end
|
32
|
-
|
33
|
-
def success?
|
34
|
-
c = code.to_i
|
35
|
-
|
36
|
-
c >= 200 && c <= 299
|
37
|
-
end
|
38
|
-
|
39
|
-
def code
|
40
|
-
response.code
|
41
|
-
end
|
42
32
|
end
|
43
33
|
end
|
44
34
|
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Analysand
|
2
|
+
module ResponseHeaders
|
3
|
+
def etag
|
4
|
+
response.get_fields('ETag').first.gsub('"', '')
|
5
|
+
end
|
6
|
+
|
7
|
+
def cookies
|
8
|
+
response.get_fields('Set-Cookie')
|
9
|
+
end
|
10
|
+
|
11
|
+
def session_cookie
|
12
|
+
return unless (cs = cookies)
|
13
|
+
|
14
|
+
cs.detect { |c| c =~ /^(AuthSession=[^;]+)/i }
|
15
|
+
$1
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'analysand/response'
|
2
|
+
|
3
|
+
module Analysand
|
4
|
+
# Public: Wraps the response from GET /_session.
|
5
|
+
#
|
6
|
+
# GET /_session can be a bit surprising. A 200 OK response from _session
|
7
|
+
# indicates that the session cookie was well-formed; it doesn't indicate that
|
8
|
+
# the session is _valid_.
|
9
|
+
#
|
10
|
+
# Hence, this class adds a #valid? predicate.
|
11
|
+
class SessionResponse < Response
|
12
|
+
def valid?
|
13
|
+
(uc = body['userCtx']) && uc['name']
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'analysand/response_headers'
|
2
|
+
require 'analysand/status_code_predicates'
|
1
3
|
require 'analysand/view_streaming/builder'
|
2
4
|
require 'fiber'
|
3
5
|
|
@@ -16,12 +18,14 @@ module Analysand
|
|
16
18
|
# resp.rows.take(100) # => first 100 rows
|
17
19
|
class StreamingViewResponse
|
18
20
|
include Enumerable
|
21
|
+
include ResponseHeaders
|
22
|
+
include StatusCodePredicates
|
19
23
|
|
20
|
-
#
|
24
|
+
# Internal: The HTTP response.
|
21
25
|
#
|
22
26
|
# This is set by Analysand::Database#stream_view. The #etag and #code
|
23
27
|
# methods use this for header information.
|
24
|
-
attr_accessor :
|
28
|
+
attr_accessor :response
|
25
29
|
|
26
30
|
def initialize
|
27
31
|
@reader = Fiber.new { yield self; "" }
|
@@ -35,10 +39,6 @@ module Analysand
|
|
35
39
|
@reader.resume
|
36
40
|
end
|
37
41
|
|
38
|
-
def etag
|
39
|
-
http_response.get_fields('ETag').first.gsub('"', '')
|
40
|
-
end
|
41
|
-
|
42
42
|
# Public: Yields documents in the view stream.
|
43
43
|
#
|
44
44
|
# Note that #docs and #rows advance the same stream, so expect to miss half
|
@@ -55,16 +55,6 @@ module Analysand
|
|
55
55
|
each { |r| yield r['doc'] if r.has_key?('doc') }
|
56
56
|
end
|
57
57
|
|
58
|
-
def code
|
59
|
-
http_response.code
|
60
|
-
end
|
61
|
-
|
62
|
-
def success?
|
63
|
-
c = code.to_i
|
64
|
-
|
65
|
-
c >= 200 && c <= 299
|
66
|
-
end
|
67
|
-
|
68
58
|
def total_rows
|
69
59
|
read until @generator.total_rows
|
70
60
|
|
data/lib/analysand/version.rb
CHANGED
data/lib/analysand/viewing.rb
CHANGED
@@ -35,7 +35,7 @@ module Analysand
|
|
35
35
|
def stream_view(view_path, parameters, credentials)
|
36
36
|
StreamingViewResponse.new do |sresp|
|
37
37
|
do_view_query(view_path, parameters, credentials) do |resp|
|
38
|
-
sresp.
|
38
|
+
sresp.response = resp
|
39
39
|
Fiber.yield
|
40
40
|
resp.read_body { |data| Fiber.yield(data) }
|
41
41
|
end
|
@@ -82,11 +82,11 @@ module Analysand
|
|
82
82
|
end
|
83
83
|
|
84
84
|
def expand_view_path(view_name)
|
85
|
-
if view_name.include?('/')
|
85
|
+
if view_name.start_with?('_design') || !view_name.include?('/')
|
86
|
+
view_name
|
87
|
+
else
|
86
88
|
design_doc, view_name = view_name.split('/', 2)
|
87
89
|
"_design/#{design_doc}/_view/#{view_name}"
|
88
|
-
else
|
89
|
-
view_name
|
90
90
|
end
|
91
91
|
end
|
92
92
|
end
|
@@ -1,12 +1,34 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
shared_examples_for 'a response' do
|
4
|
-
%w(etag success? code).each do |m|
|
4
|
+
%w(etag success? conflict? code cookies session_cookie).each do |m|
|
5
5
|
it "responds to ##{m}" do
|
6
6
|
response.should respond_to(m)
|
7
7
|
end
|
8
8
|
end
|
9
9
|
|
10
|
+
describe '#conflict?' do
|
11
|
+
describe 'if response code is 409' do
|
12
|
+
before do
|
13
|
+
response.stub!(:code => '409')
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'returns true' do
|
17
|
+
response.should be_conflict
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe 'if response code is 200' do
|
22
|
+
before do
|
23
|
+
response.stub!(:code => '200')
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'returns false' do
|
27
|
+
response.should_not be_conflict
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
10
32
|
describe '#etag' do
|
11
33
|
it 'returns a string' do
|
12
34
|
response.etag.should be_instance_of(String)
|
@@ -16,4 +38,26 @@ shared_examples_for 'a response' do
|
|
16
38
|
response.etag.should_not include('"')
|
17
39
|
end
|
18
40
|
end
|
41
|
+
|
42
|
+
describe '#session_cookie' do
|
43
|
+
describe 'with an AuthSession cookie' do
|
44
|
+
let(:cookie) do
|
45
|
+
'AuthSession=foobar; Version=1; Expires=Wed, 14 Nov 2012 16:32:04 GMT; Max-Age=600; Path=/; HttpOnly'
|
46
|
+
end
|
47
|
+
|
48
|
+
before do
|
49
|
+
response.stub!(:cookies => [cookie])
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'returns the AuthSession cookie' do
|
53
|
+
response.session_cookie.should == 'AuthSession=foobar'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe 'without an AuthSession cookie' do
|
58
|
+
it 'returns nil' do
|
59
|
+
response.session_cookie.should be_nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
19
63
|
end
|
@@ -13,6 +13,14 @@ module Analysand
|
|
13
13
|
it 'requires an absolute URI' do
|
14
14
|
lambda { Database.new(URI("/abc")) }.should raise_error(InvalidURIError)
|
15
15
|
end
|
16
|
+
|
17
|
+
it 'accepts URIs as strings' do
|
18
|
+
uri = 'http://localhost:5984/foo/'
|
19
|
+
|
20
|
+
db = Database.new(uri)
|
21
|
+
|
22
|
+
db.uri.should == URI(uri)
|
23
|
+
end
|
16
24
|
end
|
17
25
|
|
18
26
|
describe '.create!' do
|
@@ -150,6 +158,12 @@ module Analysand
|
|
150
158
|
resp.rows.should == resp.body['rows']
|
151
159
|
end
|
152
160
|
|
161
|
+
it 'works with the full document name' do
|
162
|
+
resp = db.view('_design/doc/_view/a_view')
|
163
|
+
|
164
|
+
resp.code.should == '200'
|
165
|
+
end
|
166
|
+
|
153
167
|
it 'passes through view parameters' do
|
154
168
|
resp = db.view('doc/a_view', :skip => 1)
|
155
169
|
|
@@ -13,72 +13,139 @@ module Analysand
|
|
13
13
|
it 'requires an absolute URI' do
|
14
14
|
lambda { Instance.new(URI("/abc")) }.should raise_error(InvalidURIError)
|
15
15
|
end
|
16
|
+
|
17
|
+
it 'accepts URIs as strings' do
|
18
|
+
uri = 'http://localhost:5984/'
|
19
|
+
|
20
|
+
db = Instance.new(uri)
|
21
|
+
|
22
|
+
db.uri.should == URI(uri)
|
23
|
+
end
|
16
24
|
end
|
17
25
|
|
18
26
|
let(:instance) { Instance.new(instance_uri) }
|
19
27
|
|
20
|
-
describe '#
|
21
|
-
describe '
|
22
|
-
let(:
|
28
|
+
describe '#post_session' do
|
29
|
+
describe 'with valid credentials' do
|
30
|
+
let(:resp) { instance.post_session(member1_username, member1_password) }
|
31
|
+
|
32
|
+
it 'returns success' do
|
33
|
+
resp.should be_success
|
34
|
+
end
|
23
35
|
|
24
|
-
|
36
|
+
it 'returns a session cookie in the response' do
|
37
|
+
resp.session_cookie.should_not be_empty
|
38
|
+
end
|
25
39
|
end
|
26
40
|
|
27
|
-
|
28
|
-
|
41
|
+
it 'supports hash credentials' do
|
42
|
+
resp = instance.post_session(member1_credentials)
|
29
43
|
|
30
|
-
|
44
|
+
resp.should be_success
|
31
45
|
end
|
32
46
|
|
33
|
-
describe '
|
34
|
-
|
35
|
-
session, resp = instance.establish_session('wrong', 'wrong')
|
47
|
+
describe 'with invalid credentials' do
|
48
|
+
let(:resp) { instance.post_session(member1_username, 'wrong') }
|
36
49
|
|
37
|
-
|
38
|
-
resp.
|
50
|
+
it 'does not return success' do
|
51
|
+
resp.should_not be_success
|
39
52
|
end
|
40
53
|
end
|
41
54
|
end
|
42
55
|
|
43
|
-
describe '#
|
44
|
-
|
56
|
+
describe '#test_session' do
|
57
|
+
describe 'with a valid cookie' do
|
58
|
+
let(:resp) { instance.post_session(member1_username, member1_password) }
|
59
|
+
let(:cookie) { resp.session_cookie }
|
45
60
|
|
46
|
-
|
47
|
-
|
61
|
+
it 'returns success' do
|
62
|
+
resp = instance.get_session(cookie)
|
63
|
+
|
64
|
+
resp.should be_success
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'returns valid' do
|
68
|
+
resp = instance.get_session(cookie)
|
69
|
+
|
70
|
+
resp.should be_valid
|
71
|
+
end
|
48
72
|
end
|
49
73
|
|
50
|
-
describe '
|
51
|
-
|
52
|
-
|
74
|
+
describe 'with an invalid cookie' do
|
75
|
+
let(:cookie) { 'AuthSession=YWRtaW46NTBBNDkwRUE6npTfHKz68y5q1FX4pWiB-Lzk5mQ' }
|
76
|
+
|
77
|
+
it 'returns success' do
|
78
|
+
resp = instance.get_session(cookie)
|
79
|
+
|
80
|
+
resp.should be_success
|
53
81
|
end
|
54
82
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
end
|
83
|
+
it 'does not return valid' do
|
84
|
+
resp = instance.get_session(cookie)
|
85
|
+
|
86
|
+
resp.should_not be_valid
|
60
87
|
end
|
61
88
|
end
|
62
89
|
|
63
|
-
describe '
|
64
|
-
|
65
|
-
|
90
|
+
describe 'with a malformed cookie' do
|
91
|
+
let(:cookie) { 'AuthSession=wrong' }
|
92
|
+
|
93
|
+
it 'does not return success' do
|
94
|
+
resp = instance.get_session(cookie)
|
95
|
+
|
96
|
+
resp.should_not be_success
|
66
97
|
end
|
67
98
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
end
|
99
|
+
it 'does not return valid' do
|
100
|
+
resp = instance.get_session(cookie)
|
101
|
+
|
102
|
+
resp.should_not be_valid
|
73
103
|
end
|
74
104
|
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe '#get_config' do
|
108
|
+
let(:credentials) { admin_credentials }
|
109
|
+
|
110
|
+
it 'retrieves a configuration option' do
|
111
|
+
VCR.use_cassette('get_config') do
|
112
|
+
instance.get_config('stats/rate', admin_credentials).value.should == '"1000"'
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'retrieves many configuration options' do
|
117
|
+
VCR.use_cassette('get_many_config') do
|
118
|
+
instance.get_config('stats', admin_credentials).value.should == {
|
119
|
+
'rate' => '1000',
|
120
|
+
'samples' => '[0, 60, 300, 900]'
|
121
|
+
}
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe '#set_config!' do
|
127
|
+
it 'raises ConfigurationNotSaved on non-success' do
|
128
|
+
VCR.use_cassette('unauthorized_set_config') do
|
129
|
+
lambda { instance.set_config!('stats/rate', 1000) }.should raise_error(ConfigurationNotSaved)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
75
133
|
|
76
|
-
|
77
|
-
|
78
|
-
|
134
|
+
describe '#set_config' do
|
135
|
+
let(:credentials) { admin_credentials }
|
136
|
+
|
137
|
+
it 'sets a configuration option' do
|
138
|
+
VCR.use_cassette('set_config') do
|
139
|
+
instance.set_config('stats/rate', 1200, admin_credentials)
|
140
|
+
instance.get_config('stats/rate', admin_credentials).value.should == '"1200"'
|
141
|
+
end
|
142
|
+
end
|
79
143
|
|
80
|
-
|
81
|
-
|
144
|
+
it 'accepts values from get_config' do
|
145
|
+
VCR.use_cassette('reload_config') do
|
146
|
+
samples = instance.get_config('stats/samples', admin_credentials).value
|
147
|
+
instance.set_config('stats/samples', samples, admin_credentials)
|
148
|
+
instance.get_config('stats/samples', admin_credentials).value.should == samples
|
82
149
|
end
|
83
150
|
end
|
84
151
|
end
|