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.
@@ -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. It returns true if the response code is between
10
- # (200..299) and false otherwise.
11
- # 2. Automatic JSON deserialization of all response bodies.
12
- # 3. Delegates the [] property accessor to the body.
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
@@ -0,0 +1,17 @@
1
+ module Analysand
2
+ module StatusCodePredicates
3
+ def code
4
+ response.code
5
+ end
6
+
7
+ def success?
8
+ c = code.to_i
9
+
10
+ c >= 200 && c <= 299
11
+ end
12
+
13
+ def conflict?
14
+ code.to_i == 409
15
+ end
16
+ end
17
+ 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
- # Private: The HTTP response.
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 :http_response
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
 
@@ -1,3 +1,3 @@
1
1
  module Analysand
2
- VERSION = "1.1.0"
2
+ VERSION = "2.0.0"
3
3
  end
@@ -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.http_response = resp
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
@@ -30,7 +30,7 @@ shared_examples_for 'a session grantor' do
30
30
  end
31
31
 
32
32
  it 'has the user roles' do
33
- @session[:roles].should == role_locator[JSON.parse(@resp.body)]
33
+ @session[:roles].should == role_locator[@resp.body]
34
34
  end
35
35
 
36
36
  it 'has a session token' do
@@ -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 '#establish_session' do
21
- describe 'given admin credentials' do
22
- let(:credentials) { admin_credentials }
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
- it_should_behave_like 'a session grantor'
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
- describe 'given member credentials' do
28
- let(:credentials) { member1_credentials }
41
+ it 'supports hash credentials' do
42
+ resp = instance.post_session(member1_credentials)
29
43
 
30
- it_should_behave_like 'a session grantor'
44
+ resp.should be_success
31
45
  end
32
46
 
33
- describe 'given incorrect credentials' do
34
- it 'returns [nil, response]' do
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
- session.should be_nil
38
- resp.code.should == '401'
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 '#renew_session' do
44
- let(:credentials) { admin_credentials }
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
- before do
47
- @session, _ = instance.establish_session(credentials[:username], credentials[:password])
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 'if CouchDB refreshes the session cookie' do
51
- around do |example|
52
- VCR.use_cassette('get_session_refreshes_cookie') { example.call }
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
- 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
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 '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 }
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
- 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
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
- describe 'given an invalid session' do
77
- it 'returns [nil, response]' do
78
- session, resp = instance.renew_session({ :token => 'AuthSession=wrong' })
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
- session.should be_nil
81
- resp.code.should == '400'
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