rack_dav 0.1.2
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/.gitignore +4 -0
- data/LICENSE +18 -0
- data/README.md +108 -0
- data/bin/rack_dav +20 -0
- data/lib/rack_dav/builder_namespace.rb +22 -0
- data/lib/rack_dav/controller.rb +410 -0
- data/lib/rack_dav/file_resource.rb +168 -0
- data/lib/rack_dav/handler.rb +34 -0
- data/lib/rack_dav/http_status.rb +108 -0
- data/lib/rack_dav/resource.rb +180 -0
- data/lib/rack_dav.rb +15 -0
- data/rack_dav.gemspec +28 -0
- data/spec/handler_spec.rb +270 -0
- metadata +79 -0
@@ -0,0 +1,270 @@
|
|
1
|
+
$:.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib'))
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rack_dav'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
describe RackDAV::Handler do
|
8
|
+
DOC_ROOT = File.expand_path(File.dirname(__FILE__) + '/htdocs')
|
9
|
+
METHODS = %w(GET PUT POST DELETE PROPFIND PROPPATCH MKCOL COPY MOVE OPTIONS HEAD LOCK UNLOCK)
|
10
|
+
|
11
|
+
before do
|
12
|
+
FileUtils.mkdir(DOC_ROOT) unless File.exists?(DOC_ROOT)
|
13
|
+
@controller = RackDAV::Handler.new(:root => DOC_ROOT)
|
14
|
+
end
|
15
|
+
|
16
|
+
after do
|
17
|
+
FileUtils.rm_rf(DOC_ROOT) if File.exists?(DOC_ROOT)
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :response
|
21
|
+
|
22
|
+
def request(method, uri, options={})
|
23
|
+
options = {
|
24
|
+
'HTTP_HOST' => 'localhost',
|
25
|
+
'REMOTE_USER' => 'manni'
|
26
|
+
}.merge(options)
|
27
|
+
request = Rack::MockRequest.new(@controller)
|
28
|
+
@response = request.request(method, uri, options)
|
29
|
+
end
|
30
|
+
|
31
|
+
METHODS.each do |method|
|
32
|
+
define_method(method.downcase) do |*args|
|
33
|
+
request(method, *args)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def render
|
38
|
+
xml = Builder::XmlMarkup.new
|
39
|
+
xml.instruct! :xml, :version => "1.0", :encoding => "UTF-8"
|
40
|
+
xml.namespace('d') do
|
41
|
+
yield xml
|
42
|
+
end
|
43
|
+
xml.target!
|
44
|
+
end
|
45
|
+
|
46
|
+
def url_escape(string)
|
47
|
+
string.gsub(/([^ a-zA-Z0-9_.-]+)/n) do
|
48
|
+
'%' + $1.unpack('H2' * $1.size).join('%').upcase
|
49
|
+
end.tr(' ', '+')
|
50
|
+
end
|
51
|
+
|
52
|
+
def response_xml
|
53
|
+
REXML::Document.new(@response.body)
|
54
|
+
end
|
55
|
+
|
56
|
+
def multistatus_response(pattern)
|
57
|
+
@response.should be_multi_status
|
58
|
+
REXML::XPath::match(response_xml, "/multistatus/response", '' => 'DAV:').should_not be_empty
|
59
|
+
REXML::XPath::match(response_xml, "/multistatus/response" + pattern, '' => 'DAV:')
|
60
|
+
end
|
61
|
+
|
62
|
+
def propfind_xml(*props)
|
63
|
+
render do |xml|
|
64
|
+
xml.propfind('xmlns:d' => "DAV:") do
|
65
|
+
xml.prop do
|
66
|
+
props.each do |prop|
|
67
|
+
xml.tag! prop
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'should return all options' do
|
75
|
+
options('/').should be_ok
|
76
|
+
|
77
|
+
METHODS.each do |method|
|
78
|
+
response.headers['allow'].should include(method)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'should return headers' do
|
83
|
+
put('/test.html', :input => '<html/>').should be_ok
|
84
|
+
head('/test.html').should be_ok
|
85
|
+
|
86
|
+
response.headers['etag'].should_not be_nil
|
87
|
+
response.headers['content-type'].should match(/html/)
|
88
|
+
response.headers['last-modified'].should_not be_nil
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'should not find a nonexistent resource' do
|
92
|
+
get('/not_found').should be_not_found
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'should not allow directory traversal' do
|
96
|
+
get('/../htdocs').should be_forbidden
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'should create a resource and allow its retrieval' do
|
100
|
+
put('/test', :input => 'body').should be_ok
|
101
|
+
get('/test').should be_ok
|
102
|
+
response.body.should == 'body'
|
103
|
+
end
|
104
|
+
it 'should create and find a url with escaped characters' do
|
105
|
+
put(url_escape('/a b'), :input => 'body').should be_ok
|
106
|
+
get(url_escape('/a b')).should be_ok
|
107
|
+
response.body.should == 'body'
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'should delete a single resource' do
|
111
|
+
put('/test', :input => 'body').should be_ok
|
112
|
+
delete('/test').should be_no_content
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'should delete recursively' do
|
116
|
+
mkcol('/folder').should be_created
|
117
|
+
put('/folder/a', :input => 'body').should be_ok
|
118
|
+
put('/folder/b', :input => 'body').should be_ok
|
119
|
+
|
120
|
+
delete('/folder').should be_no_content
|
121
|
+
get('/folder').should be_not_found
|
122
|
+
get('/folder/a').should be_not_found
|
123
|
+
get('/folder/b').should be_not_found
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'should not allow copy to another domain' do
|
127
|
+
put('/test', :input => 'body').should be_ok
|
128
|
+
copy('http://localhost/', 'HTTP_DESTINATION' => 'http://another/').should be_bad_gateway
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'should not allow copy to the same resource' do
|
132
|
+
put('/test', :input => 'body').should be_ok
|
133
|
+
copy('/test', 'HTTP_DESTINATION' => '/test').should be_forbidden
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'should not allow an invalid destination uri' do
|
137
|
+
put('/test', :input => 'body').should be_ok
|
138
|
+
copy('/test', 'HTTP_DESTINATION' => '%').should be_bad_request
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'should copy a single resource' do
|
142
|
+
put('/test', :input => 'body').should be_ok
|
143
|
+
copy('/test', 'HTTP_DESTINATION' => '/copy').should be_created
|
144
|
+
get('/copy').body.should == 'body'
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'should copy a resource with escaped characters' do
|
148
|
+
put(url_escape('/a b'), :input => 'body').should be_ok
|
149
|
+
copy(url_escape('/a b'), 'HTTP_DESTINATION' => url_escape('/a c')).should be_created
|
150
|
+
get(url_escape('/a c')).should be_ok
|
151
|
+
response.body.should == 'body'
|
152
|
+
end
|
153
|
+
|
154
|
+
it 'should deny a copy without overwrite' do
|
155
|
+
put('/test', :input => 'body').should be_ok
|
156
|
+
put('/copy', :input => 'copy').should be_ok
|
157
|
+
copy('/test', 'HTTP_DESTINATION' => '/copy', 'HTTP_OVERWRITE' => 'F')
|
158
|
+
|
159
|
+
multistatus_response('/href').first.text.should == 'http://localhost/test'
|
160
|
+
multistatus_response('/status').first.text.should match(/412 Precondition Failed/)
|
161
|
+
|
162
|
+
get('/copy').body.should == 'copy'
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'should allow a copy with overwrite' do
|
166
|
+
put('/test', :input => 'body').should be_ok
|
167
|
+
put('/copy', :input => 'copy').should be_ok
|
168
|
+
copy('/test', 'HTTP_DESTINATION' => '/copy', 'HTTP_OVERWRITE' => 'T').should be_no_content
|
169
|
+
get('/copy').body.should == 'body'
|
170
|
+
end
|
171
|
+
|
172
|
+
it 'should copy a collection' do
|
173
|
+
mkcol('/folder').should be_created
|
174
|
+
copy('/folder', 'HTTP_DESTINATION' => '/copy').should be_created
|
175
|
+
propfind('/copy', :input => propfind_xml(:resourcetype))
|
176
|
+
multistatus_response('/propstat/prop/resourcetype/collection').should_not be_empty
|
177
|
+
end
|
178
|
+
|
179
|
+
it 'should copy a collection resursively' do
|
180
|
+
mkcol('/folder').should be_created
|
181
|
+
put('/folder/a', :input => 'A').should be_ok
|
182
|
+
put('/folder/b', :input => 'B').should be_ok
|
183
|
+
|
184
|
+
copy('/folder', 'HTTP_DESTINATION' => '/copy').should be_created
|
185
|
+
propfind('/copy', :input => propfind_xml(:resourcetype))
|
186
|
+
multistatus_response('/propstat/prop/resourcetype/collection').should_not be_empty
|
187
|
+
|
188
|
+
get('/copy/a').body.should == 'A'
|
189
|
+
get('/copy/b').body.should == 'B'
|
190
|
+
end
|
191
|
+
|
192
|
+
it 'should move a collection recursively' do
|
193
|
+
mkcol('/folder').should be_created
|
194
|
+
put('/folder/a', :input => 'A').should be_ok
|
195
|
+
put('/folder/b', :input => 'B').should be_ok
|
196
|
+
|
197
|
+
move('/folder', 'HTTP_DESTINATION' => '/move').should be_created
|
198
|
+
propfind('/move', :input => propfind_xml(:resourcetype))
|
199
|
+
multistatus_response('/propstat/prop/resourcetype/collection').should_not be_empty
|
200
|
+
|
201
|
+
get('/move/a').body.should == 'A'
|
202
|
+
get('/move/b').body.should == 'B'
|
203
|
+
get('/folder/a').should be_not_found
|
204
|
+
get('/folder/b').should be_not_found
|
205
|
+
end
|
206
|
+
|
207
|
+
it 'should create a collection' do
|
208
|
+
mkcol('/folder').should be_created
|
209
|
+
propfind('/folder', :input => propfind_xml(:resourcetype))
|
210
|
+
multistatus_response('/propstat/prop/resourcetype/collection').should_not be_empty
|
211
|
+
end
|
212
|
+
|
213
|
+
it 'should not find properties for nonexistent resources' do
|
214
|
+
propfind('/non').should be_not_found
|
215
|
+
end
|
216
|
+
|
217
|
+
it 'should find all properties' do
|
218
|
+
xml = render do |xml|
|
219
|
+
xml.propfind('xmlns:d' => "DAV:") do
|
220
|
+
xml.allprop
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
propfind('http://localhost/', :input => xml)
|
225
|
+
|
226
|
+
multistatus_response('/href').first.text.strip.should == 'http://localhost/'
|
227
|
+
|
228
|
+
props = %w(creationdate displayname getlastmodified getetag resourcetype getcontenttype getcontentlength)
|
229
|
+
props.each do |prop|
|
230
|
+
multistatus_response('/propstat/prop/' + prop).should_not be_empty
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
it 'should find named properties' do
|
235
|
+
put('/test.html', :input => '<html/>').should be_ok
|
236
|
+
propfind('/test.html', :input => propfind_xml(:getcontenttype, :getcontentlength))
|
237
|
+
|
238
|
+
multistatus_response('/propstat/prop/getcontenttype').first.text.should == 'text/html'
|
239
|
+
multistatus_response('/propstat/prop/getcontentlength').first.text.should == '7'
|
240
|
+
end
|
241
|
+
|
242
|
+
it 'should lock a resource' do
|
243
|
+
put('/test', :input => 'body').should be_ok
|
244
|
+
|
245
|
+
xml = render do |xml|
|
246
|
+
xml.lockinfo('xmlns:d' => "DAV:") do
|
247
|
+
xml.lockscope { xml.exclusive }
|
248
|
+
xml.locktype { xml.write }
|
249
|
+
xml.owner { xml.href "http://test.de/" }
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
lock('/test', :input => xml)
|
254
|
+
|
255
|
+
response.should be_ok
|
256
|
+
|
257
|
+
match = lambda do |pattern|
|
258
|
+
REXML::XPath::match(response_xml, "/prop/lockdiscovery/activelock" + pattern, '' => 'DAV:')
|
259
|
+
end
|
260
|
+
|
261
|
+
match[''].should_not be_empty
|
262
|
+
|
263
|
+
match['/locktype'].should_not be_empty
|
264
|
+
match['/lockscope'].should_not be_empty
|
265
|
+
match['/depth'].should_not be_empty
|
266
|
+
match['/owner'].should_not be_empty
|
267
|
+
match['/timeout'].should_not be_empty
|
268
|
+
match['/locktoken'].should_not be_empty
|
269
|
+
end
|
270
|
+
end
|
metadata
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack_dav
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 31
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 2
|
10
|
+
version: 0.1.2
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Matthias Georgi
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-10-17 00:00:00 -07:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: WebDAV handler for Rack
|
23
|
+
email: matti.georgi@gmail.com
|
24
|
+
executables:
|
25
|
+
- rack_dav
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files:
|
29
|
+
- README.md
|
30
|
+
files:
|
31
|
+
- .gitignore
|
32
|
+
- LICENSE
|
33
|
+
- rack_dav.gemspec
|
34
|
+
- lib/rack_dav.rb
|
35
|
+
- lib/rack_dav/file_resource.rb
|
36
|
+
- lib/rack_dav/handler.rb
|
37
|
+
- lib/rack_dav/controller.rb
|
38
|
+
- lib/rack_dav/builder_namespace.rb
|
39
|
+
- lib/rack_dav/http_status.rb
|
40
|
+
- lib/rack_dav/resource.rb
|
41
|
+
- bin/rack_dav
|
42
|
+
- spec/handler_spec.rb
|
43
|
+
- README.md
|
44
|
+
has_rdoc: true
|
45
|
+
homepage: http://www.matthias-georgi.de/rack_dav
|
46
|
+
licenses: []
|
47
|
+
|
48
|
+
post_install_message:
|
49
|
+
rdoc_options: []
|
50
|
+
|
51
|
+
require_paths:
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
hash: 3
|
59
|
+
segments:
|
60
|
+
- 0
|
61
|
+
version: "0"
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
hash: 3
|
68
|
+
segments:
|
69
|
+
- 0
|
70
|
+
version: "0"
|
71
|
+
requirements: []
|
72
|
+
|
73
|
+
rubyforge_project:
|
74
|
+
rubygems_version: 1.3.7
|
75
|
+
signing_key:
|
76
|
+
specification_version: 3
|
77
|
+
summary: WebDAV handler for Rack
|
78
|
+
test_files: []
|
79
|
+
|