analysand 1.0.1
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.
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README +41 -0
- data/Rakefile +8 -0
- data/analysand.gemspec +30 -0
- data/bin/analysand +28 -0
- data/lib/analysand.rb +5 -0
- data/lib/analysand/bulk_response.rb +14 -0
- data/lib/analysand/change_watcher.rb +276 -0
- data/lib/analysand/connection_testing.rb +46 -0
- data/lib/analysand/database.rb +492 -0
- data/lib/analysand/errors.rb +26 -0
- data/lib/analysand/instance.rb +156 -0
- data/lib/analysand/response.rb +45 -0
- data/lib/analysand/version.rb +3 -0
- data/lib/analysand/view_response.rb +24 -0
- data/script/setup_database.rb +45 -0
- data/spec/analysand/a_session_grantor.rb +40 -0
- data/spec/analysand/change_watcher_spec.rb +84 -0
- data/spec/analysand/database_spec.rb +228 -0
- data/spec/analysand/database_writing_spec.rb +478 -0
- data/spec/analysand/instance_spec.rb +86 -0
- data/spec/analysand/response_spec.rb +22 -0
- data/spec/analysand/view_response_spec.rb +33 -0
- data/spec/fixtures/vcr_cassettes/get_session_does_not_refresh_cookie.yml +73 -0
- data/spec/fixtures/vcr_cassettes/get_session_refreshes_cookie.yml +75 -0
- data/spec/fixtures/vcr_cassettes/head_request_with_etag.yml +40 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/support/database_access.rb +40 -0
- data/spec/support/example_isolation.rb +86 -0
- data/spec/support/test_parameters.rb +39 -0
- metadata +276 -0
@@ -0,0 +1,478 @@
|
|
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(ArgumentError)
|
190
|
+
end
|
191
|
+
|
192
|
+
it 'accepts a seq parameter' do
|
193
|
+
lambda { db.ensure_full_commit(member1_credentials, :seq => 10) }.should_not raise_error(ArgumentError)
|
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 an operation fails' do
|
342
|
+
before do
|
343
|
+
doc2['_id'] = 'doc1'
|
344
|
+
end
|
345
|
+
|
346
|
+
it 'raises Analysand::BulkOperationFailed' do
|
347
|
+
lambda { db.bulk_docs!([doc1, doc2]) }.should raise_error(Analysand::BulkOperationFailed)
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
describe '#copy' do
|
353
|
+
before do
|
354
|
+
db.put!(doc_id, doc)
|
355
|
+
end
|
356
|
+
|
357
|
+
it 'copies one doc to another ID' do
|
358
|
+
db.copy(doc_id, 'bar')
|
359
|
+
|
360
|
+
db.get('bar')['foo'].should == 'bar'
|
361
|
+
end
|
362
|
+
|
363
|
+
it 'returns success if copy succeeds' do
|
364
|
+
resp = db.copy(doc_id, 'bar')
|
365
|
+
|
366
|
+
resp.should be_success
|
367
|
+
end
|
368
|
+
|
369
|
+
it 'returns failure if copy fails' do
|
370
|
+
db.put!('bar', {})
|
371
|
+
resp = db.copy(doc_id, 'bar')
|
372
|
+
|
373
|
+
resp.code.should == '409'
|
374
|
+
end
|
375
|
+
|
376
|
+
it 'overwrites documents' do
|
377
|
+
resp = db.put!('bar', {})
|
378
|
+
db.copy(doc_id, "bar?rev=#{resp['rev']}")
|
379
|
+
|
380
|
+
db.get('bar')['foo'].should == 'bar'
|
381
|
+
end
|
382
|
+
|
383
|
+
it 'passes credentials' do
|
384
|
+
set_security({ 'users' => [member1_username] })
|
385
|
+
|
386
|
+
db.copy(doc_id, 'bar', member1_credentials)
|
387
|
+
db.get('bar')['foo'].should == 'bar'
|
388
|
+
end
|
389
|
+
|
390
|
+
it 'escapes document IDs in URIs' do
|
391
|
+
db.copy(doc_id, 'an ID')
|
392
|
+
|
393
|
+
db.get('an ID')['foo'].should == 'bar'
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
shared_examples_for '#delete success examples' do
|
398
|
+
let(:rev) { @put_resp['rev'] }
|
399
|
+
|
400
|
+
before do
|
401
|
+
@put_resp = db.put!(doc_id, doc)
|
402
|
+
end
|
403
|
+
|
404
|
+
def delete(*args)
|
405
|
+
db.send(method, *args)
|
406
|
+
end
|
407
|
+
|
408
|
+
it 'deletes documents' do
|
409
|
+
db.delete(doc_id, rev)
|
410
|
+
|
411
|
+
db.get(doc_id).code.should == '404'
|
412
|
+
end
|
413
|
+
|
414
|
+
it 'returns success on deletion' do
|
415
|
+
resp = db.delete(doc_id, rev)
|
416
|
+
|
417
|
+
resp.should be_success
|
418
|
+
end
|
419
|
+
|
420
|
+
it 'passes credentials' do
|
421
|
+
set_security({ 'users' => [member1_username] })
|
422
|
+
|
423
|
+
resp = db.delete(doc_id, rev, member1_credentials)
|
424
|
+
|
425
|
+
resp.should be_success
|
426
|
+
end
|
427
|
+
|
428
|
+
it 'escapes document IDs in URIs' do
|
429
|
+
@put_resp = db.put!('an ID', doc)
|
430
|
+
|
431
|
+
resp = db.delete('an ID', rev)
|
432
|
+
resp.should be_success
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
describe '#delete' do
|
437
|
+
it_should_behave_like '#delete success examples' do
|
438
|
+
let(:method) { :delete }
|
439
|
+
end
|
440
|
+
|
441
|
+
describe 'on update conflict' do
|
442
|
+
before do
|
443
|
+
db.put!(doc_id, doc)
|
444
|
+
end
|
445
|
+
|
446
|
+
it 'returns the error code' do
|
447
|
+
resp = db.delete(doc_id, nil)
|
448
|
+
|
449
|
+
resp.code.should == '400'
|
450
|
+
end
|
451
|
+
|
452
|
+
it 'returns the error body' do
|
453
|
+
resp = db.delete(doc_id, nil)
|
454
|
+
|
455
|
+
resp.body.should have_key('error')
|
456
|
+
end
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
describe '#delete!' do
|
461
|
+
it_should_behave_like '#delete success examples' do
|
462
|
+
let(:method) { :delete! }
|
463
|
+
end
|
464
|
+
|
465
|
+
describe 'on update conflict' do
|
466
|
+
before do
|
467
|
+
db.put!(doc_id, doc)
|
468
|
+
end
|
469
|
+
|
470
|
+
it 'raises Analysand::DocumentNotDeleted' do
|
471
|
+
lambda { db.delete!(doc_id, nil) }.should raise_error(Analysand::DocumentNotDeleted) { |e|
|
472
|
+
e.response.code.should == '400'
|
473
|
+
}
|
474
|
+
end
|
475
|
+
end
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|