cloudkit 0.10.1 → 0.11.0

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 (58) hide show
  1. data/CHANGES +11 -0
  2. data/README +7 -6
  3. data/Rakefile +13 -6
  4. data/TODO +7 -5
  5. data/cloudkit.gemspec +23 -23
  6. data/doc/curl.html +2 -2
  7. data/doc/index.html +4 -6
  8. data/examples/5.ru +2 -3
  9. data/examples/TOC +1 -3
  10. data/lib/cloudkit.rb +17 -10
  11. data/lib/cloudkit/constants.rb +0 -6
  12. data/lib/cloudkit/exceptions.rb +10 -0
  13. data/lib/cloudkit/flash_session.rb +1 -3
  14. data/lib/cloudkit/oauth_filter.rb +8 -16
  15. data/lib/cloudkit/oauth_store.rb +9 -13
  16. data/lib/cloudkit/openid_filter.rb +25 -7
  17. data/lib/cloudkit/openid_store.rb +14 -17
  18. data/lib/cloudkit/request.rb +6 -1
  19. data/lib/cloudkit/service.rb +15 -15
  20. data/lib/cloudkit/store.rb +97 -284
  21. data/lib/cloudkit/store/memory_table.rb +105 -0
  22. data/lib/cloudkit/store/resource.rb +256 -0
  23. data/lib/cloudkit/store/response_helpers.rb +0 -1
  24. data/lib/cloudkit/uri.rb +88 -0
  25. data/lib/cloudkit/user_store.rb +7 -14
  26. data/spec/ext_spec.rb +76 -0
  27. data/spec/flash_session_spec.rb +20 -0
  28. data/spec/memory_table_spec.rb +86 -0
  29. data/spec/oauth_filter_spec.rb +326 -0
  30. data/spec/oauth_store_spec.rb +10 -0
  31. data/spec/openid_filter_spec.rb +64 -0
  32. data/spec/openid_store_spec.rb +101 -0
  33. data/spec/rack_builder_spec.rb +39 -0
  34. data/spec/request_spec.rb +185 -0
  35. data/spec/resource_spec.rb +291 -0
  36. data/spec/service_spec.rb +974 -0
  37. data/{test/helper.rb → spec/spec_helper.rb} +14 -2
  38. data/spec/store_spec.rb +10 -0
  39. data/spec/uri_spec.rb +93 -0
  40. data/spec/user_store_spec.rb +10 -0
  41. data/spec/util_spec.rb +11 -0
  42. metadata +37 -61
  43. data/examples/6.ru +0 -10
  44. data/lib/cloudkit/store/adapter.rb +0 -8
  45. data/lib/cloudkit/store/extraction_view.rb +0 -57
  46. data/lib/cloudkit/store/sql_adapter.rb +0 -36
  47. data/test/ext_test.rb +0 -76
  48. data/test/flash_session_test.rb +0 -22
  49. data/test/oauth_filter_test.rb +0 -331
  50. data/test/oauth_store_test.rb +0 -12
  51. data/test/openid_filter_test.rb +0 -60
  52. data/test/openid_store_test.rb +0 -12
  53. data/test/rack_builder_test.rb +0 -41
  54. data/test/request_test.rb +0 -197
  55. data/test/service_test.rb +0 -971
  56. data/test/store_test.rb +0 -93
  57. data/test/user_store_test.rb +0 -12
  58. data/test/util_test.rb +0 -13
@@ -0,0 +1,974 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe "A CloudKit::Service" do
4
+
5
+ it "should return a 501 for unimplemented methods" do
6
+ app = Rack::Builder.new {
7
+ use Rack::Lint
8
+ use CloudKit::Service, :collections => [:items, :things]
9
+ run echo_text('martino')
10
+ }
11
+
12
+ response = Rack::MockRequest.new(app).request('TRACE', '/items')
13
+ response.status.should == 501
14
+
15
+ # disable Rack::Lint so that an invalid HTTP method
16
+ # can be tested
17
+ app = Rack::Builder.new {
18
+ use CloudKit::Service, :collections => [:items, :things]
19
+ run echo_text('nothing')
20
+ }
21
+ response = Rack::MockRequest.new(app).request('REJUXTAPOSE', '/items')
22
+ response.status.should == 501
23
+ end
24
+
25
+ describe "using auth" do
26
+
27
+ before(:each) do
28
+ # mock an authenticated service in pieces
29
+ mock_auth = Proc.new { |env|
30
+ r = CloudKit::Request.new(env)
31
+ r.announce_auth(CLOUDKIT_OAUTH_FILTER_KEY)
32
+ }
33
+ inner_app = echo_text('martino')
34
+ service = CloudKit::Service.new(
35
+ inner_app, :collections => [:items, :things])
36
+ CloudKit.setup_storage_adapter unless CloudKit.storage_adapter
37
+ config = Rack::Config.new(service, &mock_auth)
38
+ authed_service = Rack::Lint.new(config)
39
+ @request = Rack::MockRequest.new(authed_service)
40
+ end
41
+
42
+ after(:each) do
43
+ CloudKit.storage_adapter.clear
44
+ end
45
+
46
+ it "should allow requests for / to pass through" do
47
+ response = @request.get('/')
48
+ response.body.should == 'martino'
49
+ end
50
+
51
+ it "should allow any non-specified resource request to pass through" do
52
+ response = @request.get('/hammers')
53
+ response.body.should == 'martino'
54
+ end
55
+
56
+ it "should return a 500 if authentication is configured incorrectly" do
57
+ # simulate auth requirement without CLOUDKIT_AUTH_KEY being set by the
58
+ # auth filter(s)
59
+ response = @request.get('/items')
60
+ response.status.should == 500
61
+ end
62
+
63
+ describe "on GET /cloudkit-meta" do
64
+
65
+ before(:each) do
66
+ @response = @request.get('/cloudkit-meta', VALID_TEST_AUTH)
67
+ end
68
+
69
+ it "should be successful" do
70
+ @response.status.should == 200
71
+ end
72
+
73
+ it "should return a list of hosted collection URIs" do
74
+ uris = JSON.parse(@response.body)['uris']
75
+ uris.sort.should == ['/items', '/things']
76
+ end
77
+
78
+ it "should return a Content-Type header" do
79
+ @response['Content-Type'].should == 'application/json'
80
+ end
81
+
82
+ it "should return an ETag" do
83
+ @response['ETag'].should_not be_nil
84
+ end
85
+
86
+ it "should not set a Last-Modified header" do
87
+ @response['Last-Modified'].should be_nil
88
+ end
89
+
90
+ end
91
+
92
+ describe "on GET /:collection" do
93
+
94
+ before(:each) do
95
+ 3.times do |i|
96
+ json = JSON.generate(:this => i.to_s)
97
+ @request.put("/items/#{i}", {:input => json}.merge(VALID_TEST_AUTH))
98
+ end
99
+ json = JSON.generate(:this => '4')
100
+ @request.put(
101
+ '/items/4', {:input => json}.merge(CLOUDKIT_AUTH_KEY => 'someoneelse'))
102
+ @response = @request.get(
103
+ '/items', {'HTTP_HOST' => 'example.org'}.merge(VALID_TEST_AUTH))
104
+ @parsed_response = JSON.parse(@response.body)
105
+ end
106
+
107
+ it "should be successful" do
108
+ @response.status.should == 200
109
+ end
110
+
111
+ it "should return a list of URIs for all owner-originated resources" do
112
+ @parsed_response['uris'].sort.should == ['/items/0', '/items/1', '/items/2']
113
+ end
114
+
115
+ it "should sort descending on last_modified date" do
116
+ @parsed_response['uris'].should == ['/items/2', '/items/1', '/items/0']
117
+ end
118
+
119
+ it "should return the total number of uris" do
120
+ @parsed_response['total'].should_not be_nil
121
+ @parsed_response['total'].should == 3
122
+ end
123
+
124
+ it "should return the offset" do
125
+ @parsed_response['offset'].should_not be_nil
126
+ @parsed_response['offset'].should == 0
127
+ end
128
+
129
+ it "should return a Content-Type header" do
130
+ @response['Content-Type'].should == 'application/json'
131
+ end
132
+
133
+ it "should return an ETag" do
134
+ @response['ETag'].should_not be_nil
135
+ end
136
+
137
+ it "should return a Last-Modified date" do
138
+ @response['Last-Modified'].should_not be_nil
139
+ end
140
+
141
+ it "should accept a limit parameter" do
142
+ response = @request.get('/items?limit=2', VALID_TEST_AUTH)
143
+ parsed_response = JSON.parse(response.body)
144
+ parsed_response['uris'].should == ['/items/2', '/items/1']
145
+ parsed_response['total'].should == 3
146
+ end
147
+
148
+ it "should accept an offset parameter" do
149
+ response = @request.get('/items?offset=1', VALID_TEST_AUTH)
150
+ parsed_response = JSON.parse(response.body)
151
+ parsed_response['uris'].should == ['/items/1', '/items/0']
152
+ parsed_response['offset'].should == 1
153
+ parsed_response['total'].should == 3
154
+ end
155
+
156
+ it "should accept combined limit and offset parameters" do
157
+ response = @request.get('/items?limit=1&offset=1', VALID_TEST_AUTH)
158
+ parsed_response = JSON.parse(response.body)
159
+ parsed_response['uris'].should == ['/items/1']
160
+ parsed_response['offset'].should == 1
161
+ parsed_response['total'].should == 3
162
+ end
163
+
164
+ it "should return an empty list if no resources are found" do
165
+ response = @request.get('/things', VALID_TEST_AUTH)
166
+ parsed_response = JSON.parse(response.body)
167
+ parsed_response['uris'].should == []
168
+ parsed_response['total'].should == 0
169
+ parsed_response['offset'].should == 0
170
+ end
171
+
172
+ it "should return a resolved link header" do
173
+ @response['Link'].should_not be_nil
174
+ @response['Link'].match("<http://example.org/items/_resolved>; rel=\"http://joncrosby.me/cloudkit/1.0/rel/resolved\"").should_not be_nil
175
+ end
176
+
177
+ end
178
+
179
+ describe "on GET /:collection/_resolved" do
180
+
181
+ before(:each) do
182
+ 3.times do |i|
183
+ json = JSON.generate(:this => i.to_s)
184
+ @request.put("/items/#{i}", {:input => json}.merge(VALID_TEST_AUTH))
185
+ end
186
+ json = JSON.generate(:this => '4')
187
+ @request.put(
188
+ '/items/4', {:input => json}.merge(CLOUDKIT_AUTH_KEY => 'someoneelse'))
189
+ @response = @request.get(
190
+ '/items/_resolved', {'HTTP_HOST' => 'example.org'}.merge(VALID_TEST_AUTH))
191
+ @parsed_response = JSON.parse(@response.body)
192
+ end
193
+
194
+ it "should be successful" do
195
+ @response.status.should == 200
196
+ end
197
+
198
+ it "should return all owner-originated documents" do
199
+ @parsed_response['documents'].map{|d| d['uri']}.sort.should == ['/items/0', '/items/1', '/items/2']
200
+ end
201
+
202
+ it "should sort descending on last_modified date" do
203
+ @parsed_response['documents'].map{|d| d['uri']}.should == ['/items/2', '/items/1', '/items/0']
204
+ end
205
+
206
+ it "should return the total number of documents" do
207
+ @parsed_response['total'].should_not be_nil
208
+ @parsed_response['total'].should == 3
209
+ end
210
+
211
+ it "should return the offset" do
212
+ @parsed_response['offset'].should_not be_nil
213
+ @parsed_response['offset'].should == 0
214
+ end
215
+
216
+ it "should return a Content-Type header" do
217
+ @response['Content-Type'].should == 'application/json'
218
+ end
219
+
220
+ it "should return an ETag" do
221
+ @response['ETag'].should_not be_nil
222
+ end
223
+
224
+ it "should return a Last-Modified date" do
225
+ @response['Last-Modified'].should_not be_nil
226
+ end
227
+
228
+ it "should accept a limit parameter" do
229
+ response = @request.get('/items/_resolved?limit=2', VALID_TEST_AUTH)
230
+ parsed_response = JSON.parse(response.body)
231
+ parsed_response['documents'].map{|d| d['uri']}.should == ['/items/2', '/items/1']
232
+ parsed_response['total'].should == 3
233
+ end
234
+
235
+ it "should accept an offset parameter" do
236
+ response = @request.get('/items/_resolved?offset=1', VALID_TEST_AUTH)
237
+ parsed_response = JSON.parse(response.body)
238
+ parsed_response['documents'].map{|d| d['uri']}.should == ['/items/1', '/items/0']
239
+ parsed_response['offset'].should == 1
240
+ parsed_response['total'].should == 3
241
+ end
242
+
243
+ it "should accept combined limit and offset parameters" do
244
+ response = @request.get('/items/_resolved?limit=1&offset=1', VALID_TEST_AUTH)
245
+ parsed_response = JSON.parse(response.body)
246
+ parsed_response['documents'].map{|d| d['uri']}.should == ['/items/1']
247
+ parsed_response['offset'].should == 1
248
+ parsed_response['total'].should == 3
249
+ end
250
+
251
+ it "should return an empty list if no documents are found" do
252
+ response = @request.get('/things/_resolved', VALID_TEST_AUTH)
253
+ parsed_response = JSON.parse(response.body)
254
+ parsed_response['documents'].should == []
255
+ parsed_response['total'].should == 0
256
+ parsed_response['offset'].should == 0
257
+ end
258
+
259
+ it "should return an index link header" do
260
+ @response['Link'].should_not be_nil
261
+ @response['Link'].match("<http://example.org/items>; rel=\"index\"").should_not be_nil
262
+ end
263
+
264
+ end
265
+
266
+ describe "on GET /:collection/:id" do
267
+
268
+ before(:each) do
269
+ json = JSON.generate(:this => 'that')
270
+ @request.put('/items/abc', {:input => json}.merge(VALID_TEST_AUTH))
271
+ @response = @request.get(
272
+ '/items/abc', {'HTTP_HOST' => 'example.org'}.merge(VALID_TEST_AUTH))
273
+ end
274
+
275
+ it "should be successful" do
276
+ @response.status.should == 200
277
+ end
278
+
279
+ it "should return a document for valid owner-originated requests" do
280
+ data = JSON.parse(@response.body)
281
+ data['this'].should == 'that'
282
+ end
283
+
284
+ it "should return a 404 if a document does not exist" do
285
+ response = @request.get('/items/nothing', VALID_TEST_AUTH)
286
+ response.status.should == 404
287
+ end
288
+
289
+ it "should return a Content-Type header" do
290
+ @response['Content-Type'].should == 'application/json'
291
+ end
292
+
293
+ it "should return an ETag header" do
294
+ @response['ETag'].should_not be_nil
295
+ end
296
+
297
+ it "should return a Last-Modified header" do
298
+ @response['Last-Modified'].should_not be_nil
299
+ end
300
+
301
+ it "should not return documents for unauthorized users" do
302
+ response = @request.get('/items/abc', CLOUDKIT_AUTH_KEY => 'bogus')
303
+ response.status.should == 404
304
+ end
305
+
306
+ it "should return a versions link header" do
307
+ @response['Link'].should_not be_nil
308
+ @response['Link'].match("<http://example.org/items/abc/versions>; rel=\"http://joncrosby.me/cloudkit/1.0/rel/versions\"").should_not be_nil
309
+ end
310
+
311
+ end
312
+
313
+ describe "on GET /:collection/:id/versions" do
314
+
315
+ before(:each) do
316
+ @etags = []
317
+ 4.times do |i|
318
+ json = JSON.generate(:this => i)
319
+ options = {:input => json}.merge(VALID_TEST_AUTH)
320
+ options.filter_merge!('HTTP_IF_MATCH' => @etags.try(:last))
321
+ result = @request.put('/items/abc', options)
322
+ @etags << JSON.parse(result.body)['etag']
323
+ end
324
+ @response = @request.get(
325
+ '/items/abc/versions', {'HTTP_HOST' => 'example.org'}.merge(VALID_TEST_AUTH))
326
+ @parsed_response = JSON.parse(@response.body)
327
+ end
328
+
329
+ it "should be successful" do
330
+ @response.status.should == 200
331
+ end
332
+
333
+ it "should be successful even if the current resource has been deleted" do
334
+ @request.delete('/items/abc', {'HTTP_IF_MATCH' => @etags.last}.merge(VALID_TEST_AUTH))
335
+ response = @request.get('/items/abc/versions', VALID_TEST_AUTH)
336
+ @response.status.should == 200
337
+ parsed_response = JSON.parse(response.body)
338
+ parsed_response['uris'].size.should == 4
339
+ end
340
+
341
+ it "should return a list of URIs for all versions of a resource" do
342
+ uris = @parsed_response['uris']
343
+ uris.should_not be_nil
344
+ uris.size.should == 4
345
+ end
346
+
347
+ it "should return a 404 if the resource does not exist" do
348
+ response = @request.get('/items/nothing/versions', VALID_TEST_AUTH)
349
+ response.status.should == 404
350
+ end
351
+
352
+ it "should return a 404 for non-owner-originated requests" do
353
+ response = @request.get(
354
+ '/items/abc/versions', CLOUDKIT_AUTH_KEY => 'someoneelse')
355
+ response.status.should == 404
356
+ end
357
+
358
+ it "should sort descending on last_modified date" do
359
+ @parsed_response['uris'].should == ['/items/abc'].concat(@etags[0..-2].reverse.map{|e| "/items/abc/versions/#{e}"})
360
+ end
361
+
362
+ it "should return the total number of uris" do
363
+ @parsed_response['total'].should_not be_nil
364
+ @parsed_response['total'].should == 4
365
+ end
366
+
367
+ it "should return the offset" do
368
+ @parsed_response['offset'].should_not be_nil
369
+ @parsed_response['offset'].should == 0
370
+ end
371
+
372
+ it "should return a Content-Type header" do
373
+ @response['Content-Type'].should == 'application/json'
374
+ end
375
+
376
+ it "should return an ETag" do
377
+ @response['ETag'].should_not be_nil
378
+ end
379
+
380
+ it "should return a Last-Modified date" do
381
+ @response['Last-Modified'].should_not be_nil
382
+ end
383
+
384
+ it "should accept a limit parameter" do
385
+ response = @request.get('/items/abc/versions?limit=2', VALID_TEST_AUTH)
386
+ parsed_response = JSON.parse(response.body)
387
+ parsed_response['uris'].should == ['/items/abc', "/items/abc/versions/#{@etags[-2]}"]
388
+ parsed_response['total'].should == 4
389
+ end
390
+
391
+ it "should accept an offset parameter" do
392
+ response = @request.get('/items/abc/versions?offset=1', VALID_TEST_AUTH)
393
+ parsed_response = JSON.parse(response.body)
394
+ parsed_response['uris'].should == @etags.reverse[1..-1].map{|e| "/items/abc/versions/#{e}"}
395
+ parsed_response['offset'].should == 1
396
+ parsed_response['total'].should == 4
397
+ end
398
+
399
+ it "should accept combined limit and offset parameters" do
400
+ response = @request.get('/items/abc/versions?limit=1&offset=1', VALID_TEST_AUTH)
401
+ parsed_response = JSON.parse(response.body)
402
+ parsed_response['uris'].should == ["/items/abc/versions/#{@etags[-2]}"]
403
+ parsed_response['offset'].should == 1
404
+ parsed_response['total'].should == 4
405
+ end
406
+
407
+ it "should return a resolved link header" do
408
+ @response['Link'].should_not be_nil
409
+ @response['Link'].match("<http://example.org/items/abc/versions/_resolved>; rel=\"http://joncrosby.me/cloudkit/1.0/rel/resolved\"").should_not be_nil
410
+ end
411
+
412
+ end
413
+
414
+ describe "on GET /:collections/:id/versions/_resolved" do
415
+
416
+ before(:each) do
417
+ @etags = []
418
+ 4.times do |i|
419
+ json = JSON.generate(:this => i)
420
+ options = {:input => json}.merge(VALID_TEST_AUTH)
421
+ options.filter_merge!('HTTP_IF_MATCH' => @etags.try(:last))
422
+ result = @request.put('/items/abc', options)
423
+ @etags << JSON.parse(result.body)['etag']
424
+ end
425
+ @response = @request.get(
426
+ '/items/abc/versions/_resolved', {'HTTP_HOST' => 'example.org'}.merge(VALID_TEST_AUTH))
427
+ @parsed_response = JSON.parse(@response.body)
428
+ end
429
+
430
+ it "should be successful" do
431
+ @response.status.should == 200
432
+ end
433
+
434
+ it "should be successful even if the current resource has been deleted" do
435
+ @request.delete(
436
+ '/items/abc', {'HTTP_IF_MATCH' => @etags.last}.merge(VALID_TEST_AUTH))
437
+ response = @request.get('/items/abc/versions/_resolved', VALID_TEST_AUTH)
438
+ @response.status.should == 200
439
+ parsed_response = JSON.parse(response.body)
440
+ parsed_response['documents'].size.should == 4
441
+ end
442
+
443
+ it "should return all versions of a document" do
444
+ documents = @parsed_response['documents']
445
+ documents.should_not be_nil
446
+ documents.size.should == 4
447
+ end
448
+
449
+ it "should return a 404 if the resource does not exist" do
450
+ response = @request.get('/items/nothing/versions/_resolved', VALID_TEST_AUTH)
451
+ response.status.should == 404
452
+ end
453
+
454
+ it "should return a 404 for non-owner-originated requests" do
455
+ response = @request.get('/items/abc/versions/_resolved', CLOUDKIT_AUTH_KEY => 'someoneelse')
456
+ response.status.should == 404
457
+ end
458
+
459
+ it "should sort descending on last_modified date" do
460
+ @parsed_response['documents'].map{|d| d['uri']}.should == ['/items/abc'].concat(@etags[0..-2].reverse.map{|e| "/items/abc/versions/#{e}"})
461
+ end
462
+
463
+ it "should return the total number of documents" do
464
+ @parsed_response['total'].should_not be_nil
465
+ @parsed_response['total'].should == 4
466
+ end
467
+
468
+ it "should return the offset" do
469
+ @parsed_response['offset'].should_not be_nil
470
+ @parsed_response['offset'].should == 0
471
+ end
472
+
473
+ it "should return a Content-Type header" do
474
+ @response['Content-Type'].should == 'application/json'
475
+ end
476
+
477
+ it "should return an ETag" do
478
+ @response['ETag'].should_not be_nil
479
+ end
480
+
481
+ it "should return a Last-Modified date" do
482
+ @response['Last-Modified'].should_not be_nil
483
+ end
484
+
485
+ it "should accept a limit parameter" do
486
+ response = @request.get(
487
+ '/items/abc/versions/_resolved?limit=2', VALID_TEST_AUTH)
488
+ parsed_response = JSON.parse(response.body)
489
+ parsed_response['documents'].map{|d| d['uri']}.should == ['/items/abc', "/items/abc/versions/#{@etags[-2]}"]
490
+ parsed_response['total'].should == 4
491
+ end
492
+
493
+ it "should accept an offset parameter" do
494
+ response = @request.get(
495
+ '/items/abc/versions/_resolved?offset=1', VALID_TEST_AUTH)
496
+ parsed_response = JSON.parse(response.body)
497
+ parsed_response['documents'].map{|d| d['uri']}.should == @etags.reverse[1..-1].map{|e| "/items/abc/versions/#{e}"}
498
+ parsed_response['offset'].should == 1
499
+ parsed_response['total'].should == 4
500
+ end
501
+
502
+ it "should accept combined limit and offset parameters" do
503
+ response = @request.get(
504
+ '/items/abc/versions/_resolved?limit=1&offset=1', VALID_TEST_AUTH)
505
+ parsed_response = JSON.parse(response.body)
506
+ parsed_response['documents'].map{|d| d['uri']}.should == ["/items/abc/versions/#{@etags[-2]}"]
507
+ parsed_response['offset'].should == 1
508
+ parsed_response['total'].should == 4
509
+ end
510
+
511
+ it "should return an index link header" do
512
+ @response['Link'].should_not be_nil
513
+ @response['Link'].match("<http://example.org/items/abc/versions>; rel=\"index\"").should_not be_nil
514
+ end
515
+
516
+ end
517
+
518
+ describe "on GET /:collection/:id/versions/:etag" do
519
+
520
+ before(:each) do
521
+ @etags = []
522
+ 2.times do |i|
523
+ json = JSON.generate(:this => i)
524
+ options = {:input => json}.merge(VALID_TEST_AUTH)
525
+ options.filter_merge!('HTTP_IF_MATCH' => @etags.try(:last))
526
+ result = @request.put('/items/abc', options)
527
+ @etags << JSON.parse(result.body)['etag']
528
+ end
529
+ @response = @request.get(
530
+ "/items/abc/versions/#{@etags.first}", VALID_TEST_AUTH)
531
+ @parsed_response = JSON.parse(@response.body)
532
+ end
533
+
534
+ it "should be successful" do
535
+ @response.status.should == 200
536
+ end
537
+
538
+ it "should return a document for valid owner-originated requests" do
539
+ @parsed_response['this'].should == 0
540
+ end
541
+
542
+ it "should return a 404 if a document is not found" do
543
+ response = @request.get(
544
+ "/items/nothing/versions/#{@etags.first}", VALID_TEST_AUTH)
545
+ response.status.should == 404
546
+ end
547
+
548
+ it "should return a Content-Type header" do
549
+ @response['Content-Type'].should == 'application/json'
550
+ end
551
+
552
+ it "should return an ETag header" do
553
+ @response['ETag'].should_not be_nil
554
+ end
555
+
556
+ it "should return a Last-Modified header" do
557
+ @response['Last-Modified'].should_not be_nil
558
+ end
559
+
560
+ it "should not return documents for unauthorized users" do
561
+ response = @request.get(
562
+ "/items/abc/versions/#{@etags.first}", CLOUDKIT_AUTH_KEY => 'someoneelse')
563
+ response.status.should == 404
564
+ end
565
+
566
+ end
567
+
568
+ describe "on POST /:collection" do
569
+
570
+ before(:each) do
571
+ json = JSON.generate(:this => 'that')
572
+ @response = @request.post(
573
+ '/items', {:input => json}.merge(VALID_TEST_AUTH))
574
+ @body = JSON.parse(@response.body)
575
+ end
576
+
577
+ it "should store the document" do
578
+ result = @request.get(@body['uri'], VALID_TEST_AUTH)
579
+ result.status.should == 200
580
+ end
581
+
582
+ it "should return a 201 when successful" do
583
+ @response.status.should == 201
584
+ end
585
+
586
+ it "should return the metadata" do
587
+ @body.keys.size.should == 4
588
+ @body.keys.sort.should == ['etag', 'last_modified', 'ok', 'uri']
589
+ end
590
+
591
+ it "should set the Content-Type header" do
592
+ @response['Content-Type'].should == 'application/json'
593
+ end
594
+
595
+ it "should not set an ETag header" do
596
+ @response['ETag'].should be_nil
597
+ end
598
+
599
+ it "should not set a Last-Modified header" do
600
+ @response['Last-Modified'].should be_nil
601
+ end
602
+
603
+ it "should return a 422 if parsing fails" do
604
+ response = @request.post('/items', {:input => 'fail'}.merge(VALID_TEST_AUTH))
605
+ response.status.should == 422
606
+ end
607
+
608
+ end
609
+
610
+ describe "on PUT /:collection/:id" do
611
+
612
+ before(:each) do
613
+ json = JSON.generate(:this => 'that')
614
+ @original = @request.put(
615
+ '/items/abc', {:input => json}.merge(VALID_TEST_AUTH))
616
+ etag = JSON.parse(@original.body)['etag']
617
+ json = JSON.generate(:this => 'other')
618
+ @response = @request.put(
619
+ '/items/abc',
620
+ :input => json,
621
+ 'HTTP_IF_MATCH' => etag,
622
+ CLOUDKIT_AUTH_KEY => TEST_REMOTE_USER)
623
+ @json = JSON.parse(@response.body)
624
+ end
625
+
626
+ it "should create a document if it does not already exist" do
627
+ json = JSON.generate(:this => 'thing')
628
+ response = @request.put(
629
+ '/items/xyz', {:input => json}.merge(VALID_TEST_AUTH))
630
+ response.status.should == 201
631
+ result = @request.get('/items/xyz', VALID_TEST_AUTH)
632
+ result.status.should == 200
633
+ JSON.parse(result.body)['this'].should == 'thing'
634
+ end
635
+
636
+ it "should not create new resources using deleted resource URIs" do
637
+ # This situation occurs when a stale client attempts to update
638
+ # a resource that has been removed. This test verifies that CloudKit
639
+ # does not attempt to create a new item with a URI equal to the
640
+ # removed item.
641
+ etag = JSON.parse(@response.body)['etag'];
642
+ @request.delete(
643
+ '/items/abc',
644
+ 'HTTP_IF_MATCH' => etag,
645
+ CLOUDKIT_AUTH_KEY => TEST_REMOTE_USER)
646
+ json = JSON.generate(:foo => 'bar')
647
+ response = @request.put(
648
+ '/items/abc',
649
+ :input => json,
650
+ 'HTTP_IF_MATCH' => etag,
651
+ CLOUDKIT_AUTH_KEY => TEST_REMOTE_USER)
652
+ response.status.should == 410
653
+ end
654
+
655
+ it "should update the document if it already exists" do
656
+ @response.status.should == 200
657
+ result = @request.get('/items/abc', VALID_TEST_AUTH)
658
+ JSON.parse(result.body)['this'].should == 'other'
659
+ end
660
+
661
+ it "should return the metadata" do
662
+ @json.keys.size.should == 4
663
+ @json.keys.sort.should == ['etag', 'last_modified', 'ok', 'uri']
664
+ end
665
+
666
+ it "should set the Content-Type header" do
667
+ @response['Content-Type'].should == 'application/json'
668
+ end
669
+
670
+ it "should not set an ETag header" do
671
+ @response['ETag'].should be_nil
672
+ end
673
+
674
+ it "should not set a Last-Modified header" do
675
+ @response['Last-Modified'].should be_nil
676
+ end
677
+
678
+ it "should not allow a remote_user change" do
679
+ json = JSON.generate(:this => 'other')
680
+ response = @request.put(
681
+ '/items/abc',
682
+ :input => json,
683
+ 'HTTP_IF_MATCH' => @json['etag'],
684
+ CLOUDKIT_AUTH_KEY => 'someone_else')
685
+ response.status.should == 404
686
+ end
687
+
688
+ it "should detect and return conflicts" do
689
+ client_a_input = JSON.generate(:this => 'updated')
690
+ client_b_input = JSON.generate(:other => 'thing')
691
+ response = @request.put(
692
+ '/items/abc',
693
+ :input => client_a_input,
694
+ 'HTTP_IF_MATCH' => @json['etag'],
695
+ CLOUDKIT_AUTH_KEY => TEST_REMOTE_USER)
696
+ response.status.should == 200
697
+ response = @request.put(
698
+ '/items/abc',
699
+ :input => client_b_input,
700
+ 'HTTP_IF_MATCH' => @json['etag'],
701
+ CLOUDKIT_AUTH_KEY => TEST_REMOTE_USER)
702
+ response.status.should == 412
703
+ end
704
+
705
+ it "should require an ETag for updates" do
706
+ json = JSON.generate(:this => 'updated')
707
+ response = @request.put(
708
+ '/items/abc',
709
+ {:input => json}.merge(VALID_TEST_AUTH))
710
+ response.status.should == 400
711
+ end
712
+
713
+ it "should return a 422 if parsing fails" do
714
+ response = @request.put(
715
+ '/items/zzz', {:input => 'fail'}.merge(VALID_TEST_AUTH))
716
+ response.status.should == 422
717
+ end
718
+
719
+ it "should version document updates" do
720
+ json = JSON.generate(:this => 'updated')
721
+ response = @request.put(
722
+ '/items/abc',
723
+ :input => json,
724
+ 'HTTP_IF_MATCH' => @json['etag'],
725
+ CLOUDKIT_AUTH_KEY => TEST_REMOTE_USER)
726
+ response.status.should == 200
727
+ etag = JSON.parse(response.body)['etag']
728
+ json = JSON.generate(:this => 'updated again')
729
+ new_response = @request.put(
730
+ '/items/abc',
731
+ :input => json,
732
+ 'HTTP_IF_MATCH' => etag,
733
+ CLOUDKIT_AUTH_KEY => TEST_REMOTE_USER)
734
+ new_response.status.should == 200
735
+ new_etag = JSON.parse(new_response.body)['etag']
736
+ new_etag.should_not == etag
737
+ end
738
+
739
+ end
740
+
741
+ describe "on DELETE /:collection/:id" do
742
+
743
+ before(:each) do
744
+ json = JSON.generate(:this => 'that')
745
+ @result = @request.put(
746
+ '/items/abc', {:input => json}.merge(VALID_TEST_AUTH))
747
+ @etag = JSON.parse(@result.body)['etag']
748
+ end
749
+
750
+ it "should delete the document" do
751
+ response = @request.delete(
752
+ '/items/abc',
753
+ 'HTTP_IF_MATCH' => @etag,
754
+ CLOUDKIT_AUTH_KEY => TEST_REMOTE_USER)
755
+ response.status.should == 200
756
+ result = @request.get('/items/abc', VALID_TEST_AUTH)
757
+ result.status.should == 410
758
+ end
759
+
760
+ it "should return the metadata" do
761
+ response = @request.delete(
762
+ '/items/abc',
763
+ 'HTTP_IF_MATCH' => @etag,
764
+ CLOUDKIT_AUTH_KEY => TEST_REMOTE_USER)
765
+ json = JSON.parse(response.body)
766
+ json.keys.size.should == 4
767
+ json.keys.sort.should == ['etag', 'last_modified', 'ok', 'uri']
768
+ end
769
+
770
+ it "should set the Content-Type header" do
771
+ response = @request.delete(
772
+ '/items/abc',
773
+ 'HTTP_IF_MATCH' => @etag,
774
+ CLOUDKIT_AUTH_KEY => TEST_REMOTE_USER)
775
+ response['Content-Type'].should == 'application/json'
776
+ end
777
+
778
+ it "should not set an ETag header" do
779
+ response = @request.delete(
780
+ '/items/abc',
781
+ 'HTTP_IF_MATCH' => @etag,
782
+ CLOUDKIT_AUTH_KEY => TEST_REMOTE_USER)
783
+ response['ETag'].should be_nil
784
+ end
785
+
786
+ it "should not set a Last-Modified header" do
787
+ response = @request.delete(
788
+ '/items/abc',
789
+ 'HTTP_IF_MATCH' => @etag,
790
+ CLOUDKIT_AUTH_KEY => TEST_REMOTE_USER)
791
+ response['Last-Modified'].should be_nil
792
+ end
793
+
794
+ it "should return a 404 for items that have never existed" do
795
+ response = @request.delete(
796
+ '/items/zzz',
797
+ 'HTTP_IF_MATCH' => @etag,
798
+ CLOUDKIT_AUTH_KEY => TEST_REMOTE_USER)
799
+ response.status.should == 404
800
+ end
801
+
802
+ it "should require an ETag" do
803
+ response = @request.delete(
804
+ '/items/abc',
805
+ VALID_TEST_AUTH)
806
+ response.status.should == 400
807
+ end
808
+
809
+ it "should verify the user in the doc" do
810
+ response = @request.delete(
811
+ '/items/abc',
812
+ 'HTTP_IF_MATCH' => @etag,
813
+ CLOUDKIT_AUTH_KEY => 'someoneelse')
814
+ response.status.should == 404
815
+ end
816
+
817
+ it "should detect and return conflicts" do
818
+ json = JSON.generate(:this => 'that')
819
+ result = @request.put(
820
+ '/items/123', {:input => json}.merge(VALID_TEST_AUTH))
821
+ etag = JSON.parse(result.body)['etag']
822
+ client_a_input = JSON.generate(:this => 'updated')
823
+ client_b_input = JSON.generate(:other => 'thing')
824
+ response = @request.put(
825
+ '/items/123',
826
+ :input => client_a_input,
827
+ 'HTTP_IF_MATCH' => etag,
828
+ CLOUDKIT_AUTH_KEY => TEST_REMOTE_USER)
829
+ response.status.should == 200
830
+ response = @request.delete(
831
+ '/items/123',
832
+ :input => client_b_input,
833
+ 'HTTP_IF_MATCH' => etag,
834
+ CLOUDKIT_AUTH_KEY => TEST_REMOTE_USER)
835
+ response.status.should == 412
836
+ end
837
+
838
+ it "should retain version history" do
839
+ response = @request.delete(
840
+ '/items/abc',
841
+ 'HTTP_IF_MATCH' => @etag,
842
+ CLOUDKIT_AUTH_KEY => TEST_REMOTE_USER)
843
+ response.status.should == 200
844
+ response = @request.get(
845
+ '/items/abc/versions',
846
+ VALID_TEST_AUTH)
847
+ json = JSON.parse(response.body)
848
+ json['total'].should == 1
849
+ end
850
+
851
+ end
852
+
853
+ describe "on OPTIONS /:collection" do
854
+
855
+ before(:each) do
856
+ @response = @request.request('OPTIONS', '/items', VALID_TEST_AUTH)
857
+ end
858
+
859
+ it "should return a 200 status" do
860
+ @response.status.should == 200
861
+ end
862
+
863
+ it "should return a list of available methods" do
864
+ @response['Allow'].should_not be_nil
865
+ methods = @response['Allow'].split(', ')
866
+ methods.sort.should == ['GET', 'HEAD', 'OPTIONS', 'POST']
867
+ end
868
+
869
+ end
870
+
871
+ describe "on OPTIONS /:collection/_resolved" do
872
+
873
+ before(:each) do
874
+ @response = @request.request('OPTIONS', '/items/_resolved', VALID_TEST_AUTH)
875
+ end
876
+
877
+ it "should return a 200 status" do
878
+ @response.status.should == 200
879
+ end
880
+
881
+ it "should return a list of available methods" do
882
+ @response['Allow'].should_not be_nil
883
+ methods = @response['Allow'].split(', ')
884
+ methods.sort.should == ['GET', 'HEAD', 'OPTIONS']
885
+ end
886
+
887
+ end
888
+
889
+ describe "on OPTIONS /:collection/:id" do
890
+
891
+ before(:each) do
892
+ @response = @request.request('OPTIONS', '/items/xyz', VALID_TEST_AUTH)
893
+ end
894
+
895
+ it "should return a 200 status" do
896
+ @response.status.should == 200
897
+ end
898
+
899
+ it "should return a list of available methods" do
900
+ @response['Allow'].should_not be_nil
901
+ methods = @response['Allow'].split(', ')
902
+ methods.sort.should == ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'PUT']
903
+ end
904
+
905
+ end
906
+
907
+ describe "on OPTIONS /:collection/:id/versions" do
908
+
909
+ before(:each) do
910
+ @response = @request.request('OPTIONS', '/items/xyz/versions', VALID_TEST_AUTH)
911
+ end
912
+
913
+ it "should return a 200 status" do
914
+ @response.status.should == 200
915
+ end
916
+
917
+ it "should return a list of available methods" do
918
+ @response['Allow'].should_not be_nil
919
+ methods = @response['Allow'].split(', ')
920
+ methods.sort.should == ['GET', 'HEAD', 'OPTIONS']
921
+ end
922
+
923
+ end
924
+
925
+ describe "on OPTIONS /:collection/:id/versions/_resolved" do
926
+
927
+ before(:each) do
928
+ @response = @request.request('OPTIONS', '/items/xyz/versions/_resolved', VALID_TEST_AUTH)
929
+ end
930
+
931
+ it "should return a 200 status" do
932
+ @response.status.should == 200
933
+ end
934
+
935
+ it "should return a list of available methods" do
936
+ @response['Allow'].should_not be_nil
937
+ methods = @response['Allow'].split(', ')
938
+ methods.sort.should == ['GET', 'HEAD', 'OPTIONS']
939
+ end
940
+
941
+ end
942
+
943
+ describe "on OPTIONS /:collection/:id/versions/:etag" do
944
+
945
+ before(:each) do
946
+ @response = @request.request('OPTIONS', "/items/xyz/versions/abc", VALID_TEST_AUTH)
947
+ end
948
+
949
+ it "should return a 200 status" do
950
+ @response.status.should == 200
951
+ end
952
+
953
+ it "should return a list of available methods" do
954
+ @response['Allow'].should_not be_nil
955
+ methods = @response['Allow'].split(', ')
956
+ methods.sort.should == ['GET', 'HEAD', 'OPTIONS']
957
+ end
958
+
959
+ end
960
+
961
+ describe "on HEAD" do
962
+
963
+ it "should return an empty body" do
964
+ json = JSON.generate(:this => 'that')
965
+ @request.put('/items/abc', {:input => json}.merge(VALID_TEST_AUTH))
966
+ response = @request.request('HEAD', '/items/abc', VALID_TEST_AUTH)
967
+ response.body.should == ''
968
+ response = @request.request('HEAD', '/items', VALID_TEST_AUTH)
969
+ response.body.should == ''
970
+ end
971
+
972
+ end
973
+ end
974
+ end