dpla-analysand 3.0.2

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.
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