dav4rack 0.0.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 +4 -0
- data/LICENSE +42 -0
- data/README.rdoc +137 -0
- data/bin/dav4rack +65 -0
- data/dav4rack.gemspec +31 -0
- data/lib/dav4rack.rb +13 -0
- data/lib/dav4rack/controller.rb +457 -0
- data/lib/dav4rack/file_resource.rb +176 -0
- data/lib/dav4rack/handler.rb +44 -0
- data/lib/dav4rack/http_status.rb +108 -0
- data/lib/dav4rack/interceptor.rb +20 -0
- data/lib/dav4rack/interceptor_resource.rb +119 -0
- data/lib/dav4rack/remote_file.rb +62 -0
- data/lib/dav4rack/resource.rb +250 -0
- data/spec/handler_spec.rb +270 -0
- metadata +81 -0
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module DAV4Rack
|
5
|
+
|
6
|
+
class RemoteFile
|
7
|
+
|
8
|
+
attr_accessor :path
|
9
|
+
|
10
|
+
alias :to_path :path
|
11
|
+
|
12
|
+
# path:: Path to remote file
|
13
|
+
# args:: Optional argument hash. Allowed keys: :size, :mime_type, :last_modified
|
14
|
+
# Create a reference to a remote file.
|
15
|
+
# NOTE: HTTPError will be raised if path does not return 200 result
|
16
|
+
def initialize(path, args={})
|
17
|
+
@path = path
|
18
|
+
@size = args[:size] || nil
|
19
|
+
@mime_type = args[:mime_type] || 'text/plain'
|
20
|
+
@modified = args[:last_modified] || nil
|
21
|
+
@uri = URI.parse(@path)
|
22
|
+
@con = Net::HTTP.new(@uri.host, @uri.port)
|
23
|
+
@call_path = @uri.path + (@uri.query ? "?#{@uri.query}" : '')
|
24
|
+
res = @con.request_get(@call_path)
|
25
|
+
@heads = res.to_hash
|
26
|
+
res.value
|
27
|
+
@store = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def size
|
31
|
+
@heads['content-length'] || @size
|
32
|
+
end
|
33
|
+
|
34
|
+
def content_type
|
35
|
+
@mime_type || @heads['content-type']
|
36
|
+
end
|
37
|
+
|
38
|
+
def last_modified
|
39
|
+
@heads['last-modified'] || @modified
|
40
|
+
end
|
41
|
+
|
42
|
+
def call(env)
|
43
|
+
dup._call(env)
|
44
|
+
end
|
45
|
+
|
46
|
+
def _call(env)
|
47
|
+
serving
|
48
|
+
end
|
49
|
+
|
50
|
+
def each
|
51
|
+
if(@store)
|
52
|
+
yield @store
|
53
|
+
else
|
54
|
+
@con.request_get(@call_path) do |res|
|
55
|
+
res.read_body(@store) do |part|
|
56
|
+
yield part
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,250 @@
|
|
1
|
+
module DAV4Rack
|
2
|
+
|
3
|
+
class LockFailure < RuntimeError
|
4
|
+
attr_reader :path_status
|
5
|
+
def initialize(*args)
|
6
|
+
super(*args)
|
7
|
+
@path_status = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def add_failure(path, status)
|
11
|
+
@path_status[path] = status
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Resource
|
16
|
+
attr_reader :path, :options, :public_path, :request
|
17
|
+
|
18
|
+
include DAV4Rack::HTTPStatus
|
19
|
+
|
20
|
+
# public_path:: Path received via request
|
21
|
+
# path:: Internal resource path (Only different from public path when using root_uri's for webdav)
|
22
|
+
# request:: Rack::Request
|
23
|
+
# options:: Any options provided for this resource
|
24
|
+
# Creates a new instance of the resource.
|
25
|
+
# NOTE: path and public_path will only differ if the root_uri has been set for the resource. The
|
26
|
+
# controller will strip out the starting path so the resource can easily determine what
|
27
|
+
# it is working on. For example:
|
28
|
+
# request -> /my/webdav/directory/actual/path
|
29
|
+
# public_path -> /my/webdav/directory/actual/path
|
30
|
+
# path -> /actual/path
|
31
|
+
def initialize(public_path, path, request, options)
|
32
|
+
@public_path = public_path.dup
|
33
|
+
@path = path.dup
|
34
|
+
@request = request
|
35
|
+
@options = options.dup
|
36
|
+
end
|
37
|
+
|
38
|
+
# If this is a collection, return the child resources.
|
39
|
+
def children
|
40
|
+
raise NotImplementedError
|
41
|
+
end
|
42
|
+
|
43
|
+
# Is this resource a collection?
|
44
|
+
def collection?
|
45
|
+
raise NotImplementedError
|
46
|
+
end
|
47
|
+
|
48
|
+
# Does this recource exist?
|
49
|
+
def exist?
|
50
|
+
raise NotImplementedError
|
51
|
+
end
|
52
|
+
|
53
|
+
# Return the creation time.
|
54
|
+
def creation_date
|
55
|
+
raise NotImplementedError
|
56
|
+
end
|
57
|
+
|
58
|
+
# Return the time of last modification.
|
59
|
+
def last_modified
|
60
|
+
raise NotImplementedError
|
61
|
+
end
|
62
|
+
|
63
|
+
# Set the time of last modification.
|
64
|
+
def last_modified=(time)
|
65
|
+
raise NotImplementedError
|
66
|
+
end
|
67
|
+
|
68
|
+
# Return an Etag, an unique hash value for this resource.
|
69
|
+
def etag
|
70
|
+
raise NotImplementedError
|
71
|
+
end
|
72
|
+
|
73
|
+
# Return the resource type. Generally only used to specify
|
74
|
+
# resource is a collection.
|
75
|
+
def resource_type
|
76
|
+
:collection if collection?
|
77
|
+
end
|
78
|
+
|
79
|
+
# Return the mime type of this resource.
|
80
|
+
def content_type
|
81
|
+
raise NotImplementedError
|
82
|
+
end
|
83
|
+
|
84
|
+
# Return the size in bytes for this resource.
|
85
|
+
def content_length
|
86
|
+
raise NotImplementedError
|
87
|
+
end
|
88
|
+
|
89
|
+
# HTTP GET request.
|
90
|
+
#
|
91
|
+
# Write the content of the resource to the response.body.
|
92
|
+
def get(request, response)
|
93
|
+
raise NotImplementedError
|
94
|
+
end
|
95
|
+
|
96
|
+
# HTTP PUT request.
|
97
|
+
#
|
98
|
+
# Save the content of the request.body.
|
99
|
+
def put(request, response)
|
100
|
+
raise NotImplementedError
|
101
|
+
end
|
102
|
+
|
103
|
+
# HTTP POST request.
|
104
|
+
#
|
105
|
+
# Usually forbidden.
|
106
|
+
def post(request, response)
|
107
|
+
raise NotImplementedError
|
108
|
+
end
|
109
|
+
|
110
|
+
# HTTP DELETE request.
|
111
|
+
#
|
112
|
+
# Delete this resource.
|
113
|
+
def delete
|
114
|
+
raise NotImplementedError
|
115
|
+
end
|
116
|
+
|
117
|
+
# HTTP COPY request.
|
118
|
+
#
|
119
|
+
# Copy this resource to given destination resource.
|
120
|
+
def copy(dest)
|
121
|
+
raise NotImplementedError
|
122
|
+
end
|
123
|
+
|
124
|
+
# HTTP MOVE request.
|
125
|
+
#
|
126
|
+
# Move this resource to given destination resource.
|
127
|
+
def move(dest)
|
128
|
+
raise NotImplemented
|
129
|
+
end
|
130
|
+
|
131
|
+
# args:: Hash of lock arguments
|
132
|
+
# Request for a lock on the given resource. A valid lock should lock
|
133
|
+
# all descendents. Failures should be noted and returned as an exception
|
134
|
+
# using LockFailure.
|
135
|
+
# Valid args keys: :timeout -> requested timeout
|
136
|
+
# :depth -> lock depth
|
137
|
+
# :scope -> lock scope
|
138
|
+
# :type -> lock type
|
139
|
+
# :owner -> lock owner
|
140
|
+
# Should return a tuple: [lock_time, locktoken] where lock_time is the
|
141
|
+
# given timeout
|
142
|
+
# NOTE: See section 9.10 of RFC 4918 for guidance about
|
143
|
+
# how locks should be generated and the expected responses
|
144
|
+
# (http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10)
|
145
|
+
def lock(args)
|
146
|
+
raise NotImplemented
|
147
|
+
end
|
148
|
+
|
149
|
+
def unlock(token)
|
150
|
+
raise NotImplemented
|
151
|
+
end
|
152
|
+
|
153
|
+
# Create this resource as collection.
|
154
|
+
def make_collection
|
155
|
+
raise NotImplementedError
|
156
|
+
end
|
157
|
+
|
158
|
+
# other:: Resource
|
159
|
+
# Returns if current resource is equal to other resource
|
160
|
+
def ==(other)
|
161
|
+
path == other.path
|
162
|
+
end
|
163
|
+
|
164
|
+
# Name of the resource
|
165
|
+
def name
|
166
|
+
File.basename(path)
|
167
|
+
end
|
168
|
+
|
169
|
+
# Name of the resource to be displayed to the client
|
170
|
+
def display_name
|
171
|
+
name
|
172
|
+
end
|
173
|
+
|
174
|
+
# Available properties
|
175
|
+
def property_names
|
176
|
+
%w(creationdate displayname getlastmodified getetag resourcetype getcontenttype getcontentlength)
|
177
|
+
end
|
178
|
+
|
179
|
+
# name:: String - Property name
|
180
|
+
# Returns the value of the given property
|
181
|
+
def get_property(name)
|
182
|
+
case name
|
183
|
+
when 'resourcetype' then resource_type
|
184
|
+
when 'displayname' then display_name
|
185
|
+
when 'creationdate' then creation_date.xmlschema
|
186
|
+
when 'getcontentlength' then content_length.to_s
|
187
|
+
when 'getcontenttype' then content_type
|
188
|
+
when 'getetag' then etag
|
189
|
+
when 'getlastmodified' then last_modified.httpdate
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# name:: String - Property name
|
194
|
+
# value:: New value
|
195
|
+
# Set the property to the given value
|
196
|
+
def set_property(name, value)
|
197
|
+
case name
|
198
|
+
when 'resourcetype' then self.resource_type = value
|
199
|
+
when 'getcontenttype' then self.content_type = value
|
200
|
+
when 'getetag' then self.etag = value
|
201
|
+
when 'getlastmodified' then self.last_modified = Time.httpdate(value)
|
202
|
+
end
|
203
|
+
rescue ArgumentError
|
204
|
+
raise HTTPStatus::Conflict
|
205
|
+
end
|
206
|
+
|
207
|
+
# name:: Property name
|
208
|
+
# Remove the property from the resource
|
209
|
+
def remove_property(name)
|
210
|
+
raise HTTPStatus::Forbidden
|
211
|
+
end
|
212
|
+
|
213
|
+
# name:: Name of child
|
214
|
+
# Create a new child with the given name
|
215
|
+
# NOTE:: Include trailing '/' if child is collection
|
216
|
+
def child(name)
|
217
|
+
new_public = public_path.dup
|
218
|
+
new_public = new_public + '/' unless new_public[-1,1] == '/'
|
219
|
+
new_public = '/' + new_public unless new_public[0,1] == '/'
|
220
|
+
new_path = path.dup
|
221
|
+
new_path = new_path + '/' unless new_path[-1,1] == '/'
|
222
|
+
new_path = '/' + new_path unless new_path[0,1] == '/'
|
223
|
+
self.class.new("#{new_public}#{name}", "#{new_path}#{name}", request, options)
|
224
|
+
end
|
225
|
+
|
226
|
+
# Return parent of this resource
|
227
|
+
def parent
|
228
|
+
elements = @path.scan(/[^\/]+/)
|
229
|
+
return nil if elements.empty?
|
230
|
+
self.class.new(('/' + @public_path.scan(/[^\/]+/)[0..-2].join('/')), ('/' + elements[0..-2].to_a.join('/')), @request, @options)
|
231
|
+
end
|
232
|
+
|
233
|
+
# Return list of descendants
|
234
|
+
def descendants
|
235
|
+
list = []
|
236
|
+
children.each do |child|
|
237
|
+
list << child
|
238
|
+
list.concat(child.descendants)
|
239
|
+
end
|
240
|
+
list
|
241
|
+
end
|
242
|
+
|
243
|
+
# Does client allow GET redirection
|
244
|
+
def allows_redirect?
|
245
|
+
%w(webdrive cyberduck konqueror).any?{|x| (request.respond_to?(:user_agent) ? request.user_agent.downcase : request.env['HTTP_USER_AGENT'].downcase) =~ /#{Regexp.escape(x)}/}
|
246
|
+
end
|
247
|
+
|
248
|
+
end
|
249
|
+
|
250
|
+
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
|