dpla-analysand 3.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +8 -0
  5. data/CHANGELOG +67 -0
  6. data/Gemfile +8 -0
  7. data/LICENSE +22 -0
  8. data/README +48 -0
  9. data/Rakefile +22 -0
  10. data/analysand.gemspec +33 -0
  11. data/bin/analysand +27 -0
  12. data/lib/analysand.rb +3 -0
  13. data/lib/analysand/bulk_response.rb +14 -0
  14. data/lib/analysand/change_watcher.rb +280 -0
  15. data/lib/analysand/config_response.rb +25 -0
  16. data/lib/analysand/connection_testing.rb +52 -0
  17. data/lib/analysand/database.rb +322 -0
  18. data/lib/analysand/errors.rb +60 -0
  19. data/lib/analysand/http.rb +90 -0
  20. data/lib/analysand/instance.rb +255 -0
  21. data/lib/analysand/reading.rb +26 -0
  22. data/lib/analysand/response.rb +35 -0
  23. data/lib/analysand/response_headers.rb +18 -0
  24. data/lib/analysand/session_response.rb +16 -0
  25. data/lib/analysand/status_code_predicates.rb +25 -0
  26. data/lib/analysand/streaming_view_response.rb +90 -0
  27. data/lib/analysand/version.rb +3 -0
  28. data/lib/analysand/view_response.rb +24 -0
  29. data/lib/analysand/view_streaming/builder.rb +142 -0
  30. data/lib/analysand/viewing.rb +95 -0
  31. data/lib/analysand/writing.rb +71 -0
  32. data/script/setup_database.rb +45 -0
  33. data/spec/analysand/a_response.rb +70 -0
  34. data/spec/analysand/change_watcher_spec.rb +102 -0
  35. data/spec/analysand/database_spec.rb +243 -0
  36. data/spec/analysand/database_writing_spec.rb +488 -0
  37. data/spec/analysand/instance_spec.rb +205 -0
  38. data/spec/analysand/response_spec.rb +26 -0
  39. data/spec/analysand/view_response_spec.rb +44 -0
  40. data/spec/analysand/view_streaming/builder_spec.rb +73 -0
  41. data/spec/analysand/view_streaming_spec.rb +122 -0
  42. data/spec/fixtures/vcr_cassettes/get_config.yml +40 -0
  43. data/spec/fixtures/vcr_cassettes/get_many_config.yml +40 -0
  44. data/spec/fixtures/vcr_cassettes/head_request_with_etag.yml +40 -0
  45. data/spec/fixtures/vcr_cassettes/reload_config.yml +114 -0
  46. data/spec/fixtures/vcr_cassettes/unauthorized_put_config.yml +43 -0
  47. data/spec/fixtures/vcr_cassettes/view.yml +40 -0
  48. data/spec/smoke/database_thread_spec.rb +59 -0
  49. data/spec/spec_helper.rb +30 -0
  50. data/spec/support/database_access.rb +40 -0
  51. data/spec/support/example_isolation.rb +86 -0
  52. data/spec/support/test_parameters.rb +39 -0
  53. metadata +283 -0
@@ -0,0 +1,205 @@
1
+ require 'spec_helper'
2
+
3
+ require 'analysand/errors'
4
+ require 'analysand/instance'
5
+ require 'json'
6
+ require 'uri'
7
+ require 'vcr'
8
+
9
+ module Analysand
10
+ describe Instance do
11
+ describe '#initialize' do
12
+ it 'requires an absolute URI' do
13
+ lambda { Instance.new(URI("/abc")) }.should raise_error(InvalidURIError)
14
+ end
15
+
16
+ it 'accepts URIs as strings' do
17
+ uri = 'http://localhost:5984/'
18
+
19
+ db = Instance.new(uri)
20
+
21
+ db.uri.should == URI(uri)
22
+ end
23
+ end
24
+
25
+ let(:instance) { Instance.new(instance_uri) }
26
+
27
+ describe '#post_session' do
28
+ describe 'with valid credentials' do
29
+ let(:resp) { instance.post_session(member1_username, member1_password) }
30
+
31
+ it 'returns success' do
32
+ resp.should be_success
33
+ end
34
+
35
+ it 'returns a session cookie in the response' do
36
+ resp.session_cookie.should_not be_empty
37
+ end
38
+ end
39
+
40
+ it 'supports hash credentials' do
41
+ resp = instance.post_session(member1_credentials)
42
+
43
+ resp.should be_success
44
+ end
45
+
46
+ describe 'with invalid credentials' do
47
+ let(:resp) { instance.post_session(member1_username, 'wrong') }
48
+
49
+ it 'does not return success' do
50
+ resp.should_not be_success
51
+ end
52
+ end
53
+ end
54
+
55
+ describe '#test_session' do
56
+ describe 'with a valid cookie' do
57
+ let(:resp) { instance.post_session(member1_username, member1_password) }
58
+ let(:cookie) { resp.session_cookie }
59
+
60
+ it 'returns success' do
61
+ resp = instance.get_session(cookie)
62
+
63
+ resp.should be_success
64
+ end
65
+
66
+ it 'returns valid' do
67
+ resp = instance.get_session(cookie)
68
+
69
+ resp.should be_valid
70
+ end
71
+ end
72
+
73
+ describe 'with an invalid cookie' do
74
+ let(:cookie) { 'AuthSession=YWRtaW46NTBBNDkwRUE6npTfHKz68y5q1FX4pWiB-Lzk5mQ' }
75
+
76
+ it 'returns success' do
77
+ resp = instance.get_session(cookie)
78
+
79
+ resp.should be_success
80
+ end
81
+
82
+ it 'does not return valid' do
83
+ resp = instance.get_session(cookie)
84
+
85
+ resp.should_not be_valid
86
+ end
87
+ end
88
+
89
+ describe 'with a malformed cookie' do
90
+ let(:cookie) { 'AuthSession=wrong' }
91
+
92
+ it 'does not return success' do
93
+ resp = instance.get_session(cookie)
94
+
95
+ resp.should_not be_success
96
+ end
97
+
98
+ it 'does not return valid' do
99
+ resp = instance.get_session(cookie)
100
+
101
+ resp.should_not be_valid
102
+ end
103
+ end
104
+ end
105
+
106
+ describe '#get_config' do
107
+ let(:credentials) { admin_credentials }
108
+
109
+ it 'retrieves a configuration option' do
110
+ VCR.use_cassette('get_config') do
111
+ instance.get_config('stats/rate', admin_credentials).value.should == '"1000"'
112
+ end
113
+ end
114
+
115
+ it 'retrieves many configuration options' do
116
+ VCR.use_cassette('get_many_config') do
117
+ json = instance.get_config('stats', admin_credentials).value
118
+
119
+ JSON.parse(json).should == {
120
+ 'rate' => '1000',
121
+ 'samples' => '[0, 60, 300, 900]'
122
+ }
123
+ end
124
+ end
125
+ end
126
+
127
+ describe '#put_config!' do
128
+ it 'raises ConfigurationNotSaved on non-success' do
129
+ VCR.use_cassette('unauthorized_put_config') do
130
+ lambda { instance.put_config!('stats/rate', '"1000"') }.should raise_error(ConfigurationNotSaved)
131
+ end
132
+ end
133
+ end
134
+
135
+ describe '#put_config' do
136
+ let(:credentials) { admin_credentials }
137
+
138
+ after do
139
+ instance.delete_config!('foo/bar', admin_credentials)
140
+ end
141
+
142
+ it 'sets a configuration option' do
143
+ instance.put_config('foo/bar', '"1200"', admin_credentials)
144
+ instance.get_config('foo/bar', admin_credentials).value.should == '"1200"'
145
+ end
146
+
147
+ it 'roundtrips values from get_config' do
148
+ instance.put_config!('foo/bar', '"[0, 60, 300, 900]"', admin_credentials)
149
+
150
+ from_db = instance.get_config('foo/bar', admin_credentials).value
151
+ instance.put_config('foo/bar', from_db, admin_credentials)
152
+ instance.get_config('foo/bar', admin_credentials).value.should == from_db
153
+ end
154
+ end
155
+
156
+ describe '#delete_config' do
157
+ let(:credentials) { admin_credentials }
158
+
159
+ before do
160
+ instance.put_config!('foo/bar', '"1000"', admin_credentials)
161
+ end
162
+
163
+ it 'deletes configuration options' do
164
+ instance.delete_config('foo/bar', admin_credentials)
165
+
166
+ resp = instance.get_config('foo/bar', admin_credentials)
167
+ expect(resp.not_found?).to eq(true)
168
+ end
169
+ end
170
+
171
+ describe '#put_admin' do
172
+ let(:credentials) { admin_credentials }
173
+ let(:password) { 'password' }
174
+ let(:username) { 'new_admin' }
175
+
176
+ after do
177
+ instance.delete_admin!(username, credentials)
178
+ end
179
+
180
+ it 'adds an admin to the CouchDB admins list' do
181
+ instance.put_admin(username, password, admin_credentials)
182
+
183
+ resp = instance.post_session(username, password)
184
+ resp.body['roles'].should include('_admin')
185
+ end
186
+ end
187
+
188
+ describe '#delete_admin' do
189
+ let(:credentials) { admin_credentials }
190
+ let(:password) { 'password' }
191
+ let(:username) { 'new_admin' }
192
+
193
+ before do
194
+ instance.put_admin!(username, password, admin_credentials)
195
+ end
196
+
197
+ it 'deletes an admin from the CouchDB admins list' do
198
+ instance.delete_admin(username, admin_credentials)
199
+
200
+ resp = instance.post_session(username, password)
201
+ resp.should be_unauthorized
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ require 'analysand/database'
4
+ require 'analysand/response'
5
+
6
+ require File.expand_path('../a_response', __FILE__)
7
+
8
+ module Analysand
9
+ describe Response do
10
+ let(:db) { Database.new(database_uri) }
11
+
12
+ let(:response) do
13
+ VCR.use_cassette('head_request_with_etag') do
14
+ db.head('abc123', admin_credentials)
15
+ end
16
+ end
17
+
18
+ it_should_behave_like 'a response'
19
+
20
+ describe '#etag' do
21
+ it 'removes quotes from ETags' do
22
+ response.etag.should == '1-967a00dff5e02add41819138abb3284d'
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ require 'analysand/database'
4
+ require 'analysand/view_response'
5
+
6
+ require File.expand_path('../a_response', __FILE__)
7
+
8
+ module Analysand
9
+ describe ViewResponse do
10
+ let(:resp_with_docs) do
11
+ '{"rows": [{"id":"foo","key":"foo","value":{},"doc":{"foo":"bar"}}]}'
12
+ end
13
+
14
+ let(:resp_without_docs) do
15
+ '{"rows": [{"id":"foo","key":"foo","value":{}}]}'
16
+ end
17
+
18
+ let(:db) { Database.new(database_uri) }
19
+
20
+ it_should_behave_like 'a response' do
21
+ let(:response) do
22
+ VCR.use_cassette('view') { db.view('doc/a_view') }
23
+ end
24
+ end
25
+
26
+ describe '#docs' do
27
+ describe 'if the view includes docs' do
28
+ subject { ViewResponse.new(double(:body => resp_with_docs)) }
29
+
30
+ it 'returns the value of the "doc" key in each row' do
31
+ subject.docs.should == [{'foo' => 'bar'}]
32
+ end
33
+ end
34
+
35
+ describe 'if the view does not include docs' do
36
+ subject { ViewResponse.new(double(:body => resp_without_docs)) }
37
+
38
+ it 'returns []' do
39
+ subject.docs.should == []
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,73 @@
1
+ require 'spec_helper'
2
+
3
+ require 'analysand/view_streaming/builder'
4
+
5
+ module Analysand
6
+ module ViewStreaming
7
+ describe Builder do
8
+ let(:builder) { Builder.new }
9
+
10
+ it 'recognizes the "total_rows" key' do
11
+ builder << '{"total_rows":10000}'
12
+
13
+ builder.total_rows.should == 10000
14
+ end
15
+
16
+ it 'recognizes the "offset" key' do
17
+ builder << '{"offset":20}'
18
+
19
+ builder.offset.should == 20
20
+ end
21
+
22
+ describe 'for rows' do
23
+ before do
24
+ builder << '{"total_rows":10000,"offset":20,"rows":'
25
+ end
26
+
27
+ it 'builds objects' do
28
+ builder << '[{"id":"foo","key":"bar","value":1}]'
29
+
30
+ builder.staged_rows.should == [
31
+ { 'id' => 'foo', 'key' => 'bar', 'value' => 1 }
32
+ ]
33
+ end
34
+
35
+ it 'builds objects containing other objects' do
36
+ builder << '[{"id":"foo","key":"bar","value":{"total_rows":1}}]'
37
+
38
+ builder.staged_rows.should == [
39
+ { 'id' => 'foo',
40
+ 'key' => 'bar',
41
+ 'value' => {
42
+ 'total_rows' => 1
43
+ }
44
+ }
45
+ ]
46
+ end
47
+
48
+ it 'builds objects containing arrays' do
49
+ builder << '[{"id":"foo","key":"bar","value":{"things":[1,2,3]}}]'
50
+
51
+ builder.staged_rows.should == [
52
+ { 'id' => 'foo',
53
+ 'key' => 'bar',
54
+ 'value' => {
55
+ 'things' => [1, 2, 3]
56
+ }
57
+ }
58
+ ]
59
+ end
60
+ end
61
+
62
+ describe 'given unexpected top-level keys' do
63
+ before do
64
+ builder << '{"total_rows":10000,"offset":20,"rows":[],'
65
+ end
66
+
67
+ it 'raises Analysand::UnexpectedViewKey' do
68
+ lambda { builder << '"abc":"def"}' }.should raise_error(Analysand::UnexpectedViewKey)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,122 @@
1
+ require 'spec_helper'
2
+
3
+ require 'analysand/database'
4
+ require 'benchmark'
5
+
6
+ require File.expand_path('../a_response', __FILE__)
7
+
8
+ module Analysand
9
+ describe Database do
10
+ let(:db) { Database.new(database_uri) }
11
+ let(:row_count) { 15000 }
12
+
13
+ before do
14
+ WebMock.disable!
15
+ end
16
+
17
+ after do
18
+ WebMock.enable!
19
+ end
20
+
21
+ before do
22
+ clean_databases!
23
+
24
+ doc = {
25
+ 'views' => {
26
+ 'a_view' => {
27
+ 'map' => %Q{
28
+ function (doc) {
29
+ var i;
30
+
31
+ for(i = 0; i < #{row_count}; i++) {
32
+ emit(doc['_id'], i);
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
39
+
40
+ db.put!('_design/doc', doc, admin_credentials)
41
+ db.put!('abc123', {}, admin_credentials)
42
+ end
43
+
44
+ shared_examples_for 'a view streamer' do
45
+ def get_view(options = {})
46
+ db.send(method, 'doc/a_view', options)
47
+ end
48
+
49
+ describe 'response' do
50
+ it_should_behave_like 'a response' do
51
+ let(:response) { get_view(:stream => true) }
52
+ end
53
+ end
54
+
55
+ it 'returns all rows in order' do
56
+ resp = get_view(:stream => true)
57
+
58
+ resp.rows.map { |r| r['value'] }.should == (0...row_count).to_a
59
+ end
60
+
61
+ it 'yields docs' do
62
+ resp = get_view(:include_docs => true, :stream => true)
63
+
64
+ expect(resp.docs.take(10).all? { |d| d.has_key?('_id') }).to eq(true)
65
+ end
66
+
67
+ it 'returns rows as soon as possible' do
68
+ # first, make sure the view's built
69
+ db.head('_design/doc/_view/a_view', admin_credentials)
70
+
71
+ streamed = Benchmark.realtime do
72
+ resp = get_view(:stream => true)
73
+ resp.rows.take(10)
74
+ end
75
+
76
+ read_everything = Benchmark.realtime do
77
+ resp = get_view
78
+ resp.rows.take(10)
79
+ end
80
+
81
+ streamed.should_not be_within(0.5).of(read_everything)
82
+ end
83
+
84
+ it 'returns view metadata' do
85
+ resp = get_view(:stream => true)
86
+
87
+ resp.total_rows.should == row_count
88
+ resp.offset.should == 0
89
+ end
90
+
91
+ describe '#each' do
92
+ it 'returns an Enumerator if no block is given' do
93
+ resp = get_view(:stream => true)
94
+
95
+ resp.rows.each.should be_instance_of(Enumerator)
96
+ end
97
+ end
98
+ end
99
+
100
+ describe '#view in streaming mode' do
101
+ it_should_behave_like 'a view streamer' do
102
+ let(:method) { :view }
103
+ end
104
+
105
+ it 'returns error codes from failures' do
106
+ resp = db.view('doc/nonexistent', :stream => true)
107
+
108
+ resp.code.should == '404'
109
+ end
110
+ end
111
+
112
+ describe '#view! in streaming mode' do
113
+ it_should_behave_like 'a view streamer' do
114
+ let(:method) { :view! }
115
+ end
116
+
117
+ it 'raises CannotAccessView on failure' do
118
+ lambda { db.view!('doc/nonexistent', :stream => true) }.should raise_error(CannotAccessView)
119
+ end
120
+ end
121
+ end
122
+ end