georgi-rack_dav 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/rack_dav.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'rack_dav'
3
+ s.version = '0.1.1'
4
+ s.summary = 'WebDAV handler for Rack'
5
+ s.author = 'Matthias Georgi'
6
+ s.email = 'matti.georgi@gmail.com'
7
+ s.homepage = 'http://www.matthias-georgi.de/rack_dav'
8
+ s.description = 'WebDAV handler for Rack'
9
+ s.require_path = 'lib'
10
+ s.executables << 'rack_dav'
11
+ s.has_rdoc = true
12
+ s.extra_rdoc_files = ['README.md']
13
+ s.files = %w{
14
+ .gitignore
15
+ LICENSE
16
+ rack_dav.gemspec
17
+ lib/rack_dav.rb
18
+ lib/rack_dav/file_resource.rb
19
+ lib/rack_dav/handler.rb
20
+ lib/rack_dav/controller.rb
21
+ lib/rack_dav/builder_namespace.rb
22
+ lib/rack_dav/http_status.rb
23
+ lib/rack_dav/resource.rb
24
+ bin/rack_dav
25
+ spec/handler_spec.rb
26
+ README.md
27
+ }
28
+ end
@@ -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,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: georgi-rack_dav
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Matthias Georgi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-05-16 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: WebDAV handler for Rack
17
+ email: matti.georgi@gmail.com
18
+ executables:
19
+ - rack_dav
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.md
24
+ files:
25
+ - .gitignore
26
+ - LICENSE
27
+ - rack_dav.gemspec
28
+ - lib/rack_dav.rb
29
+ - lib/rack_dav/file_resource.rb
30
+ - lib/rack_dav/handler.rb
31
+ - lib/rack_dav/controller.rb
32
+ - lib/rack_dav/builder_namespace.rb
33
+ - lib/rack_dav/http_status.rb
34
+ - lib/rack_dav/resource.rb
35
+ - bin/rack_dav
36
+ - spec/handler_spec.rb
37
+ - README.md
38
+ has_rdoc: true
39
+ homepage: http://www.matthias-georgi.de/rack_dav
40
+ post_install_message:
41
+ rdoc_options: []
42
+
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: "0"
50
+ version:
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ version:
57
+ requirements: []
58
+
59
+ rubyforge_project:
60
+ rubygems_version: 1.2.0
61
+ signing_key:
62
+ specification_version: 2
63
+ summary: WebDAV handler for Rack
64
+ test_files: []
65
+