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.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/CHANGELOG +67 -0
- data/Gemfile +8 -0
- data/LICENSE +22 -0
- data/README +48 -0
- data/Rakefile +22 -0
- data/analysand.gemspec +33 -0
- data/bin/analysand +27 -0
- data/lib/analysand.rb +3 -0
- data/lib/analysand/bulk_response.rb +14 -0
- data/lib/analysand/change_watcher.rb +280 -0
- data/lib/analysand/config_response.rb +25 -0
- data/lib/analysand/connection_testing.rb +52 -0
- data/lib/analysand/database.rb +322 -0
- data/lib/analysand/errors.rb +60 -0
- data/lib/analysand/http.rb +90 -0
- data/lib/analysand/instance.rb +255 -0
- data/lib/analysand/reading.rb +26 -0
- data/lib/analysand/response.rb +35 -0
- data/lib/analysand/response_headers.rb +18 -0
- data/lib/analysand/session_response.rb +16 -0
- data/lib/analysand/status_code_predicates.rb +25 -0
- data/lib/analysand/streaming_view_response.rb +90 -0
- data/lib/analysand/version.rb +3 -0
- data/lib/analysand/view_response.rb +24 -0
- data/lib/analysand/view_streaming/builder.rb +142 -0
- data/lib/analysand/viewing.rb +95 -0
- data/lib/analysand/writing.rb +71 -0
- data/script/setup_database.rb +45 -0
- data/spec/analysand/a_response.rb +70 -0
- data/spec/analysand/change_watcher_spec.rb +102 -0
- data/spec/analysand/database_spec.rb +243 -0
- data/spec/analysand/database_writing_spec.rb +488 -0
- data/spec/analysand/instance_spec.rb +205 -0
- data/spec/analysand/response_spec.rb +26 -0
- data/spec/analysand/view_response_spec.rb +44 -0
- data/spec/analysand/view_streaming/builder_spec.rb +73 -0
- data/spec/analysand/view_streaming_spec.rb +122 -0
- data/spec/fixtures/vcr_cassettes/get_config.yml +40 -0
- data/spec/fixtures/vcr_cassettes/get_many_config.yml +40 -0
- data/spec/fixtures/vcr_cassettes/head_request_with_etag.yml +40 -0
- data/spec/fixtures/vcr_cassettes/reload_config.yml +114 -0
- data/spec/fixtures/vcr_cassettes/unauthorized_put_config.yml +43 -0
- data/spec/fixtures/vcr_cassettes/view.yml +40 -0
- data/spec/smoke/database_thread_spec.rb +59 -0
- data/spec/spec_helper.rb +30 -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 +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
|