cloudkit 0.10.1 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
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