cloudkit 0.9.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.
- data/CHANGES +2 -0
- data/COPYING +20 -0
- data/README +55 -0
- data/Rakefile +35 -0
- data/TODO +22 -0
- data/cloudkit.gemspec +82 -0
- data/doc/curl.html +329 -0
- data/doc/images/example-code.gif +0 -0
- data/doc/images/json-title.gif +0 -0
- data/doc/images/oauth-discovery-logo.gif +0 -0
- data/doc/images/openid-logo.gif +0 -0
- data/doc/index.html +87 -0
- data/doc/main.css +151 -0
- data/doc/rest-api.html +358 -0
- data/examples/1.ru +3 -0
- data/examples/2.ru +3 -0
- data/examples/3.ru +6 -0
- data/examples/4.ru +5 -0
- data/examples/5.ru +10 -0
- data/examples/6.ru +10 -0
- data/examples/TOC +17 -0
- data/lib/cloudkit.rb +74 -0
- data/lib/cloudkit/flash_session.rb +22 -0
- data/lib/cloudkit/oauth_filter.rb +273 -0
- data/lib/cloudkit/oauth_store.rb +56 -0
- data/lib/cloudkit/openid_filter.rb +198 -0
- data/lib/cloudkit/openid_store.rb +101 -0
- data/lib/cloudkit/rack/builder.rb +120 -0
- data/lib/cloudkit/rack/router.rb +20 -0
- data/lib/cloudkit/request.rb +159 -0
- data/lib/cloudkit/service.rb +135 -0
- data/lib/cloudkit/store.rb +459 -0
- data/lib/cloudkit/store/adapter.rb +9 -0
- data/lib/cloudkit/store/extraction_view.rb +57 -0
- data/lib/cloudkit/store/response.rb +51 -0
- data/lib/cloudkit/store/response_helpers.rb +72 -0
- data/lib/cloudkit/store/sql_adapter.rb +36 -0
- data/lib/cloudkit/templates/authorize_request_token.erb +19 -0
- data/lib/cloudkit/templates/oauth_descriptor.erb +43 -0
- data/lib/cloudkit/templates/oauth_meta.erb +8 -0
- data/lib/cloudkit/templates/openid_login.erb +31 -0
- data/lib/cloudkit/templates/request_authorization.erb +23 -0
- data/lib/cloudkit/templates/request_token_denied.erb +18 -0
- data/lib/cloudkit/user_store.rb +44 -0
- data/lib/cloudkit/util.rb +60 -0
- data/test/ext_test.rb +57 -0
- data/test/flash_session_test.rb +22 -0
- data/test/helper.rb +50 -0
- data/test/oauth_filter_test.rb +331 -0
- data/test/oauth_store_test.rb +12 -0
- data/test/openid_filter_test.rb +54 -0
- data/test/openid_store_test.rb +12 -0
- data/test/rack_builder_test.rb +41 -0
- data/test/request_test.rb +197 -0
- data/test/service_test.rb +718 -0
- data/test/store_test.rb +99 -0
- data/test/user_store_test.rb +12 -0
- data/test/util_test.rb +13 -0
- metadata +190 -0
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'helper'
|
2
|
+
class OpenIDFilterTest < Test::Unit::TestCase
|
3
|
+
|
4
|
+
context "An OpenIDFilter" do
|
5
|
+
|
6
|
+
setup do
|
7
|
+
@request = Rack::MockRequest.new(openid_app)
|
8
|
+
end
|
9
|
+
|
10
|
+
should "allow root url pass through" do
|
11
|
+
response = @request.get('/')
|
12
|
+
assert_equal 200, response.status
|
13
|
+
end
|
14
|
+
|
15
|
+
should "redirect to the login page if authorization is required" do
|
16
|
+
response = @request.get('/protected')
|
17
|
+
assert_equal 302, response.status
|
18
|
+
assert_equal '/login', response['Location']
|
19
|
+
end
|
20
|
+
|
21
|
+
should "notify downstream nodes of its presence" do
|
22
|
+
app = Rack::Builder.new do
|
23
|
+
use Rack::Session::Pool
|
24
|
+
use CloudKit::OpenIDFilter
|
25
|
+
run echo_env('cloudkit.via')
|
26
|
+
end
|
27
|
+
response = Rack::MockRequest.new(app).get('/')
|
28
|
+
assert_equal 'cloudkit.filter.openid', response.body
|
29
|
+
end
|
30
|
+
|
31
|
+
context "with upstream authorization middleware" do
|
32
|
+
|
33
|
+
should "allow pass through if the auth env variable is populated" do
|
34
|
+
response = @request.get('/protected', auth)
|
35
|
+
assert_equal 200, response.status
|
36
|
+
assert_equal remote_user, response.body
|
37
|
+
end
|
38
|
+
|
39
|
+
should "return the auth challenge header" do
|
40
|
+
response = @request.get('/protected',
|
41
|
+
'cloudkit.via' => 'cloudkit.filter.oauth',
|
42
|
+
'cloudkit.challenge' => {'WWW-Authenticate' => 'etc.'})
|
43
|
+
assert response['WWW-Authenticate']
|
44
|
+
end
|
45
|
+
|
46
|
+
should "return a 401 status if authorization is required" do
|
47
|
+
response = @request.get('/protected',
|
48
|
+
'cloudkit.via' => 'cloudkit.filter.oauth',
|
49
|
+
'cloudkit.challenge' => {'WWW-Authenticate' => 'etc.'})
|
50
|
+
assert_equal 401, response.status
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'helper'
|
2
|
+
class RackBuilderTest < Test::Unit::TestCase
|
3
|
+
|
4
|
+
context "Rack::Builder" do
|
5
|
+
|
6
|
+
should "expose services" do
|
7
|
+
app = Rack::Builder.new do
|
8
|
+
expose :items, :things
|
9
|
+
run lambda {|app| [200, {}, ['hello']]}
|
10
|
+
end
|
11
|
+
response = Rack::MockRequest.new(app).get('/items')
|
12
|
+
assert_equal 200, response.status
|
13
|
+
documents = JSON.parse(response.body)['uris']
|
14
|
+
assert_equal [], documents
|
15
|
+
end
|
16
|
+
|
17
|
+
should "expose services with auth using 'contain'" do
|
18
|
+
app = Rack::Builder.new do
|
19
|
+
contain :items, :things
|
20
|
+
run lambda {|app| [200, {}, ['hello']]}
|
21
|
+
end
|
22
|
+
response = Rack::MockRequest.new(app).get('/items')
|
23
|
+
assert_equal 401, response.status
|
24
|
+
response = Rack::MockRequest.new(app).get('/things')
|
25
|
+
assert_equal 401, response.status
|
26
|
+
response = Rack::MockRequest.new(app).get('/')
|
27
|
+
assert_equal 200, response.status
|
28
|
+
assert_equal 'hello', response.body
|
29
|
+
end
|
30
|
+
|
31
|
+
should "insert a default app if one does not exist" do
|
32
|
+
app = Rack::Builder.new { contain :items }
|
33
|
+
response = Rack::MockRequest.new(app).get('/items')
|
34
|
+
assert_equal 401, response.status
|
35
|
+
response = Rack::MockRequest.new(app).get('/')
|
36
|
+
assert_equal 200, response.status
|
37
|
+
assert response.body.match('CloudKit')
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,197 @@
|
|
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.filter.oauth')
|
72
|
+
assert request.via.include?('cloudkit.filter.oauth')
|
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.filter.oauth')
|
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.user' => '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.filter.openid.url.login' => '/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.filter.openid.url.logout' => '/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
|
@@ -0,0 +1,718 @@
|
|
1
|
+
require 'helper'
|
2
|
+
class ServiceTest < Test::Unit::TestCase
|
3
|
+
|
4
|
+
context "A CloudKit::Service" do
|
5
|
+
|
6
|
+
setup do
|
7
|
+
@request = Rack::MockRequest.new(plain_service)
|
8
|
+
end
|
9
|
+
|
10
|
+
teardown do
|
11
|
+
FileUtils.rm_f('service.db')
|
12
|
+
end
|
13
|
+
|
14
|
+
should "return a 501 for unimplemented methods" do
|
15
|
+
response = @request.request('TRACE', '/items')
|
16
|
+
assert_equal 501, response.status
|
17
|
+
response = @request.request('REJUXTAPOSE', '/items')
|
18
|
+
assert_equal 501, response.status
|
19
|
+
end
|
20
|
+
|
21
|
+
context "using auth" do
|
22
|
+
|
23
|
+
setup do
|
24
|
+
@store = CloudKit::Store.new(
|
25
|
+
:adapter => CloudKit::SQLAdapter.new('sqlite://service.db'),
|
26
|
+
:collections => [:items, :things])
|
27
|
+
@request = Rack::MockRequest.new(authed_service)
|
28
|
+
end
|
29
|
+
|
30
|
+
should "allow requests for / to pass through" do
|
31
|
+
response = @request.get('/')
|
32
|
+
assert_equal 'martino', response.body
|
33
|
+
end
|
34
|
+
|
35
|
+
should "allow any non-specified resource request to pass through" do
|
36
|
+
response = @request.get('/hammers')
|
37
|
+
assert_equal 'martino', response.body
|
38
|
+
end
|
39
|
+
|
40
|
+
should "return a 500 if authentication is configured incorrectly" do
|
41
|
+
# simulate auth requirement without the auth_key being set by the
|
42
|
+
# auth filter(s)
|
43
|
+
response = @request.get('/items')
|
44
|
+
assert_equal 500, response.status
|
45
|
+
end
|
46
|
+
|
47
|
+
context "on GET /cloudkit-meta" do
|
48
|
+
|
49
|
+
setup do
|
50
|
+
@response = @request.get('/cloudkit-meta', auth_key => remote_user)
|
51
|
+
end
|
52
|
+
|
53
|
+
should "be successful" do
|
54
|
+
assert_equal 200, @response.status
|
55
|
+
end
|
56
|
+
|
57
|
+
should "return a list of hosted collection URIs" do
|
58
|
+
uris = JSON.parse(@response.body)['uris']
|
59
|
+
assert_same_elements ['/things', '/items'], uris
|
60
|
+
end
|
61
|
+
|
62
|
+
should "return a Content-Type header" do
|
63
|
+
assert_equal 'application/json', @response['Content-Type']
|
64
|
+
end
|
65
|
+
|
66
|
+
should "return an ETag" do
|
67
|
+
assert @response['ETag']
|
68
|
+
end
|
69
|
+
|
70
|
+
should "not set a Last-Modified header" do
|
71
|
+
assert_nil @response['Last-Modified']
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context "on GET /:collection" do
|
76
|
+
|
77
|
+
setup do
|
78
|
+
3.times do |i|
|
79
|
+
json = JSON.generate(:this => i.to_s)
|
80
|
+
@store.put("/items/#{i}", :json => json, :remote_user => remote_user)
|
81
|
+
end
|
82
|
+
json = JSON.generate(:this => '4')
|
83
|
+
@store.put('/items/4', :json => json, :remote_user => 'someoneelse')
|
84
|
+
@response = @request.get('/items', auth_key => remote_user)
|
85
|
+
@parsed_response = JSON.parse(@response.body)
|
86
|
+
end
|
87
|
+
|
88
|
+
should "be successful" do
|
89
|
+
assert_equal 200, @response.status
|
90
|
+
end
|
91
|
+
|
92
|
+
should "return a list of URIs for all owner-originated resources" do
|
93
|
+
assert_same_elements ['/items/0', '/items/1', '/items/2'],
|
94
|
+
@parsed_response['uris']
|
95
|
+
end
|
96
|
+
|
97
|
+
should "sort descending on last_modified date" do
|
98
|
+
assert_equal ['/items/2', '/items/1', '/items/0'],
|
99
|
+
@parsed_response['uris']
|
100
|
+
end
|
101
|
+
|
102
|
+
should "return the total number of uris" do
|
103
|
+
assert @parsed_response['total']
|
104
|
+
assert_equal 3, @parsed_response['total']
|
105
|
+
end
|
106
|
+
|
107
|
+
should "return the offset" do
|
108
|
+
assert @parsed_response['offset']
|
109
|
+
assert_equal 0, @parsed_response['offset']
|
110
|
+
end
|
111
|
+
|
112
|
+
should "return a Content-Type header" do
|
113
|
+
assert_equal 'application/json', @response['Content-Type']
|
114
|
+
end
|
115
|
+
|
116
|
+
should "return an ETag" do
|
117
|
+
assert @response['ETag']
|
118
|
+
end
|
119
|
+
|
120
|
+
should "return a Last-Modified date" do
|
121
|
+
assert @response['Last-Modified']
|
122
|
+
end
|
123
|
+
|
124
|
+
should "accept a limit parameter" do
|
125
|
+
response = @request.get('/items?limit=2', auth_key => remote_user)
|
126
|
+
parsed_response = JSON.parse(response.body)
|
127
|
+
assert_equal ['/items/2', '/items/1'], parsed_response['uris']
|
128
|
+
assert_equal 3, parsed_response['total']
|
129
|
+
end
|
130
|
+
|
131
|
+
should "accept an offset parameter" do
|
132
|
+
response = @request.get('/items?offset=1', auth_key => remote_user)
|
133
|
+
parsed_response = JSON.parse(response.body)
|
134
|
+
assert_equal ['/items/1', '/items/0'], parsed_response['uris']
|
135
|
+
assert_equal 1, parsed_response['offset']
|
136
|
+
assert_equal 3, parsed_response['total']
|
137
|
+
end
|
138
|
+
|
139
|
+
should "accept combined limit and offset parameters" do
|
140
|
+
response = @request.get('/items?limit=1&offset=1', auth_key => remote_user)
|
141
|
+
parsed_response = JSON.parse(response.body)
|
142
|
+
assert_equal ['/items/1'], parsed_response['uris']
|
143
|
+
assert_equal 1, parsed_response['offset']
|
144
|
+
assert_equal 3, parsed_response['total']
|
145
|
+
end
|
146
|
+
|
147
|
+
should "return an empty list if no resources are found" do
|
148
|
+
response = @request.get('/things', auth_key => remote_user)
|
149
|
+
parsed_response = JSON.parse(response.body)
|
150
|
+
assert_equal [], parsed_response['uris']
|
151
|
+
assert_equal 0, parsed_response['total']
|
152
|
+
assert_equal 0, parsed_response['offset']
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
context "on GET /:collection/:id" do
|
157
|
+
|
158
|
+
setup do
|
159
|
+
json = JSON.generate(:this => 'that')
|
160
|
+
@store.put('/items/abc', :json => json, :remote_user => remote_user)
|
161
|
+
@response = @request.get(
|
162
|
+
'/items/abc', 'HTTP_HOST' => 'example.org', auth_key => remote_user)
|
163
|
+
end
|
164
|
+
|
165
|
+
should "be successful" do
|
166
|
+
assert_equal 200, @response.status
|
167
|
+
end
|
168
|
+
|
169
|
+
should "return a document for valid owner-originated requests" do
|
170
|
+
data = JSON.parse(@response.body)
|
171
|
+
assert_equal 'that', data['this']
|
172
|
+
end
|
173
|
+
|
174
|
+
should "return a 404 if a document does not exist" do
|
175
|
+
response = @request.get('/items/nothing', auth_key => remote_user)
|
176
|
+
assert_equal 404, response.status
|
177
|
+
end
|
178
|
+
|
179
|
+
should "return a Content-Type header" do
|
180
|
+
assert_equal 'application/json', @response['Content-Type']
|
181
|
+
end
|
182
|
+
|
183
|
+
should "return an ETag header" do
|
184
|
+
assert @response['ETag']
|
185
|
+
end
|
186
|
+
|
187
|
+
should "return a Last-Modified header" do
|
188
|
+
assert @response['Last-Modified']
|
189
|
+
end
|
190
|
+
|
191
|
+
should "not return documents for unauthorized users" do
|
192
|
+
response = @request.get('/items/abc', auth_key => 'bogus')
|
193
|
+
assert_equal 404, response.status
|
194
|
+
end
|
195
|
+
|
196
|
+
should "return a versions link header" do
|
197
|
+
assert @response['Link']
|
198
|
+
assert @response['Link'].match("<http://example.org/items/abc/versions>; rel=\"http://joncrosby.me/cloudkit/1.0/rel/versions\"")
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
context "on GET /:collection/:id/versions" do
|
203
|
+
|
204
|
+
setup do
|
205
|
+
@etags = []
|
206
|
+
4.times do |i|
|
207
|
+
json = JSON.generate(:this => i)
|
208
|
+
options = {:json => json, :remote_user => remote_user}
|
209
|
+
options.filter_merge!(:etag => @etags.try(:last))
|
210
|
+
result = @store.put('/items/abc', options)
|
211
|
+
@etags << result.parsed_content['etag']
|
212
|
+
end
|
213
|
+
@response = @request.get('/items/abc/versions', auth_key => remote_user)
|
214
|
+
@parsed_response = JSON.parse(@response.body)
|
215
|
+
end
|
216
|
+
|
217
|
+
should "be successful" do
|
218
|
+
assert_equal 200, @response.status
|
219
|
+
end
|
220
|
+
|
221
|
+
should "be successful even if the current resource has been deleted" do
|
222
|
+
@store.delete('/items/abc', :etag => @etags.last, :remote_user => remote_user)
|
223
|
+
response = @request.get('/items/abc/versions', auth_key => remote_user)
|
224
|
+
assert_equal 200, @response.status
|
225
|
+
parsed_response = JSON.parse(response.body)
|
226
|
+
assert_equal 4, parsed_response['uris'].size
|
227
|
+
end
|
228
|
+
|
229
|
+
should "return a list of URIs for all versions of a resource" do
|
230
|
+
uris = @parsed_response['uris']
|
231
|
+
assert uris
|
232
|
+
assert_equal 4, uris.size
|
233
|
+
end
|
234
|
+
|
235
|
+
should "return a 404 if the resource does not exist" do
|
236
|
+
response = @request.get('/items/nothing/versions', auth_key => remote_user)
|
237
|
+
assert_equal 404, response.status
|
238
|
+
end
|
239
|
+
|
240
|
+
should "return a 404 for non-owner-originated requests" do
|
241
|
+
response = @request.get('/items/abc/versions', auth_key => 'someoneelse')
|
242
|
+
assert_equal 404, response.status
|
243
|
+
end
|
244
|
+
|
245
|
+
should "sort descending on last_modified date" do
|
246
|
+
assert_equal ['/items/abc'].concat(@etags[0..-2].reverse.map{|e| "/items/abc/versions/#{e}"}),
|
247
|
+
@parsed_response['uris']
|
248
|
+
end
|
249
|
+
|
250
|
+
should "return the total number of uris" do
|
251
|
+
assert @parsed_response['total']
|
252
|
+
assert_equal 4, @parsed_response['total']
|
253
|
+
end
|
254
|
+
|
255
|
+
should "return the offset" do
|
256
|
+
assert @parsed_response['offset']
|
257
|
+
assert_equal 0, @parsed_response['offset']
|
258
|
+
end
|
259
|
+
|
260
|
+
should "return a Content-Type header" do
|
261
|
+
assert_equal 'application/json', @response['Content-Type']
|
262
|
+
end
|
263
|
+
|
264
|
+
should "return an ETag" do
|
265
|
+
assert @response['ETag']
|
266
|
+
end
|
267
|
+
|
268
|
+
should "return a Last-Modified date" do
|
269
|
+
assert @response['Last-Modified']
|
270
|
+
end
|
271
|
+
|
272
|
+
should "accept a limit parameter" do
|
273
|
+
response = @request.get('/items/abc/versions?limit=2', auth_key => remote_user)
|
274
|
+
parsed_response = JSON.parse(response.body)
|
275
|
+
assert_equal ['/items/abc', "/items/abc/versions/#{@etags[-2]}"],
|
276
|
+
parsed_response['uris']
|
277
|
+
assert_equal 4, parsed_response['total']
|
278
|
+
end
|
279
|
+
|
280
|
+
should "accept an offset parameter" do
|
281
|
+
response = @request.get('/items/abc/versions?offset=1', auth_key => remote_user)
|
282
|
+
parsed_response = JSON.parse(response.body)
|
283
|
+
assert_equal @etags.reverse[1..-1].map{|e| "/items/abc/versions/#{e}"},
|
284
|
+
parsed_response['uris']
|
285
|
+
assert_equal 1, parsed_response['offset']
|
286
|
+
assert_equal 4, parsed_response['total']
|
287
|
+
end
|
288
|
+
|
289
|
+
should "accept combined limit and offset parameters" do
|
290
|
+
response = @request.get('/items/abc/versions?limit=1&offset=1', auth_key => remote_user)
|
291
|
+
parsed_response = JSON.parse(response.body)
|
292
|
+
assert_equal ["/items/abc/versions/#{@etags[-2]}"], parsed_response['uris']
|
293
|
+
assert_equal 1, parsed_response['offset']
|
294
|
+
assert_equal 4, parsed_response['total']
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
context "on GET /:collection/:id/versions/:etag" do
|
299
|
+
|
300
|
+
setup do
|
301
|
+
@etags = []
|
302
|
+
2.times do |i|
|
303
|
+
json = JSON.generate(:this => i)
|
304
|
+
options = {:json => json, :remote_user => remote_user}
|
305
|
+
options.filter_merge!(:etag => @etags.try(:last))
|
306
|
+
result = @store.put('/items/abc', options)
|
307
|
+
@etags << result.parsed_content['etag']
|
308
|
+
end
|
309
|
+
@response = @request.get(
|
310
|
+
"/items/abc/versions/#{@etags.first}", auth_key => remote_user)
|
311
|
+
@parsed_response = JSON.parse(@response.body)
|
312
|
+
end
|
313
|
+
|
314
|
+
should "be successful" do
|
315
|
+
assert_equal 200, @response.status
|
316
|
+
end
|
317
|
+
|
318
|
+
should "return a document for valid owner-originated requests" do
|
319
|
+
assert_equal 0, @parsed_response['this']
|
320
|
+
end
|
321
|
+
|
322
|
+
should "return a 404 if a document is not found" do
|
323
|
+
response = @request.get(
|
324
|
+
"/items/nothing/versions/#{@etags.first}", auth_key => remote_user)
|
325
|
+
assert_equal 404, response.status
|
326
|
+
end
|
327
|
+
|
328
|
+
should "return a Content-Type header" do
|
329
|
+
assert_equal 'application/json', @response['Content-Type']
|
330
|
+
end
|
331
|
+
|
332
|
+
should "return an ETag header" do
|
333
|
+
assert @response['ETag']
|
334
|
+
end
|
335
|
+
|
336
|
+
should "return a Last-Modified header" do
|
337
|
+
assert @response['Last-Modified']
|
338
|
+
end
|
339
|
+
|
340
|
+
should "not return documents for unauthorized users" do
|
341
|
+
response = @request.get(
|
342
|
+
"/items/abc/versions/#{@etags.first}", auth_key => 'someoneelse')
|
343
|
+
assert_equal 404, response.status
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
context "on POST /:collection" do
|
348
|
+
|
349
|
+
setup do
|
350
|
+
json = JSON.generate(:this => 'that')
|
351
|
+
@response = @request.post(
|
352
|
+
'/items', :input => json, auth_key => remote_user)
|
353
|
+
@body = JSON.parse(@response.body)
|
354
|
+
end
|
355
|
+
|
356
|
+
should "store the document" do
|
357
|
+
result = @store.get(@body['uri'])
|
358
|
+
assert_equal 200, result.status
|
359
|
+
end
|
360
|
+
|
361
|
+
should "return a 201 when successful" do
|
362
|
+
assert_equal 201, @response.status
|
363
|
+
end
|
364
|
+
|
365
|
+
should "return the metadata" do
|
366
|
+
assert_equal 4, @body.keys.size
|
367
|
+
assert_same_elements ['ok', 'uri', 'etag', 'last_modified'], @body.keys
|
368
|
+
end
|
369
|
+
|
370
|
+
should "set the Content-Type header" do
|
371
|
+
assert_equal 'application/json', @response['Content-Type']
|
372
|
+
end
|
373
|
+
|
374
|
+
should "not set an ETag header" do
|
375
|
+
assert_nil @response['ETag']
|
376
|
+
end
|
377
|
+
|
378
|
+
should "not set a Last-Modified header" do
|
379
|
+
assert_nil @response['Last-Modified']
|
380
|
+
end
|
381
|
+
|
382
|
+
should "return a 422 if parsing fails" do
|
383
|
+
response = @request.post('/items', :input => 'fail', auth_key => remote_user)
|
384
|
+
assert_equal 422, response.status
|
385
|
+
end
|
386
|
+
|
387
|
+
should "insert into its views" do
|
388
|
+
view = CloudKit::ExtractionView.new(
|
389
|
+
:fruits,
|
390
|
+
:observe => :items,
|
391
|
+
:extract => [:apple, :lemon])
|
392
|
+
store = CloudKit::Store.new(
|
393
|
+
:collections => [:items],
|
394
|
+
:views => [view])
|
395
|
+
json = JSON.generate(:apple => 'green')
|
396
|
+
store.put('/items/123', :json => json)
|
397
|
+
json = JSON.generate(:apple => 'red')
|
398
|
+
store.put('/items/456', :json => json)
|
399
|
+
result = store.get('/fruits', :apple => 'green')
|
400
|
+
uris = result.parsed_content['uris']
|
401
|
+
assert_equal 1, uris.size
|
402
|
+
assert uris.include?('/items/123')
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
context "on PUT /:collection/:id" do
|
407
|
+
|
408
|
+
setup do
|
409
|
+
json = JSON.generate(:this => 'that')
|
410
|
+
@original = @store.put('/items/abc', :json => json, :remote_user => remote_user)
|
411
|
+
etag = @original.parsed_content['etag']
|
412
|
+
json = JSON.generate(:this => 'other', :etag => etag)
|
413
|
+
@response = @request.put(
|
414
|
+
'/items/abc',
|
415
|
+
:input => json,
|
416
|
+
'HTTP_IF_MATCH' => etag,
|
417
|
+
auth_key => remote_user)
|
418
|
+
@json = JSON.parse(@response.body)
|
419
|
+
end
|
420
|
+
|
421
|
+
should "create a document if it does not already exist" do
|
422
|
+
json = JSON.generate(:this => 'thing')
|
423
|
+
response = @request.put(
|
424
|
+
'/items/xyz', :input => json, auth_key => remote_user)
|
425
|
+
assert_equal 201, response.status
|
426
|
+
result = @store.get('/items/xyz')
|
427
|
+
assert_equal 200, result.status
|
428
|
+
assert_equal 'thing', result.parsed_content['this']
|
429
|
+
end
|
430
|
+
|
431
|
+
should "update the document if it already exists" do
|
432
|
+
assert_equal 200, @response.status
|
433
|
+
result = @store.get('/items/abc').parsed_content
|
434
|
+
assert_equal 'other', result['this']
|
435
|
+
end
|
436
|
+
|
437
|
+
should "return the metadata" do
|
438
|
+
assert_equal 4, @json.keys.size
|
439
|
+
assert_same_elements ['ok', 'uri', 'etag', 'last_modified'], @json.keys
|
440
|
+
end
|
441
|
+
|
442
|
+
should "set the Content-Type header" do
|
443
|
+
assert_equal 'application/json', @response['Content-Type']
|
444
|
+
end
|
445
|
+
|
446
|
+
should "not set an ETag header" do
|
447
|
+
assert_nil @response['ETag']
|
448
|
+
end
|
449
|
+
|
450
|
+
should "not set a Last-Modified header" do
|
451
|
+
assert_nil @response['Last-Modified']
|
452
|
+
end
|
453
|
+
|
454
|
+
should "not allow a remote_user change" do
|
455
|
+
json = JSON.generate(:this => 'other')
|
456
|
+
response = @request.put(
|
457
|
+
'/items/abc',
|
458
|
+
:input => json,
|
459
|
+
'HTTP_IF_MATCH' => @json['etag'],
|
460
|
+
auth_key => 'someone_else')
|
461
|
+
assert_equal 404, response.status
|
462
|
+
end
|
463
|
+
|
464
|
+
should "detect and return conflicts" do
|
465
|
+
client_a_input = JSON.generate(:this => 'updated')
|
466
|
+
client_b_input = JSON.generate(:other => 'thing')
|
467
|
+
response = @request.put(
|
468
|
+
'/items/abc',
|
469
|
+
:input => client_a_input,
|
470
|
+
'HTTP_IF_MATCH' => @json['etag'],
|
471
|
+
auth_key => remote_user)
|
472
|
+
assert_equal 200, response.status
|
473
|
+
response = @request.put(
|
474
|
+
'/items/abc',
|
475
|
+
:input => client_b_input,
|
476
|
+
'HTTP_IF_MATCH' => @json['etag'],
|
477
|
+
auth_key => remote_user)
|
478
|
+
assert_equal 412, response.status
|
479
|
+
end
|
480
|
+
|
481
|
+
should "require an ETag for updates" do
|
482
|
+
json = JSON.generate(:this => 'updated')
|
483
|
+
response = @request.put(
|
484
|
+
'/items/abc',
|
485
|
+
:input => json,
|
486
|
+
auth_key => remote_user)
|
487
|
+
assert_equal 400, response.status
|
488
|
+
end
|
489
|
+
|
490
|
+
should "return a 422 if parsing fails" do
|
491
|
+
response = @request.put(
|
492
|
+
'/items/zzz', :input => 'fail', auth_key => remote_user)
|
493
|
+
assert_equal 422, response.status
|
494
|
+
end
|
495
|
+
|
496
|
+
should "version document updates" do
|
497
|
+
json = JSON.generate(:this => 'updated')
|
498
|
+
response = @request.put(
|
499
|
+
'/items/abc',
|
500
|
+
:input => json,
|
501
|
+
'HTTP_IF_MATCH' => @json['etag'],
|
502
|
+
auth_key => remote_user)
|
503
|
+
assert_equal 200, response.status
|
504
|
+
etag = JSON.parse(response.body)['etag']
|
505
|
+
json = JSON.generate(:this => 'updated again')
|
506
|
+
new_response = @request.put(
|
507
|
+
'/items/abc',
|
508
|
+
:input => json,
|
509
|
+
'HTTP_IF_MATCH' => etag,
|
510
|
+
auth_key => remote_user)
|
511
|
+
assert_equal 200, new_response.status
|
512
|
+
new_etag = JSON.parse(new_response.body)['etag']
|
513
|
+
assert_not_equal etag, new_etag
|
514
|
+
end
|
515
|
+
|
516
|
+
should "update its views" do
|
517
|
+
view = CloudKit::ExtractionView.new(
|
518
|
+
:fruits,
|
519
|
+
:observe => :items,
|
520
|
+
:extract => [:apple, :lemon])
|
521
|
+
store = CloudKit::Store.new(
|
522
|
+
:collections => [:items],
|
523
|
+
:views => [view])
|
524
|
+
json = JSON.generate(:apple => 'green')
|
525
|
+
result = store.put('/items/123', :json => json)
|
526
|
+
json = JSON.generate(:apple => 'red')
|
527
|
+
store.put(
|
528
|
+
'/items/123', :etag => result.parsed_content['etag'], :json => json)
|
529
|
+
result = store.get('/fruits', :apple => 'green')
|
530
|
+
uris = result.parsed_content['uris']
|
531
|
+
assert_equal 0, uris.size
|
532
|
+
result = store.get('/fruits', :apple => 'red')
|
533
|
+
uris = result.parsed_content['uris']
|
534
|
+
assert_equal 1, uris.size
|
535
|
+
assert uris.include?('/items/123')
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
context "on DELETE /:collection/:id" do
|
540
|
+
|
541
|
+
setup do
|
542
|
+
json = JSON.generate(:this => 'that')
|
543
|
+
@result = @store.put('/items/abc', :json => json, :remote_user => remote_user)
|
544
|
+
@etag = @result.parsed_content['etag']
|
545
|
+
end
|
546
|
+
|
547
|
+
should "delete the document" do
|
548
|
+
response = @request.delete(
|
549
|
+
'/items/abc',
|
550
|
+
'HTTP_IF_MATCH' => @etag,
|
551
|
+
auth_key => remote_user)
|
552
|
+
assert_equal 200, response.status
|
553
|
+
result = @store.get('/items/abc')
|
554
|
+
assert_equal 410, result.status
|
555
|
+
end
|
556
|
+
|
557
|
+
should "return the metadata" do
|
558
|
+
response = @request.delete(
|
559
|
+
'/items/abc',
|
560
|
+
'HTTP_IF_MATCH' => @etag,
|
561
|
+
auth_key => remote_user)
|
562
|
+
json = JSON.parse(response.body)
|
563
|
+
assert_equal 4, json.keys.size
|
564
|
+
assert_same_elements ['ok', 'uri', 'etag', 'last_modified'], json.keys
|
565
|
+
end
|
566
|
+
|
567
|
+
should "set the Content-Type header" do
|
568
|
+
response = @request.delete(
|
569
|
+
'/items/abc',
|
570
|
+
'HTTP_IF_MATCH' => @etag,
|
571
|
+
auth_key => remote_user)
|
572
|
+
assert_equal 'application/json', response['Content-Type']
|
573
|
+
end
|
574
|
+
|
575
|
+
should "not set an ETag header" do
|
576
|
+
response = @request.delete(
|
577
|
+
'/items/abc',
|
578
|
+
'HTTP_IF_MATCH' => @etag,
|
579
|
+
auth_key => remote_user)
|
580
|
+
assert_nil response['ETag']
|
581
|
+
end
|
582
|
+
|
583
|
+
should "not set a Last-Modified header" do
|
584
|
+
response = @request.delete(
|
585
|
+
'/items/abc',
|
586
|
+
'HTTP_IF_MATCH' => @etag,
|
587
|
+
auth_key => remote_user)
|
588
|
+
assert_nil response['Last-Modified']
|
589
|
+
end
|
590
|
+
|
591
|
+
should "return a 404 for items that have never existed" do
|
592
|
+
response = @request.delete(
|
593
|
+
'/items/zzz',
|
594
|
+
'HTTP_IF_MATCH' => @etag,
|
595
|
+
auth_key => remote_user)
|
596
|
+
assert_equal 404, response.status
|
597
|
+
end
|
598
|
+
|
599
|
+
should "require an ETag" do
|
600
|
+
response = @request.delete(
|
601
|
+
'/items/abc',
|
602
|
+
auth_key => remote_user)
|
603
|
+
assert_equal 400, response.status
|
604
|
+
end
|
605
|
+
|
606
|
+
should "verify the user in the doc" do
|
607
|
+
response = @request.delete(
|
608
|
+
'/items/abc',
|
609
|
+
'HTTP_IF_MATCH' => @etag,
|
610
|
+
auth_key => 'someoneelse')
|
611
|
+
assert_equal 404, response.status
|
612
|
+
end
|
613
|
+
|
614
|
+
should "detect and return conflicts" do
|
615
|
+
json = JSON.generate(:this => 'that')
|
616
|
+
result = @store.put('/items/123', :json => json, :remote_user => remote_user)
|
617
|
+
etag = result.parsed_content['etag']
|
618
|
+
client_a_input = JSON.generate(:this => 'updated')
|
619
|
+
client_b_input = JSON.generate(:other => 'thing')
|
620
|
+
response = @request.put(
|
621
|
+
'/items/123',
|
622
|
+
:input => client_a_input,
|
623
|
+
'HTTP_IF_MATCH' => etag,
|
624
|
+
auth_key => remote_user)
|
625
|
+
assert_equal 200, response.status
|
626
|
+
response = @request.delete(
|
627
|
+
'/items/123',
|
628
|
+
:input => client_b_input,
|
629
|
+
'HTTP_IF_MATCH' => etag,
|
630
|
+
auth_key => remote_user)
|
631
|
+
assert_equal 412, response.status
|
632
|
+
end
|
633
|
+
|
634
|
+
should "retain version history" do
|
635
|
+
response = @request.delete(
|
636
|
+
'/items/abc',
|
637
|
+
'HTTP_IF_MATCH' => @etag,
|
638
|
+
auth_key => remote_user)
|
639
|
+
assert_equal 200, response.status
|
640
|
+
response = @request.get(
|
641
|
+
'/items/abc/versions',
|
642
|
+
auth_key => remote_user)
|
643
|
+
json = JSON.parse(response.body)
|
644
|
+
assert_equal 1, json['total']
|
645
|
+
end
|
646
|
+
|
647
|
+
should "remove records from its views" do
|
648
|
+
view = CloudKit::ExtractionView.new(
|
649
|
+
:fruits,
|
650
|
+
:observe => :items,
|
651
|
+
:extract => [:apple, :lemon])
|
652
|
+
store = CloudKit::Store.new(
|
653
|
+
:collections => [:items],
|
654
|
+
:views => [view])
|
655
|
+
json = JSON.generate(:apple => 'green')
|
656
|
+
result = store.put('/items/123', :json => json)
|
657
|
+
store.delete('/items/123', :etag => result.parsed_content['etag'])
|
658
|
+
result = store.get('/fruits', :apple => 'green')
|
659
|
+
uris = result.parsed_content['uris']
|
660
|
+
assert_equal [], uris
|
661
|
+
end
|
662
|
+
end
|
663
|
+
|
664
|
+
context "on OPTIONS /:collection" do
|
665
|
+
|
666
|
+
setup do
|
667
|
+
@response = @request.request('OPTIONS', '/items', auth_key => remote_user)
|
668
|
+
end
|
669
|
+
|
670
|
+
should "return a 200 status" do
|
671
|
+
assert_equal 200, @response.status
|
672
|
+
end
|
673
|
+
|
674
|
+
should "return a list of available methods" do
|
675
|
+
assert @response['Allow']
|
676
|
+
methods = @response['Allow'].split(', ')
|
677
|
+
assert_same_elements(['GET', 'POST', 'HEAD', 'OPTIONS'], methods)
|
678
|
+
end
|
679
|
+
end
|
680
|
+
|
681
|
+
context "on OPTIONS /:collection/:id" do
|
682
|
+
|
683
|
+
setup do
|
684
|
+
@response = @request.request('OPTIONS', '/items/xyz', auth_key => remote_user)
|
685
|
+
end
|
686
|
+
|
687
|
+
should "return a 200 status" do
|
688
|
+
assert_equal 200, @response.status
|
689
|
+
end
|
690
|
+
|
691
|
+
should "return a list of available methods" do
|
692
|
+
assert @response['Allow']
|
693
|
+
methods = @response['Allow'].split(', ')
|
694
|
+
assert_same_elements(['GET', 'PUT', 'DELETE', 'HEAD', 'OPTIONS'], methods)
|
695
|
+
end
|
696
|
+
end
|
697
|
+
|
698
|
+
context "on OPTIONS /:collection/:id/versions" do
|
699
|
+
end
|
700
|
+
|
701
|
+
context "on OPTIONS /:collection/:id/versions/:etag" do
|
702
|
+
end
|
703
|
+
|
704
|
+
context "on HEAD" do
|
705
|
+
|
706
|
+
should "return an empty body" do
|
707
|
+
json = JSON.generate(:this => 'that')
|
708
|
+
@store.put('/items/abc', :json => json, :remote_user => remote_user)
|
709
|
+
response = @request.request('HEAD', '/items/abc', auth_key => remote_user)
|
710
|
+
assert_equal '', response.body
|
711
|
+
response = @request.request('HEAD', '/items', auth_key => remote_user)
|
712
|
+
assert_equal '', response.body
|
713
|
+
end
|
714
|
+
|
715
|
+
end
|
716
|
+
end
|
717
|
+
end
|
718
|
+
end
|