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,70 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples_for 'a response' do
4
+ %w(success? conflict? unauthorized?
5
+ etag code cookies session_cookie).each do |m|
6
+ it "responds to ##{m}" do
7
+ response.should respond_to(m)
8
+ end
9
+ end
10
+
11
+ describe '#conflict?' do
12
+ it 'returns true if response code is 409' do
13
+ response.stub(:code => '409')
14
+
15
+ expect(response.conflict?).to eq(true)
16
+ end
17
+
18
+ it 'returns false if response code is 200' do
19
+ response.stub(:code => '200')
20
+
21
+ expect(response.conflict?).to eq(false)
22
+ end
23
+ end
24
+
25
+ describe '#unauthorized?' do
26
+ it 'returns true if response code is 401' do
27
+ response.stub(:code => '401')
28
+
29
+ response.should be_unauthorized
30
+ end
31
+
32
+ it 'returns false if response code is 200' do
33
+ response.stub(:code => '200')
34
+
35
+ response.should_not be_unauthorized
36
+ end
37
+ end
38
+
39
+ describe '#etag' do
40
+ it 'returns a string' do
41
+ response.etag.should be_instance_of(String)
42
+ end
43
+
44
+ it 'returns ETags without quotes' do
45
+ response.etag.should_not include('"')
46
+ end
47
+ end
48
+
49
+ describe '#session_cookie' do
50
+ describe 'with an AuthSession cookie' do
51
+ let(:cookie) do
52
+ 'AuthSession=foobar; Version=1; Expires=Wed, 14 Nov 2012 16:32:04 GMT; Max-Age=600; Path=/; HttpOnly'
53
+ end
54
+
55
+ before do
56
+ response.stub(:cookies => [cookie])
57
+ end
58
+
59
+ it 'returns the AuthSession cookie' do
60
+ response.session_cookie.should == 'AuthSession=foobar'
61
+ end
62
+ end
63
+
64
+ describe 'without an AuthSession cookie' do
65
+ it 'returns nil' do
66
+ response.session_cookie.should be_nil
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,102 @@
1
+ require 'spec_helper'
2
+
3
+ require 'analysand/change_watcher'
4
+ require 'analysand/database'
5
+ require 'celluloid'
6
+
7
+ module Analysand
8
+ describe ChangeWatcher do
9
+ class TestWatcher < Analysand::ChangeWatcher
10
+ attr_accessor :changes
11
+
12
+ def initialize(database, credentials)
13
+ super(database)
14
+
15
+ self.changes = []
16
+
17
+ @credentials = credentials
18
+ end
19
+
20
+ def customize_request(req)
21
+ req.basic_auth(@credentials[:username], @credentials[:password])
22
+ end
23
+
24
+ def process(change)
25
+ changes << change
26
+ change_processed(change)
27
+ end
28
+ end
29
+
30
+ let(:db) { Database.new(database_uri) }
31
+
32
+ before do
33
+ create_databases!
34
+ end
35
+
36
+ after do
37
+ Celluloid.shutdown
38
+
39
+ drop_databases!
40
+ end
41
+
42
+ describe '#connection_ok' do
43
+ describe 'with a non-public change feed' do
44
+ before do
45
+ set_security({ 'names' => [admin_username] })
46
+ end
47
+
48
+ after do
49
+ clear_security
50
+ end
51
+
52
+ it 'passes credentials' do
53
+ watcher = TestWatcher.new(db, admin_credentials)
54
+
55
+ expect(watcher.connection_ok).to eq(true)
56
+ end
57
+ end
58
+ end
59
+
60
+ describe '#changes_feed_uri' do
61
+ let!(:watcher) { TestWatcher.new(db, admin_credentials) }
62
+
63
+ describe 'when invoked multiple times' do
64
+ it 'returns what it returned the first time' do
65
+ uri1 = watcher.changes_feed_uri
66
+ uri2 = watcher.changes_feed_uri
67
+
68
+ uri1.should == uri2
69
+ end
70
+ end
71
+ end
72
+
73
+ describe '#waiter_for' do
74
+ let!(:watcher) { TestWatcher.new(db, admin_credentials) }
75
+
76
+ describe 'if the given document has not been processed' do
77
+ it 'blocks until the document has been processed' do
78
+ waiter = watcher.waiter_for('bar')
79
+
80
+ Thread.new do
81
+ db.put('foo', { 'foo' => 'bar' }, admin_credentials)
82
+ db.put('bar', { 'foo' => 'bar' }, admin_credentials)
83
+ end
84
+
85
+ waiter.wait
86
+
87
+ watcher.changes.detect { |r| r['id'] == 'foo' }.should_not be_nil
88
+ end
89
+ end
90
+ end
91
+
92
+ it 'receives changes' do
93
+ watcher = TestWatcher.new(db, admin_credentials)
94
+
95
+ waiter = watcher.waiter_for('foo')
96
+ db.put('foo', { 'foo' => 'bar' }, admin_credentials)
97
+ waiter.wait
98
+
99
+ expect(watcher.changes.select { |r| r['id'] == 'foo' }.length).to eq(1)
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,243 @@
1
+ require 'spec_helper'
2
+
3
+ require 'analysand/database'
4
+ require 'analysand/errors'
5
+ require 'thread'
6
+ require 'uri'
7
+
8
+ module Analysand
9
+ describe Database do
10
+ let(:db) { Database.new(database_uri) }
11
+
12
+ describe '#initialize' do
13
+ it 'requires an absolute URI' do
14
+ lambda { Database.new(URI("/abc")) }.should raise_error(InvalidURIError)
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
24
+ end
25
+
26
+ describe '.create!' do
27
+ before do
28
+ drop_databases!
29
+ end
30
+
31
+ it 'creates the database at the given URI' do
32
+ db = Database.create!(database_uri, admin_credentials)
33
+
34
+ resp = db.ping
35
+ resp.body['db_name'].should == database_name
36
+ end
37
+
38
+ it 'raises an exception if the database cannot be created' do
39
+ Database.create!(database_uri, admin_credentials)
40
+
41
+ lambda { Database.create!(database_uri, admin_credentials) }.should raise_error(DatabaseError)
42
+ end
43
+ end
44
+
45
+ describe '.drop' do
46
+ before do
47
+ create_databases!
48
+ end
49
+
50
+ it 'drops the database at the given URI' do
51
+ resp = Database.drop(database_uri, admin_credentials)
52
+
53
+ resp.should be_success
54
+ Database.new(database_uri).ping.code.should == '404'
55
+ end
56
+ end
57
+
58
+ describe '#head' do
59
+ before do
60
+ clean_databases!
61
+
62
+ db.put!('foo', { 'foo' => 'bar' })
63
+ end
64
+
65
+ it 'retrieves the rev of a document' do
66
+ resp = db.head('foo')
67
+
68
+ resp.etag.should_not be_empty
69
+ end
70
+ end
71
+
72
+ describe '#get!' do
73
+ before do
74
+ clean_databases!
75
+
76
+ db.put!('foo', { 'foo' => 'bar' })
77
+ end
78
+
79
+ describe 'if the response code is 200' do
80
+ it 'returns the document' do
81
+ db.get!('foo').body['foo'].should == 'bar'
82
+ end
83
+ end
84
+
85
+ describe 'if the response code is 404' do
86
+ it 'raises Analysand::CannotAccessDocument' do
87
+ lambda { db.get!('bar') }.should raise_error(Analysand::CannotAccessDocument)
88
+ end
89
+
90
+ it 'includes the response in the exception' do
91
+ code = nil
92
+
93
+ begin
94
+ db.get!('bar')
95
+ rescue Analysand::CannotAccessDocument => e
96
+ code = e.response.code
97
+ end
98
+
99
+ code.should == '404'
100
+ end
101
+ end
102
+ end
103
+
104
+ describe '#get_attachment' do
105
+ let(:io) { StringIO.new('an attachment') }
106
+
107
+ before do
108
+ clean_databases!
109
+
110
+ db.put_attachment('doc_id/a', io, {}, admin_credentials)
111
+ end
112
+
113
+ xit 'permits streaming' do
114
+ resp = db.get_attachment('doc_id/a')
115
+
116
+ lambda { resp.read_body { } }.should_not raise_error
117
+ end
118
+ end
119
+
120
+ describe '#view' do
121
+ before do
122
+ clean_databases!
123
+
124
+ doc = {
125
+ 'views' => {
126
+ 'a_view' => {
127
+ 'map' => %q{function (doc) { emit(doc['_id'], 1); }}
128
+ },
129
+ 'composite_key' => {
130
+ 'map' => %q{function (doc) { emit([1, doc['_id']], 1); }}
131
+ }
132
+ }
133
+ }
134
+
135
+ db.put('_design/doc', doc, admin_credentials)
136
+ db.put('abc123', {}, admin_credentials)
137
+ db.put('abc456', {}, admin_credentials)
138
+ end
139
+
140
+ it 'retrieves _all_docs' do
141
+ resp = db.view('_all_docs')
142
+
143
+ resp.code.should == '200'
144
+ end
145
+
146
+ it 'retrieves a view' do
147
+ resp = db.view('doc/a_view')
148
+
149
+ resp.code.should == '200'
150
+ resp.total_rows.should == resp.body['total_rows']
151
+ resp.offset.should == resp.body['offset']
152
+ resp.rows.should == resp.body['rows']
153
+ end
154
+
155
+ it 'works with the full document name' do
156
+ resp = db.view('_design/doc/_view/a_view')
157
+
158
+ resp.code.should == '200'
159
+ end
160
+
161
+ it 'passes through view parameters' do
162
+ resp = db.view('doc/a_view', :skip => 1)
163
+
164
+ resp.offset.should == 1
165
+ resp.rows.length.should == 1
166
+ end
167
+
168
+ it 'JSON-encodes the key parameter' do
169
+ resp = db.view('doc/composite_key', :key => [1, 'abc123'])
170
+
171
+ resp.code.should == '200'
172
+ resp.rows.length.should == 1
173
+ end
174
+
175
+ it 'JSON-encodes the keys parameter' do
176
+ resp = db.view('doc/composite_key', :keys => [[1, 'abc123'], [1, 'abc456']])
177
+
178
+ resp.code.should == '200'
179
+ resp.rows.length.should == 2
180
+ end
181
+
182
+ it 'JSON-encodes the startkey parameter' do
183
+ resp = db.view('doc/composite_key', :startkey => [1, 'abc123'])
184
+
185
+ resp.code.should == '200'
186
+ resp.rows.length.should == 2
187
+ end
188
+
189
+ it 'JSON-encodes the endkey parameter' do
190
+ resp = db.view('doc/composite_key', :endkey => [1, 'abc456'], :skip => 1)
191
+
192
+ resp.code.should == '200'
193
+ resp.rows.length.should == 1
194
+ end
195
+
196
+ it 'can issue POSTs' do
197
+ resp = db.view('doc/a_view', :keys => ['abc123', 'abc456'], :post => true)
198
+
199
+ resp.code.should == '200'
200
+ resp.rows.length.should == 2
201
+ end
202
+
203
+ it 'passes credentials' do
204
+ security = {
205
+ 'members' => {
206
+ 'names' => [member1_username],
207
+ 'roles' => []
208
+ }
209
+ }
210
+
211
+ db.put!('_security', security, admin_credentials)
212
+
213
+ resp = db.view('doc/a_view', { :skip => 1 }, member1_credentials)
214
+
215
+ resp.code.should == '200'
216
+ end
217
+ end
218
+
219
+ describe '#view!' do
220
+ before do
221
+ clean_databases!
222
+ end
223
+
224
+ describe 'if the response code is 404' do
225
+ it 'raises Analysand::CannotAccessView' do
226
+ lambda { db.view!('unknown/view') }.should raise_error(Analysand::CannotAccessView)
227
+ end
228
+
229
+ it 'includes the response in the exception' do
230
+ code = nil
231
+
232
+ begin
233
+ db.view!('unknown/view')
234
+ rescue Analysand::CannotAccessView => e
235
+ code = e.response.code
236
+ end
237
+
238
+ code.should == '404'
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,488 @@
1
+ require 'spec_helper'
2
+
3
+ require 'analysand/database'
4
+ require 'analysand/errors'
5
+
6
+ module Analysand
7
+ describe Database do
8
+ let(:db) { Database.new(database_uri) }
9
+
10
+ let(:doc_id) { 'abc123' }
11
+ let(:doc) do
12
+ { 'foo' => 'bar' }
13
+ end
14
+
15
+ before do
16
+ clean_databases!
17
+ clear_security
18
+ end
19
+
20
+ shared_examples_for '#put success examples' do
21
+ def put(*args)
22
+ db.send(method, *args)
23
+ end
24
+
25
+ it 'creates documents' do
26
+ put(doc_id, doc)
27
+
28
+ db.get(doc_id).body['foo'].should == 'bar'
29
+ end
30
+
31
+ it 'returns success on document creation' do
32
+ resp = put(doc_id, doc)
33
+
34
+ resp.should be_success
35
+ end
36
+
37
+ it 'ignores the _id attribute in documents' do
38
+ doc.update('_id' => 'wrong')
39
+ put(doc_id, doc)
40
+
41
+ db.get(doc_id).body['foo'].should == 'bar'
42
+ db.get('wrong').should_not be_success
43
+ end
44
+
45
+ it 'passes credentials' do
46
+ set_security({ 'users' => [member1_username] })
47
+
48
+ put(doc_id, doc, member1_credentials)
49
+ db.get(doc_id).should be_success
50
+ end
51
+
52
+ it 'passes the batch option' do
53
+ resp = put(doc_id, doc, nil, :batch => 'ok')
54
+
55
+ resp.code.should == '202'
56
+ end
57
+
58
+ it 'updates documents' do
59
+ resp = put(doc_id, doc)
60
+
61
+ doc['bar'] = 'baz'
62
+ doc['_rev'] = resp['rev']
63
+
64
+ put(doc_id, doc)
65
+ db.get(doc_id)['bar'].should == 'baz'
66
+ end
67
+
68
+ it 'returns success on document update' do
69
+ resp = put(doc_id, doc)
70
+
71
+ doc['bar'] = 'baz'
72
+ doc['_rev'] = resp['rev']
73
+
74
+ resp = put(doc_id, doc)
75
+ resp.should be_success
76
+ end
77
+
78
+ it 'escapes document IDs' do
79
+ db.put('an ID', doc)
80
+
81
+ db.get('an ID').should be_success
82
+ end
83
+
84
+ it 'handles URN-like IDs' do
85
+ db.put('org.couchdb.doc:one', doc)
86
+
87
+ db.get('org.couchdb.doc:one').should be_success
88
+ end
89
+ end
90
+
91
+ describe '#create' do
92
+ before do
93
+ drop_databases!
94
+ end
95
+
96
+ it 'creates a database' do
97
+ db.create(admin_credentials)
98
+
99
+ db.ping.should be_success
100
+ end
101
+
102
+ it 'returns success' do
103
+ db.create(admin_credentials).should be_success
104
+ end
105
+ end
106
+
107
+ describe '#drop' do
108
+ it 'drops the database' do
109
+ db.drop(admin_credentials)
110
+
111
+ db.ping.should_not be_success
112
+ end
113
+
114
+ it 'returns success' do
115
+ db.drop(admin_credentials).should be_success
116
+ end
117
+ end
118
+
119
+ describe '#drop!' do
120
+ it 'drops the database' do
121
+ db.drop(admin_credentials)
122
+
123
+ db.ping.should_not be_success
124
+ end
125
+
126
+ it 'raises Analysand::CannotDropDatabase on failure' do
127
+ lambda { db.drop!(member1_credentials) }.should raise_error(Analysand::CannotDropDatabase)
128
+ end
129
+ end
130
+
131
+ describe '#put' do
132
+ it_should_behave_like '#put success examples' do
133
+ let(:method) { :put }
134
+ end
135
+
136
+ describe 'on update conflict' do
137
+ before do
138
+ db.put(doc_id, doc)
139
+ end
140
+
141
+ it 'returns the error code' do
142
+ resp = db.put(doc_id, doc)
143
+
144
+ resp.code.should == '409'
145
+ end
146
+
147
+ it 'returns the error body' do
148
+ resp = db.put(doc_id, doc)
149
+
150
+ resp.body.should have_key('error')
151
+ end
152
+ end
153
+ end
154
+
155
+ describe '#put!' do
156
+ it_should_behave_like '#put success examples' do
157
+ let(:method) { :put! }
158
+ end
159
+
160
+ describe 'on update conflict' do
161
+ before do
162
+ db.put(doc_id, doc)
163
+ end
164
+
165
+ it 'raises Analysand::DocumentNotSaved' do
166
+ lambda { db.put!(doc_id, doc) }.should raise_error(Analysand::DocumentNotSaved) { |e|
167
+ e.response.code.should == '409'
168
+ }
169
+ end
170
+ end
171
+ end
172
+
173
+ describe '#ensure_full_commit' do
174
+ before do
175
+ db.put('abc', {}, :batch => :ok)
176
+ end
177
+
178
+ it 'returns success' do
179
+ db.ensure_full_commit.should be_success
180
+ end
181
+
182
+ it 'flushes batched PUTs' do
183
+ db.ensure_full_commit
184
+
185
+ db.get('abc').should be_success
186
+ end
187
+
188
+ it 'accepts credentials' do
189
+ lambda { db.ensure_full_commit(member1_credentials) }.should_not raise_error
190
+ end
191
+
192
+ it 'accepts a seq parameter' do
193
+ lambda { db.ensure_full_commit(member1_credentials, :seq => 10) }.should_not raise_error
194
+ end
195
+ end
196
+
197
+ describe '#put_attachment' do
198
+ let(:string) { 'an attachment' }
199
+ let(:io) { StringIO.new(string) }
200
+
201
+ it 'creates attachments' do
202
+ db.put_attachment("#{doc_id}/attachment", io)
203
+
204
+ db.get_attachment("#{doc_id}/attachment").body.should == string
205
+ end
206
+
207
+ it 'returns success when the attachment is uploaded' do
208
+ resp = db.put_attachment("#{doc_id}/attachment", io)
209
+
210
+ resp.should be_success
211
+ end
212
+
213
+ it 'passes credentials' do
214
+ set_security({ 'users' => [member1_username] })
215
+
216
+ resp = db.put_attachment("#{doc_id}/a", io, member1_credentials)
217
+ resp.should be_success
218
+ end
219
+
220
+ it 'sends the rev of the target document' do
221
+ resp = db.put!(doc_id, doc)
222
+ rev = resp['rev']
223
+
224
+ resp = db.put_attachment("#{doc_id}/a", io, nil, :rev => rev)
225
+ resp.should be_success
226
+ end
227
+
228
+ it 'sends the content type of the attachment' do
229
+ db.put_attachment("#{doc_id}/a", io, nil, :content_type => 'text/plain')
230
+
231
+ type = db.get_attachment("#{doc_id}/a").get_fields('Content-Type')
232
+ type.should == ['text/plain']
233
+ end
234
+ end
235
+
236
+ describe '#bulk_docs' do
237
+ let(:doc1) { { '_id' => 'doc1', 'foo' => 'bar' } }
238
+ let(:doc2) { { '_id' => 'doc2', 'bar' => 'baz' } }
239
+
240
+ it 'creates many documents' do
241
+ db.bulk_docs([doc1, doc2])
242
+
243
+ db.get('doc1')['foo'].should == 'bar'
244
+ db.get('doc2')['bar'].should == 'baz'
245
+ end
246
+
247
+ it 'updates many documents' do
248
+ r1 = db.put!('doc1', doc1)
249
+ r2 = db.put!('doc2', doc2)
250
+
251
+ doc1['foo'] = 'qux'
252
+ doc2['bar'] = 'quux'
253
+ doc1['_rev'] = r1['rev']
254
+ doc2['_rev'] = r2['rev']
255
+
256
+ db.bulk_docs([doc1, doc2])
257
+
258
+ db.get('doc1')['foo'].should == 'qux'
259
+ db.get('doc2')['bar'].should == 'quux'
260
+ end
261
+
262
+ it 'deletes many documents' do
263
+ r1 = db.put!('doc1', doc1)
264
+ r2 = db.put!('doc2', doc2)
265
+
266
+ d1 = { '_id' => 'doc1', '_rev' => r1['rev'], '_deleted' => true }
267
+ d2 = { '_id' => 'doc2', '_rev' => r2['rev'], '_deleted' => true }
268
+
269
+ db.bulk_docs([d1, d2])
270
+
271
+ db.get('doc1').code.should == '404'
272
+ db.get('doc2').code.should == '404'
273
+ end
274
+
275
+ it 'updates and deletes documents' do
276
+ r1 = db.put!('doc1', doc1)
277
+ r2 = db.put!('doc2', doc2)
278
+
279
+ d1 = { '_id' => 'doc1', '_rev' => r1['rev'], '_deleted' => true }
280
+ d2 = { '_id' => 'doc2', '_rev' => r2['rev'], 'bar' => 'quux' }
281
+
282
+ db.bulk_docs([d1, d2])
283
+
284
+ db.get('doc1').code.should == '404'
285
+ db.get('doc2')['bar'].should == 'quux'
286
+ end
287
+
288
+ it 'returns success if all operations succeeded' do
289
+ resp = db.bulk_docs([doc1, doc2])
290
+
291
+ resp.should be_success
292
+ end
293
+
294
+ it 'returns non-success if one operation had an error' do
295
+ db.put!('doc1', doc1)
296
+
297
+ resp = db.bulk_docs([doc1, doc2])
298
+
299
+ resp.should_not be_success
300
+ end
301
+
302
+ it 'passes credentials' do
303
+ set_security({ 'users' => [member1_username] })
304
+
305
+ resp = db.bulk_docs([doc1, doc2], member1_credentials)
306
+
307
+ resp.should be_success
308
+ end
309
+
310
+ it 'operates in non-atomic mode by default' do
311
+ db.put!('doc1', doc1)
312
+ db.bulk_docs([doc1, doc2])
313
+
314
+ db.get('doc2').should be_success
315
+ end
316
+
317
+ it 'supports all-or-nothing mode' do
318
+ # Force a validation failure to check that all-or-nothing mode is
319
+ # properly enabled.
320
+ db.put!('doc1', doc1)
321
+ db.put!('_design/validation', {
322
+ 'validate_doc_update' => 'function(){throw({forbidden: ""});}'
323
+ }, admin_credentials)
324
+
325
+ db.bulk_docs([doc1, doc2], nil, :all_or_nothing => true)
326
+
327
+ db.get('doc2').code.should == '404'
328
+ end
329
+ end
330
+
331
+ describe '#bulk_docs!' do
332
+ let(:doc1) { { '_id' => 'doc1', 'foo' => 'bar' } }
333
+ let(:doc2) { { '_id' => 'doc2', 'bar' => 'baz' } }
334
+
335
+ it 'returns success if all operations succeeded' do
336
+ resp = db.bulk_docs!([doc1, doc2])
337
+
338
+ resp.should be_success
339
+ end
340
+
341
+ describe 'if authorization fails' do
342
+ let(:wrong) do
343
+ { :username => 'wrong', :password => 'wrong' }
344
+ end
345
+
346
+ it 'raises Analysand::BulkOperationFailed' do
347
+ lambda { db.bulk_docs!([doc1, doc2], wrong) }.should raise_error(Analysand::BulkOperationFailed)
348
+ end
349
+ end
350
+
351
+ describe 'if an operation fails' do
352
+ before do
353
+ doc2['_id'] = 'doc1'
354
+ end
355
+
356
+ it 'raises Analysand::BulkOperationFailed' do
357
+ lambda { db.bulk_docs!([doc1, doc2]) }.should raise_error(Analysand::BulkOperationFailed, /bulk operation failed/i)
358
+ end
359
+ end
360
+ end
361
+
362
+ describe '#copy' do
363
+ before do
364
+ db.put!(doc_id, doc)
365
+ end
366
+
367
+ it 'copies one doc to another ID' do
368
+ db.copy(doc_id, 'bar')
369
+
370
+ db.get('bar')['foo'].should == 'bar'
371
+ end
372
+
373
+ it 'returns success if copy succeeds' do
374
+ resp = db.copy(doc_id, 'bar')
375
+
376
+ resp.should be_success
377
+ end
378
+
379
+ it 'returns failure if copy fails' do
380
+ db.put!('bar', {})
381
+ resp = db.copy(doc_id, 'bar')
382
+
383
+ resp.code.should == '409'
384
+ end
385
+
386
+ it 'overwrites documents' do
387
+ resp = db.put!('bar', {})
388
+ db.copy(doc_id, "bar?rev=#{resp['rev']}")
389
+
390
+ db.get('bar')['foo'].should == 'bar'
391
+ end
392
+
393
+ it 'passes credentials' do
394
+ set_security({ 'users' => [member1_username] })
395
+
396
+ db.copy(doc_id, 'bar', member1_credentials)
397
+ db.get('bar')['foo'].should == 'bar'
398
+ end
399
+
400
+ it 'escapes document IDs in URIs' do
401
+ db.copy(doc_id, 'an ID')
402
+
403
+ db.get('an ID')['foo'].should == 'bar'
404
+ end
405
+ end
406
+
407
+ shared_examples_for '#delete success examples' do
408
+ let(:rev) { @put_resp['rev'] }
409
+
410
+ before do
411
+ @put_resp = db.put!(doc_id, doc)
412
+ end
413
+
414
+ def delete(*args)
415
+ db.send(method, *args)
416
+ end
417
+
418
+ it 'deletes documents' do
419
+ db.delete(doc_id, rev)
420
+
421
+ db.get(doc_id).code.should == '404'
422
+ end
423
+
424
+ it 'returns success on deletion' do
425
+ resp = db.delete(doc_id, rev)
426
+
427
+ resp.should be_success
428
+ end
429
+
430
+ it 'passes credentials' do
431
+ set_security({ 'users' => [member1_username] })
432
+
433
+ resp = db.delete(doc_id, rev, member1_credentials)
434
+
435
+ resp.should be_success
436
+ end
437
+
438
+ it 'escapes document IDs in URIs' do
439
+ @put_resp = db.put!('an ID', doc)
440
+
441
+ resp = db.delete('an ID', rev)
442
+ resp.should be_success
443
+ end
444
+ end
445
+
446
+ describe '#delete' do
447
+ it_should_behave_like '#delete success examples' do
448
+ let(:method) { :delete }
449
+ end
450
+
451
+ describe 'on update conflict' do
452
+ before do
453
+ db.put!(doc_id, doc)
454
+ end
455
+
456
+ it 'returns the error code' do
457
+ resp = db.delete(doc_id, nil)
458
+
459
+ resp.code.should == '400'
460
+ end
461
+
462
+ it 'returns the error body' do
463
+ resp = db.delete(doc_id, nil)
464
+
465
+ resp.body.should have_key('error')
466
+ end
467
+ end
468
+ end
469
+
470
+ describe '#delete!' do
471
+ it_should_behave_like '#delete success examples' do
472
+ let(:method) { :delete! }
473
+ end
474
+
475
+ describe 'on update conflict' do
476
+ before do
477
+ db.put!(doc_id, doc)
478
+ end
479
+
480
+ it 'raises Analysand::DocumentNotDeleted' do
481
+ lambda { db.delete!(doc_id, nil) }.should raise_error(Analysand::DocumentNotDeleted) { |e|
482
+ e.response.code.should == '400'
483
+ }
484
+ end
485
+ end
486
+ end
487
+ end
488
+ end