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