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