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