rack_dav_sp 0.2.dev

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,16 @@
1
+ module RackDAV
2
+
3
+ # Holds information about library version.
4
+ module Version
5
+ MAJOR = 0
6
+ MINOR = 2
7
+ PATCH = "dev"
8
+ BUILD = nil
9
+
10
+ STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join(".")
11
+ end
12
+
13
+ # The current library version.
14
+ VERSION = Version::STRING
15
+
16
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "rack_dav/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "rack_dav_sp"
7
+ s.version = RackDAV::VERSION
8
+ s.author = "Matthias Georgi"
9
+ s.email = "matti.georgi@gmail.com"
10
+ s.homepage = "http://www.matthias-georgi.de/rack_dav"
11
+ s.summary = "WebDAV handler for Rack."
12
+ s.description = "WebDAV handler for Rack."
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
+ s.require_paths = ["lib"]
17
+
18
+ s.extra_rdoc_files = ["README.md"]
19
+
20
+ s.add_dependency("rack", ">= 1.2.0")
21
+ s.add_dependency("builder")
22
+ s.add_development_dependency("rspec", "~> 2.6.0")
23
+ end
@@ -0,0 +1,441 @@
1
+ require 'spec_helper'
2
+ require 'fileutils'
3
+
4
+ require 'rack/mock'
5
+
6
+ require 'spec/support/lockable_file_resource'
7
+
8
+ class Rack::MockResponse
9
+
10
+ attr_reader :original_response
11
+
12
+ def initialize_with_original(*args)
13
+ status, headers, @original_response = *args
14
+ initialize_without_original(*args)
15
+ end
16
+
17
+ alias_method :initialize_without_original, :initialize
18
+ alias_method :initialize, :initialize_with_original
19
+ end
20
+
21
+ describe RackDAV::Handler do
22
+
23
+ DOC_ROOT = File.expand_path(File.dirname(__FILE__) + '/htdocs')
24
+ METHODS = %w(GET PUT POST DELETE PROPFIND PROPPATCH MKCOL COPY MOVE OPTIONS HEAD LOCK UNLOCK)
25
+ CLASS_2 = METHODS
26
+ CLASS_1 = CLASS_2 - %w(LOCK UNLOCK)
27
+
28
+ before do
29
+ FileUtils.mkdir(DOC_ROOT) unless File.exists?(DOC_ROOT)
30
+ end
31
+
32
+ after do
33
+ FileUtils.rm_rf(DOC_ROOT) if File.exists?(DOC_ROOT)
34
+ end
35
+
36
+ attr_reader :response
37
+
38
+ context "Given a Lockable resource" do
39
+ before do
40
+ @controller = RackDAV::Handler.new(
41
+ :root => DOC_ROOT,
42
+ :resource_class => RackDAV::LockableFileResource
43
+ )
44
+ end
45
+
46
+ describe "OPTIONS" do
47
+ it "is successful" do
48
+ options('/').should be_ok
49
+ end
50
+
51
+ it "sets the allow header with class 2 methods" do
52
+ options('/')
53
+ CLASS_2.each do |method|
54
+ response.headers['allow'].should include(method)
55
+ end
56
+ end
57
+ end
58
+
59
+ describe "LOCK" do
60
+ before(:each) do
61
+ put("/test", :input => "body").should be_ok
62
+ lock("/test", :input => File.read(fixture("requests/lock.xml")))
63
+ end
64
+
65
+ describe "creation" do
66
+ it "succeeds" do
67
+ response.should be_ok
68
+ end
69
+
70
+ it "sets a compliant rack response" do
71
+ body = response.original_response.body
72
+ body.should be_a(Array)
73
+ body.should have(1).part
74
+ end
75
+
76
+ it "prints the lockdiscovery" do
77
+ lockdiscovery_response response_locktoken
78
+ end
79
+ end
80
+
81
+ describe "refreshing" do
82
+ context "a valid locktoken" do
83
+ it "prints the lockdiscovery" do
84
+ token = response_locktoken
85
+ lock("/test", 'HTTP_IF' => "(#{token})").should be_ok
86
+ lockdiscovery_response token
87
+ end
88
+
89
+ it "accepts it without parenthesis" do
90
+ token = response_locktoken
91
+ lock("/test", 'HTTP_IF' => token).should be_ok
92
+ lockdiscovery_response token
93
+ end
94
+
95
+ it "accepts it with excess angular braces (office 2003)" do
96
+ token = response_locktoken
97
+ lock("/test", 'HTTP_IF' => "(<#{token}>)").should be_ok
98
+ lockdiscovery_response token
99
+ end
100
+ end
101
+
102
+ context "an invalid locktoken" do
103
+ it "bails out" do
104
+ lock("/test", 'HTTP_IF' => '123')
105
+ response.should be_forbidden
106
+ response.body.should be_empty
107
+ end
108
+ end
109
+
110
+ context "no locktoken" do
111
+ it "bails out" do
112
+ lock("/test")
113
+ response.should be_bad_request
114
+ response.body.should be_empty
115
+ end
116
+ end
117
+
118
+ end
119
+ end
120
+
121
+ describe "UNLOCK" do
122
+ before(:each) do
123
+ put("/test", :input => "body").should be_ok
124
+ lock("/test", :input => File.read(fixture("requests/lock.xml"))).should be_ok
125
+ end
126
+
127
+ context "given a valid token" do
128
+ before(:each) do
129
+ token = response_locktoken
130
+ unlock("/test", 'HTTP_LOCK_TOKEN' => "(#{token})")
131
+ end
132
+
133
+ it "unlocks the resource" do
134
+ response.should be_no_content
135
+ end
136
+ end
137
+
138
+ context "given an invalid token" do
139
+ before(:each) do
140
+ unlock("/test", 'HTTP_LOCK_TOKEN' => '(123)')
141
+ end
142
+
143
+ it "bails out" do
144
+ response.should be_forbidden
145
+ end
146
+ end
147
+
148
+ context "given no token" do
149
+ before(:each) do
150
+ unlock("/test")
151
+ end
152
+
153
+ it "bails out" do
154
+ response.should be_bad_request
155
+ end
156
+ end
157
+
158
+ end
159
+ end
160
+
161
+ context "Given a not lockable resource" do
162
+ before do
163
+ @controller = RackDAV::Handler.new(
164
+ :root => DOC_ROOT,
165
+ :resource_class => RackDAV::FileResource
166
+ )
167
+ end
168
+
169
+ describe "OPTIONS" do
170
+ it "is successful" do
171
+ options('/').should be_ok
172
+ end
173
+
174
+ it "sets the allow header with class 2 methods" do
175
+ options('/')
176
+ CLASS_1.each do |method|
177
+ response.headers['allow'].should include(method)
178
+ end
179
+ end
180
+ end
181
+
182
+ it 'should return headers' do
183
+ put('/test.html', :input => '<html/>').should be_ok
184
+ head('/test.html').should be_ok
185
+
186
+ response.headers['etag'].should_not be_nil
187
+ response.headers['content-type'].should match(/html/)
188
+ response.headers['last-modified'].should_not be_nil
189
+ end
190
+
191
+ it 'should not find a nonexistent resource' do
192
+ get('/not_found').should be_not_found
193
+ end
194
+
195
+ it 'should not allow directory traversal' do
196
+ get('/../htdocs').should be_forbidden
197
+ end
198
+
199
+ it 'should create a resource and allow its retrieval' do
200
+ put('/test', :input => 'body').should be_ok
201
+ get('/test').should be_ok
202
+ response.body.should == 'body'
203
+ end
204
+ it 'should create and find a url with escaped characters' do
205
+ put(url_escape('/a b'), :input => 'body').should be_ok
206
+ get(url_escape('/a b')).should be_ok
207
+ response.body.should == 'body'
208
+ end
209
+
210
+ it 'should delete a single resource' do
211
+ put('/test', :input => 'body').should be_ok
212
+ delete('/test').should be_no_content
213
+ end
214
+
215
+ it 'should delete recursively' do
216
+ mkcol('/folder').should be_created
217
+ put('/folder/a', :input => 'body').should be_ok
218
+ put('/folder/b', :input => 'body').should be_ok
219
+
220
+ delete('/folder').should be_no_content
221
+ get('/folder').should be_not_found
222
+ get('/folder/a').should be_not_found
223
+ get('/folder/b').should be_not_found
224
+ end
225
+
226
+ it 'should not allow copy to another domain' do
227
+ put('/test', :input => 'body').should be_ok
228
+ copy('http://localhost/', 'HTTP_DESTINATION' => 'http://another/').should be_bad_gateway
229
+ end
230
+
231
+ it 'should not allow copy to the same resource' do
232
+ put('/test', :input => 'body').should be_ok
233
+ copy('/test', 'HTTP_DESTINATION' => '/test').should be_forbidden
234
+ end
235
+
236
+ it 'should not allow an invalid destination uri' do
237
+ put('/test', :input => 'body').should be_ok
238
+ copy('/test', 'HTTP_DESTINATION' => '%').should be_bad_request
239
+ end
240
+
241
+ it 'should copy a single resource' do
242
+ put('/test', :input => 'body').should be_ok
243
+ copy('/test', 'HTTP_DESTINATION' => '/copy').should be_created
244
+ get('/copy').body.should == 'body'
245
+ end
246
+
247
+ it 'should copy a resource with escaped characters' do
248
+ put(url_escape('/a b'), :input => 'body').should be_ok
249
+ copy(url_escape('/a b'), 'HTTP_DESTINATION' => url_escape('/a c')).should be_created
250
+ get(url_escape('/a c')).should be_ok
251
+ response.body.should == 'body'
252
+ end
253
+
254
+ it 'should deny a copy without overwrite' do
255
+ put('/test', :input => 'body').should be_ok
256
+ put('/copy', :input => 'copy').should be_ok
257
+ copy('/test', 'HTTP_DESTINATION' => '/copy', 'HTTP_OVERWRITE' => 'F')
258
+
259
+ multistatus_response('/href').first.text.should == 'http://localhost/test'
260
+ multistatus_response('/status').first.text.should match(/412 Precondition Failed/)
261
+
262
+ get('/copy').body.should == 'copy'
263
+ end
264
+
265
+ it 'should allow a copy with overwrite' do
266
+ put('/test', :input => 'body').should be_ok
267
+ put('/copy', :input => 'copy').should be_ok
268
+ copy('/test', 'HTTP_DESTINATION' => '/copy', 'HTTP_OVERWRITE' => 'T').should be_no_content
269
+ get('/copy').body.should == 'body'
270
+ end
271
+
272
+ it 'should copy a collection' do
273
+ mkcol('/folder').should be_created
274
+ copy('/folder', 'HTTP_DESTINATION' => '/copy').should be_created
275
+ propfind('/copy', :input => propfind_xml(:resourcetype))
276
+ multistatus_response('/propstat/prop/resourcetype/collection').should_not be_empty
277
+ end
278
+
279
+ it 'should copy a collection resursively' do
280
+ mkcol('/folder').should be_created
281
+ put('/folder/a', :input => 'A').should be_ok
282
+ put('/folder/b', :input => 'B').should be_ok
283
+
284
+ copy('/folder', 'HTTP_DESTINATION' => '/copy').should be_created
285
+ propfind('/copy', :input => propfind_xml(:resourcetype))
286
+ multistatus_response('/propstat/prop/resourcetype/collection').should_not be_empty
287
+
288
+ get('/copy/a').body.should == 'A'
289
+ get('/copy/b').body.should == 'B'
290
+ end
291
+
292
+ it 'should move a collection recursively' do
293
+ mkcol('/folder').should be_created
294
+ put('/folder/a', :input => 'A').should be_ok
295
+ put('/folder/b', :input => 'B').should be_ok
296
+
297
+ move('/folder', 'HTTP_DESTINATION' => '/move').should be_created
298
+ propfind('/move', :input => propfind_xml(:resourcetype))
299
+ multistatus_response('/propstat/prop/resourcetype/collection').should_not be_empty
300
+
301
+ get('/move/a').body.should == 'A'
302
+ get('/move/b').body.should == 'B'
303
+ get('/folder/a').should be_not_found
304
+ get('/folder/b').should be_not_found
305
+ end
306
+
307
+ it 'should create a collection' do
308
+ mkcol('/folder').should be_created
309
+ propfind('/folder', :input => propfind_xml(:resourcetype))
310
+ multistatus_response('/propstat/prop/resourcetype/collection').should_not be_empty
311
+ end
312
+
313
+ it 'should not find properties for nonexistent resources' do
314
+ propfind('/non').should be_not_found
315
+ end
316
+
317
+ it 'should find all properties' do
318
+ xml = render do |xml|
319
+ xml.propfind('xmlns:d' => "DAV:") do
320
+ xml.allprop
321
+ end
322
+ end
323
+
324
+ propfind('http://localhost/', :input => xml)
325
+
326
+ multistatus_response('/href').first.text.strip.should == 'http://localhost/'
327
+
328
+ props = %w(creationdate displayname getlastmodified getetag resourcetype getcontenttype getcontentlength)
329
+ props.each do |prop|
330
+ multistatus_response('/propstat/prop/' + prop).should_not be_empty
331
+ end
332
+ end
333
+
334
+ it 'should find named properties' do
335
+ put('/test.html', :input => '<html/>').should be_ok
336
+ propfind('/test.html', :input => propfind_xml(:getcontenttype, :getcontentlength))
337
+
338
+ multistatus_response('/propstat/prop/getcontenttype').first.text.should == 'text/html'
339
+ multistatus_response('/propstat/prop/getcontentlength').first.text.should == '7'
340
+ end
341
+
342
+ it 'should not support LOCK' do
343
+ put('/test', :input => 'body').should be_ok
344
+
345
+ xml = render do |xml|
346
+ xml.lockinfo('xmlns:d' => "DAV:") do
347
+ xml.lockscope { xml.exclusive }
348
+ xml.locktype { xml.write }
349
+ xml.owner { xml.href "http://test.de/" }
350
+ end
351
+ end
352
+
353
+ lock('/test', :input => xml).should be_method_not_allowed
354
+ end
355
+
356
+ it 'should not support UNLOCK' do
357
+ put('/test', :input => 'body').should be_ok
358
+ unlock('/test', :input => '').should be_method_not_allowed
359
+ end
360
+
361
+ end
362
+
363
+
364
+ private
365
+
366
+ def request(method, uri, options={})
367
+ options = {
368
+ 'HTTP_HOST' => 'localhost',
369
+ 'REMOTE_USER' => 'manni'
370
+ }.merge(options)
371
+ request = Rack::MockRequest.new(@controller)
372
+ @response = request.request(method, uri, options)
373
+ end
374
+
375
+ METHODS.each do |method|
376
+ define_method(method.downcase) do |*args|
377
+ request(method, *args)
378
+ end
379
+ end
380
+
381
+
382
+ def render
383
+ xml = Builder::XmlMarkup.new
384
+ xml.instruct! :xml, :version => "1.0", :encoding => "UTF-8"
385
+ xml.namespace('d') do
386
+ yield xml
387
+ end
388
+ xml.target!
389
+ end
390
+
391
+ def url_escape(string)
392
+ string.gsub(/([^ a-zA-Z0-9_.-]+)/n) do
393
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
394
+ end.tr(' ', '+')
395
+ end
396
+
397
+ def response_xml
398
+ @response_xml ||= REXML::Document.new(@response.body)
399
+ end
400
+
401
+ def response_locktoken
402
+ REXML::XPath::match(response_xml,
403
+ "/prop/lockdiscovery/activelock/locktoken/href", '' => 'DAV:'
404
+ ).first.text
405
+ end
406
+
407
+ def lockdiscovery_response(token)
408
+ match = lambda do |pattern|
409
+ REXML::XPath::match(response_xml, "/prop/lockdiscovery/activelock" + pattern, '' => 'DAV:')
410
+ end
411
+
412
+ match[''].should_not be_empty
413
+
414
+ match['/locktype'].should_not be_empty
415
+ match['/lockscope'].should_not be_empty
416
+ match['/depth'].should_not be_empty
417
+ match['/owner'].should_not be_empty
418
+ match['/timeout'].should_not be_empty
419
+ match['/locktoken/href'].should_not be_empty
420
+ match['/locktoken/href'].first.text.should == token
421
+ end
422
+
423
+ def multistatus_response(pattern)
424
+ @response.should be_multi_status
425
+ REXML::XPath::match(response_xml, "/multistatus/response", '' => 'DAV:').should_not be_empty
426
+ REXML::XPath::match(response_xml, "/multistatus/response" + pattern, '' => 'DAV:')
427
+ end
428
+
429
+ def propfind_xml(*props)
430
+ render do |xml|
431
+ xml.propfind('xmlns:d' => "DAV:") do
432
+ xml.prop do
433
+ props.each do |prop|
434
+ xml.tag! prop
435
+ end
436
+ end
437
+ end
438
+ end
439
+ end
440
+
441
+ end