rocking_chair 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,284 @@
1
+ module RockingChair
2
+ class View
3
+
4
+ attr_accessor :database, :keys, :options, :ruby_store, :design_document, :design_document_name, :view_name, :view_document
5
+
6
+ def self.run(database, design_document_name, view_name, options = {})
7
+ raise ArgumentError, "Need a databae, a design_doc_name and a view_name" unless database.present? && design_document_name.present? && view_name.present?
8
+ new(database, design_document_name, view_name, options).find.render
9
+ end
10
+
11
+ def self.run_all(database, options = {})
12
+ new(database, :all, :all, options).find.render_for_all
13
+ end
14
+
15
+ def initialize(database, design_document_name, view_name, options = {})
16
+ unless design_document_name == :all && view_name == :all
17
+ RockingChair::Error.raise_404 unless database.exists?("_design/#{design_document_name}")
18
+ @design_document = JSON.parse(database.storage["_design/#{design_document_name}"])
19
+ @view_document = design_document['views'][view_name] || RockingChair::Error.raise_404
20
+ end
21
+
22
+ @database = database
23
+ @keys = database.storage.keys
24
+ @design_document_name = design_document_name
25
+ @view_name = view_name
26
+ initialize_ruby_store
27
+
28
+ @options = {
29
+ 'reduce' => false,
30
+ 'limit' => nil,
31
+ 'key' => nil,
32
+ 'descending' => false,
33
+ 'include_docs' => false,
34
+ 'without_deleted' => false,
35
+ 'endkey' => nil,
36
+ 'startkey' => nil,
37
+ 'endkey_docid' => nil,
38
+ 'startkey_docid' => nil
39
+ }.update(options)
40
+ @options.assert_valid_keys('reduce', 'limit', 'key', 'descending', 'include_docs', 'without_deleted', 'endkey', 'startkey', 'endkey_docid', 'startkey_docid')
41
+ RockingChair::Helper.jsonfy_options(@options, 'key', 'startkey', 'endkey', 'startkey_docid', 'endkey_docid')
42
+
43
+ normalize_view_name
44
+ end
45
+
46
+ def find
47
+ if view_name == :all
48
+ find_all
49
+ elsif match = view_name.match(/\Aall_documents\Z/)
50
+ find_all_by_class
51
+ elsif match = view_name.match(/\Aby_(\w+)\Z/)
52
+ find_by_attribute(match[1])
53
+ elsif match = view_name.match(/\Aassociation_#{design_document_name}_belongs_to_(\w+)\Z/)
54
+ find_belongs_to(match[1])
55
+ else
56
+ raise "Unknown View implementation for view #{view_name.inspect} in design document _design/#{design_document_name}"
57
+ end
58
+ self
59
+ end
60
+
61
+ def render
62
+ offset = 0
63
+ total_size = keys.size
64
+ filter_by_startkey_docid_and_endkey_docid
65
+ filter_by_limit
66
+
67
+ if options['reduce'].to_s == 'true'
68
+ { "rows" => [{'key' => options['key'], 'value' => keys.size }]}.to_json
69
+ else
70
+ rows = keys.map do |key|
71
+ document = ruby_store[key]
72
+ if options['include_docs'].to_s == 'true'
73
+ {'id' => RockingChair::Helper.access('_id', document), 'value' => nil, 'doc' => (document.respond_to?(:_document) ? document._document : document) }.merge(key_description)
74
+ else
75
+ {'id' => RockingChair::Helper.access('_id', document), 'key' => options['key'], 'value' => nil}.merge(key_description)
76
+ end
77
+ end
78
+ { "total_rows" => total_size, "offset" => offset, "rows" => rows}.to_json
79
+ end
80
+ end
81
+
82
+ def render_for_all
83
+ offset = filter_by_startkey
84
+ filter_by_endkey
85
+ filter_by_limit
86
+
87
+ rows = keys.map do |key|
88
+ document = ruby_store[key]
89
+ if options['include_docs'].to_s == 'true'
90
+ {'id' => document['_id'], 'key' => document['_id'], 'value' => document.update('rev' => document['_rev'])}
91
+ else
92
+ {'id' => document['_id'], 'key' => document['_id'], 'value' => {'rev' => document['_rev']}}
93
+ end
94
+ end
95
+
96
+ { "total_rows" => database.document_count, "offset" => offset, "rows" => rows}.to_json
97
+ end
98
+
99
+ protected
100
+
101
+ def find_all
102
+ sort_by_attribute('_id')
103
+ end
104
+
105
+ def find_all_by_class
106
+ filter_items_without_correct_ruby_class
107
+ filter_deleted_items if options['without_deleted'].to_s == 'true'
108
+ end
109
+
110
+ def find_belongs_to(belongs_to)
111
+ filter_items_by_key([foreign_key_id(belongs_to)])
112
+ filter_items_without_correct_ruby_class
113
+ filter_deleted_items if options['without_deleted'].to_s == 'true'
114
+ end
115
+
116
+ def find_by_attribute(attribute_string)
117
+ attributes = attribute_string.split("_and_")
118
+
119
+ filter_items_by_key(attributes)
120
+ filter_items_without_correct_ruby_class
121
+ filter_deleted_items if options['without_deleted'].to_s == 'true'
122
+ sort_by_attribute(attributes.first)
123
+ end
124
+
125
+ def normalize_view_name
126
+ return if view_name.is_a?(Symbol)
127
+
128
+ if view_name.match(/_withoutdeleted\Z/) || view_name.match(/_without_deleted\Z/)
129
+ options['without_deleted'] = true
130
+ elsif view_name.match(/_withdeleted\Z/) || view_name.match(/_with_deleted\Z/)
131
+ options['without_deleted'] = false
132
+ else
133
+ options['without_deleted'] = view_document['map'].match(/\"soft\" deleted/) ? true : nil
134
+ end
135
+ @view_name = view_name.gsub(/_withoutdeleted\Z/, '').gsub(/_without_deleted\Z/, '').gsub(/_withdeleted\Z/, '').gsub(/_with_deleted\Z/, '')
136
+ end
137
+
138
+ def initialize_ruby_store
139
+ @ruby_store = database.storage.dup
140
+ @ruby_store.each{|k,v| ruby_store[k] = JSON.parse(v) }
141
+ end
142
+
143
+ def filter_items_by_key(attributes)
144
+ if options['startkey']
145
+ filter_items_by_range(attributes)
146
+ else
147
+ filter_items_by_exact_key(attributes)
148
+ end
149
+ end
150
+
151
+ def filter_items_by_exact_key(attributes)
152
+ filter_keys = options['key'].is_a?(Array) ? options['key'] : [options['key']]
153
+ attributes.each_with_index do |attribute, index|
154
+ filter_items_without_attribute_value(attribute, filter_keys[index])
155
+ end
156
+ end
157
+
158
+ def filter_items_by_range(attributes)
159
+ start_keys = options['startkey'].is_a?(Array) ? options['startkey'] : [options['startkey']]
160
+ end_keys = options['endkey'].is_a?(Array) ? options['endkey'] : [options['endkey']]
161
+
162
+ attributes.each_with_index do |attribute, index|
163
+ filter_items_not_in_range(attribute, start_keys[index], end_keys[index])
164
+ end
165
+ end
166
+
167
+ def filter_items_not_in_range(attribute, start_key, end_key)
168
+ @keys = keys.select do |key|
169
+ document = ruby_store[key]
170
+ if end_key
171
+ RockingChair::Helper.access(attribute, document) && (RockingChair::Helper.access(attribute, document) >= start_key) && (RockingChair::Helper.access(attribute, document) <= end_key)
172
+ else
173
+ RockingChair::Helper.access(attribute, document) && (RockingChair::Helper.access(attribute, document) >= start_key)
174
+ end
175
+ end
176
+ end
177
+
178
+ def filter_deleted_items
179
+ @keys = keys.delete_if do |key|
180
+ document = ruby_store[key]
181
+ RockingChair::Helper.access('deleted_at', document).present?
182
+ end
183
+ end
184
+
185
+ def sort_by_attribute(attribute)
186
+ attribute ||= '_id'
187
+ @keys = (options['descending'].to_s == 'true') ?
188
+ keys.sort{|x,y| RockingChair::Helper.access(attribute, ruby_store[y]) <=> RockingChair::Helper.access(attribute, ruby_store[x]) } :
189
+ keys.sort{|x,y| RockingChair::Helper.access(attribute, ruby_store[x]) <=> RockingChair::Helper.access(attribute, ruby_store[y]) }
190
+ end
191
+
192
+ def filter_items_without_attribute_value(attribute, attr_value)
193
+ if attr_value
194
+ @keys = keys.select do |key|
195
+ document = ruby_store[key]
196
+ if RockingChair::Helper.access(attribute, document).is_a?(Array)
197
+ RockingChair::Helper.access(attribute, document).include?(attr_value)
198
+ else
199
+ RockingChair::Helper.access(attribute, document) == attr_value
200
+ end
201
+ end
202
+ else
203
+ @keys = keys.select do |key|
204
+ document = ruby_store[key]
205
+ RockingChair::Helper.access(attribute, document).present?
206
+ end
207
+ end
208
+ end
209
+
210
+ def filter_items_without_correct_ruby_class
211
+ klass_name = design_document_name.classify
212
+ @keys = keys.select do |key|
213
+ document = ruby_store[key]
214
+ RockingChair::Helper.access('ruby_class', document).to_s.classify == klass_name
215
+ end
216
+ end
217
+
218
+ def filter_by_limit
219
+ if options['limit']
220
+ @keys = keys[0, options['limit'].to_i]
221
+ end
222
+ end
223
+
224
+ def filter_by_startkey_docid_and_endkey_docid
225
+ if options['startkey_docid'] || options['endkey_docid']
226
+ @keys = keys.select do |key|
227
+ if options['startkey_docid'] && options['endkey_docid']
228
+ ( key >= options['startkey_docid']) && (key <= options['endkey_docid'])
229
+ elsif options['startkey_docid']
230
+ key >= options['startkey_docid']
231
+ else
232
+ key <= options['endkey_docid']
233
+ end
234
+ end
235
+ end
236
+ end
237
+
238
+ def filter_by_startkey
239
+ offset = 0
240
+ if options['startkey']
241
+ startkey_found = false
242
+ @keys = keys.map do |key|
243
+ if startkey_found || key == options['startkey']
244
+ startkey_found = true
245
+ key
246
+ else
247
+ offset += 1
248
+ nil
249
+ end
250
+ end.compact
251
+ end
252
+ return offset
253
+ end
254
+
255
+ def filter_by_endkey
256
+ if options['endkey']
257
+ endkey_found = false
258
+ @keys = keys.map do |key|
259
+ if key == options['endkey']
260
+ endkey_found = true
261
+ key
262
+ elsif endkey_found
263
+ nil
264
+ else
265
+ key
266
+ end
267
+ end.compact
268
+ end
269
+ end
270
+
271
+ def foreign_key_id(name)
272
+ name.underscore.gsub('/','__').gsub('::','__') + "_id"
273
+ end
274
+
275
+ def key_description
276
+ description = {'key' => options['key']}
277
+ description = {'startkey' => options['startkey'], 'endkey' => options['endkey']} if options['startkey']
278
+ description.update('startkey_docid' => options['startkey_docid']) if options['startkey_docid']
279
+ description.update('endkey_docid' => options['endkey_docid']) if options['endkey_docid']
280
+ description
281
+ end
282
+
283
+ end
284
+ end
@@ -0,0 +1,350 @@
1
+ require File.dirname(__FILE__) + "/test_helper"
2
+
3
+ class CouchRestTest < Test::Unit::TestCase
4
+ context "The HTTP Apdapter for CouchRest" do
5
+ setup do
6
+ RockingChair::Server.reset
7
+ @couch = CouchRest.new("http://127.0.0.1:5984")
8
+ end
9
+
10
+ context "Database API" do
11
+
12
+ should "return the info JSON" do
13
+ assert_equal({"couchdb" => "Welcome","version" => "0.10.1"}, @couch.info)
14
+ end
15
+
16
+ should "return a list of all databases" do
17
+ assert_equal [], @couch.databases
18
+ @couch.create_db('database-name')
19
+ assert_equal ['database-name'], @couch.databases
20
+ end
21
+
22
+ should "create a database" do
23
+ db = @couch.create_db('database-name')
24
+ end
25
+
26
+ should "return information on a database" do
27
+ @couch.create_db('database-name')
28
+ assert_equal({
29
+ "db_name" => "database-name",
30
+ "doc_count" => 0,
31
+ "doc_del_count" => 0,
32
+ "update_seq" => 10,
33
+ "purge_seq" => 0,
34
+ "compact_running" => false,
35
+ "disk_size" => 16473,
36
+ "instance_start_time" => "1265409273572320",
37
+ "disk_format_version" => 4}, @couch.database('database-name').info)
38
+ end
39
+
40
+ should "delete a database" do
41
+ db = @couch.database('database-name')
42
+ db.delete!
43
+ end
44
+
45
+ end
46
+
47
+ context "when asking for UUIDs" do
48
+ should "return a UUID" do
49
+ RockingChair::Database.expects(:uuids).with('2').returns(['1', '2'])
50
+ assert_equal '2', @couch.next_uuid(2)
51
+ end
52
+ end
53
+
54
+ context "Bulk Document API" do
55
+ setup do
56
+ @db = @couch.create_db('RockingChair')
57
+ RockingChair::Database.any_instance.stubs(:rev).returns('the-revision')
58
+ end
59
+
60
+ context "loading all documents" do
61
+ should "load the total documents" do
62
+ 5.times do |i|
63
+ @db.save_doc({'_id' => "item-#{i}", 'a' => 'b'})
64
+ end
65
+ assert_equal({
66
+ "total_rows" => 5, "offset" => 0, "rows" => [
67
+ {"id" => "item-0", "key" => "item-0", "value" => {"rev" => "the-revision"}},
68
+ {"id" => "item-1", "key" => "item-1", "value" => {"rev" => "the-revision"}},
69
+ {"id" => "item-2", "key" => "item-2", "value" => {"rev" => "the-revision"}},
70
+ {"id" => "item-3", "key" => "item-3", "value" => {"rev" => "the-revision"}},
71
+ {"id" => "item-4", "key" => "item-4", "value" => {"rev" => "the-revision"}}
72
+ ]
73
+ }, @db.documents)
74
+ end
75
+
76
+ should "sort the result by ID ascending" do
77
+ @db.save_doc({'_id' => "C", 'a' => 'b'})
78
+ @db.save_doc({'_id' => "B", 'a' => 'b'})
79
+ @db.save_doc({'_id' => "A", 'a' => 'b'})
80
+ assert_equal({
81
+ "total_rows" => 3, "offset" => 0, "rows" => [
82
+ {"id" => "A", "key" => "A", "value" => {"rev" => "the-revision"}},
83
+ {"id" => "B", "key" => "B", "value" => {"rev" => "the-revision"}},
84
+ {"id" => "C", "key" => "C", "value" => {"rev" => "the-revision"}}
85
+ ]
86
+ }, @db.documents)
87
+ end
88
+
89
+ should "sort the result by ID descending" do
90
+ @db.save_doc({'_id' => "C", 'a' => 'b'})
91
+ @db.save_doc({'_id' => "B", 'a' => 'b'})
92
+ @db.save_doc({'_id' => "A", 'a' => 'b'})
93
+ assert_equal({
94
+ "total_rows" => 3, "offset" => 0, "rows" => [
95
+ {"id" => "C", "key" => "C", "value" => {"rev" => "the-revision"}},
96
+ {"id" => "B", "key" => "B", "value" => {"rev" => "the-revision"}},
97
+ {"id" => "A", "key" => "A", "value" => {"rev" => "the-revision"}}
98
+ ]
99
+ }, @db.documents('descending' => true))
100
+ end
101
+
102
+ should "start by the given key" do
103
+ @db.save_doc({'_id' => "C", 'a' => 'b'})
104
+ @db.save_doc({'_id' => "B", 'a' => 'b'})
105
+ @db.save_doc({'_id' => "A", 'a' => 'b'})
106
+
107
+ assert_equal({
108
+ "total_rows" => 3, "offset" => 1, "rows" => [
109
+ {"id" => "B", "key" => "B", "value" => {"rev" => "the-revision"}},
110
+ {"id" => "C", "key" => "C", "value" => {"rev" => "the-revision"}}
111
+ ]
112
+ }, @db.documents(:startkey => 'B'))
113
+ end
114
+
115
+ should "start by the given key, include docs, descending and limit" do
116
+ @db.save_doc({'_id' => "C", 'a' => 'b'})
117
+ @db.save_doc({'_id' => "B", 'a' => 'b'})
118
+ @db.save_doc({'_id' => "A", 'a' => 'b'})
119
+ @db.save_doc({'_id' => "D", 'a' => 'b'})
120
+
121
+ assert_equal({
122
+ "total_rows" => 4, "offset" => 1, "rows" => [
123
+ {"id" => "C", "key" => "C", "value" => {"rev" => "the-revision", '_rev' => 'the-revision', '_id' => 'C', 'a' => 'b'}},
124
+ {"id" => "B", "key" => "B", "value" => {"rev" => "the-revision", '_rev' => 'the-revision', '_id' => 'B', 'a' => 'b'}}
125
+ ]
126
+ }, @db.documents(:startkey => 'C', :limit => 2, :include_docs => true, :descending => true))
127
+ end
128
+ end
129
+
130
+ context "bulk modifications" do
131
+
132
+ should "accept bulk inserts/updates" do
133
+ RockingChair::Database.stubs(:uuid).returns('foo-id')
134
+
135
+ docs = [{"_id" => 'a', "value" => 1}, {"_id" => 'b', 'value' => 2}, {'value' => 3}]
136
+ assert_equal([
137
+ {'id' => 'a', "rev" => 'the-revision'},
138
+ {'id' => 'b', "rev" => 'the-revision'},
139
+ {'id' => 'foo-id', "rev" => 'the-revision'}
140
+ ], @db.bulk_save(docs))
141
+ end
142
+
143
+ should "handle automatic inserts/updates" do
144
+ @db.bulk_save_cache_limit = 5
145
+ assert_nothing_raised do
146
+ 10.times do |i|
147
+ @db.save_doc({'_id' => "new-item-#{i}", 'content' => 'here'}, true)
148
+ end
149
+ end
150
+ assert_equal 10, @db.info['doc_count']
151
+ end
152
+
153
+ should "do updates" do
154
+ @db.save_doc({'_id' => "A", 'a' => 'b'})
155
+ @db.save_doc({'_id' => "B", 'a' => 'b'})
156
+
157
+ docs = [{"_id" => 'A', "a" => 1, '_rev' => 'the-revision'}, {'value' => 3}]
158
+ @db.bulk_save(docs)
159
+
160
+ assert_equal({'_id' => 'A', 'a' => 1, '_rev' => 'the-revision'}, @db.get('A'))
161
+ end
162
+
163
+ should "do deletes" do
164
+ @db.save_doc({'_id' => "A", 'a' => 'b'})
165
+ @db.save_doc({'_id' => "B", 'a' => 'b'})
166
+
167
+ docs = [{"_id" => 'A', "a" => 1, '_rev' => 'the-revision', '_deleted' => true}]
168
+ @db.bulk_save(docs)
169
+
170
+ assert_raise(RestClient::ResourceNotFound) do
171
+ @db.get('A')
172
+ end
173
+ end
174
+
175
+ end
176
+ end
177
+
178
+ context "Document API" do
179
+ setup do
180
+ @db = @couch.create_db('RockingChair')
181
+ RockingChair::Database.any_instance.stubs(:rev).returns('the-revision')
182
+ end
183
+
184
+ context "when retrieving a document (GET)" do
185
+ setup do
186
+ @db.save_doc({'_id' => 'the-doc-id', 'a' => 'b'})
187
+ end
188
+
189
+ should "load the document" do
190
+ assert_equal({"_id" => "the-doc-id","a" => "b", '_rev' => 'the-revision'}, @db.get('the-doc-id'))
191
+ end
192
+
193
+ should "raise a 404 if the document does not exist" do
194
+ assert_raise(RestClient::ResourceNotFound) do
195
+ @db.get('no-such-id')
196
+ end
197
+ end
198
+
199
+ should "load the document by a specific revision" do
200
+ assert_equal({"_id" => "the-doc-id","a" => "b", '_rev' => 'the-revision'}, @db.get('the-doc-id', :rev => 'the-revision'))
201
+ end
202
+
203
+ should "raise a 404 if the revision is not there" do
204
+ assert_raise(RestClient::ResourceNotFound) do
205
+ @db.get('the-doc-id', :rev => 'non-existant-revision')
206
+ end
207
+ end
208
+
209
+ should "load the document witht the revisions history" do
210
+ assert_equal({"_id" => "the-doc-id","a" => "b", '_rev' => 'the-revision', "_revisions" => {"start" => 1,"ids" => ["the-revision"]}}, @db.get('the-doc-id', :revs => 'true'))
211
+ end
212
+
213
+ should "load the document witht the detailed revisions history" do
214
+ assert_equal({"_id" => "the-doc-id","a" => "b", '_rev' => 'the-revision', "_revs_info" => [{"rev" => "the-revision", "status" => "disk"}]}, @db.get('the-doc-id', :revs_info => 'true'))
215
+ end
216
+ end
217
+
218
+ context "when storing a document (PUT)" do
219
+ should "store a new the document" do
220
+ assert_equal 0, @db.info['doc_count']
221
+ @db.save_doc({'_id' => 'new-item', 'content' => 'here'})
222
+ assert_equal 1, @db.info['doc_count']
223
+ assert_equal({"_id" => "new-item","content" => "here", '_rev' => 'the-revision'}, @db.get('new-item'))
224
+ end
225
+
226
+ should "update a document" do
227
+ seq = sequence('revision')
228
+ RockingChair::Database.any_instance.expects(:rev).returns('first-rev').in_sequence(seq)
229
+ RockingChair::Database.any_instance.expects(:rev).returns('second-rev').in_sequence(seq)
230
+
231
+ @db.save_doc({'_id' => 'new-item', 'content' => 'here'})
232
+ @db.save_doc({'_id' => 'new-item', 'content' => 'better', '_rev' => 'first-rev'})
233
+
234
+ assert_equal({"_id" => "new-item","content" => "better", '_rev' => 'second-rev'}, @db.get('new-item'))
235
+ end
236
+
237
+ should "raise 409 on a revision conflict" do
238
+ @db.save_doc({'_id' => 'new-item', 'content' => 'here'})
239
+ assert_raise(HttpAbstraction::Conflict) do
240
+ @db.save_doc({'_id' => 'new-item', 'content' => 'better', '_rev' => 'wrong-revision'})
241
+ end
242
+
243
+ assert_equal({"_id" => "new-item","content" => "here", '_rev' => 'the-revision'}, @db.get('new-item'))
244
+ end
245
+
246
+ should "ignore the batch parameter" do
247
+ assert_nothing_raised do
248
+ @db.save_doc({'_id' => 'new-item', 'content' => 'here'}, false, true)
249
+ end
250
+ assert_equal({"_id" => "new-item","content" => "here", '_rev' => 'the-revision'}, @db.get('new-item'))
251
+ end
252
+
253
+ end
254
+
255
+ context "when storing a document (POST)" do
256
+ should "store a new the document" do
257
+ RockingChair::Database.expects(:uuid).returns('5')
258
+ assert_equal 0, @db.info['doc_count']
259
+ assert_equal({"rev"=>"the-revision", "id"=>"5", "ok"=>true},
260
+ CouchRest.post('127.0.0.1:5984/RockingChair/', {'content' => 'here'}))
261
+ assert_equal 1, @db.info['doc_count']
262
+ end
263
+ end
264
+
265
+ context "when deleting a document (DELETE)" do
266
+ should "delete if the rev matches" do
267
+ RockingChair::Database.any_instance.stubs(:rev).returns('123')
268
+
269
+ @db.save_doc({'a' => 'b', '_id' => 'delete_me'})
270
+ @db.delete_doc({'a' => 'b', '_id' => 'delete_me', '_rev' => '123'})
271
+ assert_raise(RestClient::ResourceNotFound) do
272
+ @db.get('delete_me')
273
+ end
274
+ end
275
+
276
+ should "fail with conflich if the rev does not matche" do
277
+ @db.save_doc({'a' => 'b', '_id' => 'delete_me'})
278
+ assert_raise(HttpAbstraction::Conflict) do
279
+ @db.delete_doc({'a' => 'b', '_id' => 'delete_me', '_rev' => 'wrong-revision'})
280
+ end
281
+ assert_nothing_raised do
282
+ @db.get('delete_me')
283
+ end
284
+ end
285
+ end
286
+
287
+ context "when copying a document (COPY)" do
288
+ setup do
289
+ RockingChair::Database.any_instance.stubs(:rev).returns('123')
290
+ @db.save_doc({'a' => 'b', '_id' => 'original'})
291
+ end
292
+
293
+ context "when using an destination ID" do
294
+
295
+ should "copy" do
296
+ @db.copy_doc({'a' => 'b', '_id' => 'original', '_rev' => '123'}, 'the_new_id')
297
+ assert_equal({'a' => 'b', '_id' => 'the_new_id', '_rev' => '123'}, @db.get('the_new_id'))
298
+ end
299
+
300
+ should "copy with overwrite with revision" do
301
+ seq = sequence('revision')
302
+ RockingChair::Database.any_instance.expects(:rev).returns('first-rev').in_sequence(seq)
303
+ RockingChair::Database.any_instance.expects(:rev).returns('second-rev').in_sequence(seq)
304
+
305
+ @db.save_doc({'1' => '2', '_id' => 'destination'})
306
+
307
+ @db.copy_doc({'a' => 'b', '_id' => 'original', '_rev' => '123'}, "destination?rev=first-rev")
308
+ assert_equal({'a' => 'b', '_id' => 'destination', '_rev' => 'second-rev'}, @db.get('destination'))
309
+ end
310
+
311
+ should "not copy with overwrite if the revision does not match" do
312
+ @db.save_doc({'1' => '2', '_id' => 'destination'})
313
+ assert_raise(HttpAbstraction::Conflict) do
314
+ @db.copy_doc({'a' => 'b', '_id' => 'original', '_rev' => '123'}, 'destination?rev=not-here')
315
+ end
316
+ end
317
+
318
+ end
319
+
320
+ context "when using a destination object" do
321
+ should "copy" do
322
+ destination = {"_id" => 'destination', 'c' => 'a', '_rev' => '123'}
323
+ @db.copy_doc({'a' => 'b', '_id' => 'original', '_rev' => '123'}, destination)
324
+ assert_equal({'a' => 'b', '_id' => 'destination', '_rev' => '123'}, @db.get('destination'))
325
+ end
326
+
327
+ should "copy with overwrite with revision" do
328
+ seq = sequence('revision')
329
+ RockingChair::Database.any_instance.expects(:rev).returns('first-rev').in_sequence(seq)
330
+ RockingChair::Database.any_instance.expects(:rev).returns('second-rev').in_sequence(seq)
331
+
332
+ @db.save_doc({'1' => '2', '_id' => 'destination'})
333
+
334
+ @db.copy_doc({'a' => 'b', '_id' => 'original', '_rev' => '123'}, {'1' => '2', '_id' => 'destination', '_rev' => 'first-rev'})
335
+ assert_equal({'a' => 'b', '_id' => 'destination', '_rev' => 'second-rev'}, @db.get('destination'))
336
+ end
337
+
338
+ should "not copy with overwrite if the revision does not match" do
339
+ @db.save_doc({'1' => '2', '_id' => 'destination'})
340
+ assert_raise(HttpAbstraction::Conflict) do
341
+ @db.copy_doc({'a' => 'b', '_id' => 'original', '_rev' => '123'}, {'1' => '2', '_id' => 'destination', '_rev' => 'missing'})
342
+ end
343
+ end
344
+ end
345
+ end
346
+
347
+ end
348
+
349
+ end
350
+ end