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
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