Skip to content
Browse files

Merge pull request #4 from vjt/master

Locking for Office 2010 compatibility
  • Loading branch information...
2 parents 1653337 + 80067b7 commit c81f4404b5a97b08550c667355a3623b03406f0d @georgi georgi committed Jun 16, 2011
Showing with 444 additions and 200 deletions.
  1. +1 −1 Gemfile
  2. +11 −9 Gemfile.lock
  3. +4 −0 README.md
  4. +103 −28 lib/rack_dav/controller.rb
  5. +4 −0 lib/rack_dav/resource.rb
  6. +1 −1 rack_dav.gemspec
  7. +290 −161 spec/controller_spec.rb
  8. +30 −0 spec/support/lockable_file_resource.rb
View
2 Gemfile
@@ -1,4 +1,4 @@
source "http://rubygems.org"
-# Specify your gem's dependencies in rack_Dev.gemspec
+# Specify your gem's dependencies in rack_dav.gemspec
gemspec
View
20 Gemfile.lock
@@ -10,20 +10,22 @@ GEM
specs:
builder (3.0.0)
diff-lcs (1.1.2)
- rack (1.2.1)
- rspec (2.4.0)
- rspec-core (~> 2.4.0)
- rspec-expectations (~> 2.4.0)
- rspec-mocks (~> 2.4.0)
- rspec-core (2.4.0)
- rspec-expectations (2.4.0)
+ rack (1.2.3)
+ rspec (2.6.0)
+ rspec-core (~> 2.6.0)
+ rspec-expectations (~> 2.6.0)
+ rspec-mocks (~> 2.6.0)
+ rspec-core (2.6.3)
+ rspec-expectations (2.6.0)
diff-lcs (~> 1.1.2)
- rspec-mocks (2.4.0)
+ rspec-mocks (2.6.0)
PLATFORMS
java
ruby
DEPENDENCIES
+ builder
+ rack (>= 1.2.0)
rack_dav!
- rspec (~> 2.4.0)
+ rspec (~> 2.6.0)
View
4 README.md
@@ -85,6 +85,10 @@ to retrieve and change the resources:
* __make\_collection__: Create this resource as collection.
+* __lock(token, timeout, scope, type, owner)__: Lock this resource.
+ If scope, type and owner are nil, refresh the given lock.
+
+* __unlock(token)__: Unlock this resource
Note, that it is generally possible, that a resource object is
instantiated for a not yet existing resource.
View
131 lib/rack_dav/controller.rb
@@ -26,8 +26,14 @@ def url_unescape(s)
end
def options
- response["Allow"] = 'OPTIONS,HEAD,GET,PUT,POST,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK'
- response["Dav"] = "1,2"
+ response["Allow"] = 'OPTIONS,HEAD,GET,PUT,POST,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE'
+ response["Dav"] = "1"
+
+ if resource.lockable?
+ response["Allow"] << ",LOCK,UNLOCK"
+ response["Dav"] << ",2"
+ end
+
response["Ms-Author-Via"] = "DAV"
end
@@ -179,39 +185,29 @@ def proppatch
end
def lock
+ raise MethodNotAllowed unless resource.lockable?
raise NotFound if not resource.exist?
- lockscope = request_match("/lockinfo/lockscope/*")[0].name
- locktype = request_match("/lockinfo/locktype/*")[0].name
- owner = request_match("/lockinfo/owner/href")[0]
- locktoken = "opaquelocktoken:" + sprintf('%x-%x-%s', Time.now.to_i, Time.now.sec, resource.etag)
-
- response['Lock-Token'] = locktoken
-
- render_xml do |xml|
- xml.prop('xmlns:D' => "DAV:") do
- xml.lockdiscovery do
- xml.activelock do
- xml.lockscope { xml.tag! lockscope }
- xml.locktype { xml.tag! locktype }
- xml.depth 'Infinity'
- if owner
- xml.owner { xml.href owner.text }
- end
- xml.timeout "Second-60"
- xml.locktoken do
- xml.href locktoken
- end
- end
- end
- end
+ timeout = request_timeout
+ if timeout.nil? || timeout.zero?
+ timeout = 60
+ end
+
+ if request_document.to_s.empty?
+ refresh_lock timeout
+ else
+ create_lock timeout
end
end
def unlock
- raise NoContent
- end
+ raise MethodNotAllowed unless resource.lockable?
+
+ locktoken = request_locktoken('LOCK_TOKEN')
+ raise BadRequest if locktoken.nil?
+ response.status = resource.unlock(locktoken) ? NoContent : Forbidden
+ end
private
@@ -308,6 +304,29 @@ def request_match(pattern)
REXML::XPath::match(request_document, pattern, '' => 'DAV:')
end
+ # Quick and dirty parsing of the WEBDAV Timeout header.
+ # Refuses infinity, rejects anything but Second- timeouts
+ #
+ # @return [nil] or [Fixnum]
+ #
+ # @api internal
+ #
+ def request_timeout
+ timeout = request.env['HTTP_TIMEOUT']
+ return if timeout.nil? || timeout.empty?
+
+ timeout = timeout.split /,\s*/
+ timeout.reject! {|t| t !~ /^Second-/}
+ timeout.first.sub('Second-', '').to_i
+ end
+
+ def request_locktoken(header)
+ token = request.env["HTTP_#{header}"]
+ return if token.nil? || token.empty?
+ token.scan /^\(?<?(.+?)>?\)?$/
+ return $1
+ end
+
# Creates a new XML document, yields given block
# and sets the response.body with the final XML content.
# The response length is updated accordingly.
@@ -399,6 +418,62 @@ def propstats(xml, stats)
end
end
+ def create_lock(timeout)
+ lockscope = request_match("/lockinfo/lockscope/*")[0].name
+ locktype = request_match("/lockinfo/locktype/*")[0].name
+ owner = request_match("/lockinfo/owner/href")[0]
+ owner = owner.text if owner
+ locktoken = "opaquelocktoken:" + sprintf('%x-%x-%s', Time.now.to_i, Time.now.sec, resource.etag)
+
+ # Quick & Dirty - FIXME: Lock should become a new Class
+ # and this dirty parameter passing refactored.
+ unless resource.lock(locktoken, timeout, lockscope, locktype, owner)
+ raise Forbidden
+ end
+
+ response['Lock-Token'] = locktoken
+
+ render_lockdiscovery locktoken, lockscope, locktype, timeout, owner
+ end
+
+ def refresh_lock(timeout)
+ locktoken = request_locktoken('IF')
+ raise BadRequest if locktoken.nil?
+
+ timeout, lockscope, locktype, owner = resource.lock(locktoken, timeout)
+ unless lockscope && locktype && timeout
+ raise Forbidden
+ end
+
+ render_lockdiscovery locktoken, lockscope, locktype, timeout, owner
+ end
+
+ # FIXME add multiple locks support
+ def render_lockdiscovery(locktoken, lockscope, locktype, timeout, owner)
+ render_xml do |xml|
+ xml.prop('xmlns:D' => "DAV:") do
+ xml.lockdiscovery do
+ render_lock(xml, locktoken, lockscope, locktype, timeout, owner)
+ end
+ end
+ end
+ end
+
+ def render_lock(xml, locktoken, lockscope, locktype, timeout, owner)
+ xml.activelock do
+ xml.lockscope { xml.tag! lockscope }
+ xml.locktype { xml.tag! locktype }
+ xml.depth 'Infinity'
+ if owner
+ xml.owner { xml.href owner }
+ end
+ xml.timeout "Second-#{timeout}"
+ xml.locktoken do
+ xml.href locktoken
+ end
+ end
+ end
+
def rexml_convert(xml, element)
if element.elements.empty?
if element.text
View
4 lib/rack_dav/resource.rb
@@ -130,6 +130,10 @@ def child(name, option={})
self.class.new(path + '/' + name, options)
end
+ def lockable?
+ self.respond_to?(:lock) && self.respond_to?(:unlock)
+ end
+
def property_names
%w(creationdate displayname getlastmodified getetag resourcetype getcontenttype getcontentlength)
end
View
2 rack_dav.gemspec
@@ -19,5 +19,5 @@ Gem::Specification.new do |s|
s.add_dependency("rack", ">= 1.2.0")
s.add_dependency("builder")
- s.add_development_dependency("rspec", "~> 2.4.0")
+ s.add_development_dependency("rspec", "~> 2.6.0")
end
View
451 spec/controller_spec.rb
@@ -3,6 +3,8 @@
require 'rack/mock'
+require 'spec/support/lockable_file_resource'
+
class Rack::MockResponse
attr_reader :original_response
@@ -16,15 +18,15 @@ def initialize_with_original(*args)
alias_method :initialize, :initialize_with_original
end
-
describe RackDAV::Handler do
DOC_ROOT = File.expand_path(File.dirname(__FILE__) + '/htdocs')
METHODS = %w(GET PUT POST DELETE PROPFIND PROPPATCH MKCOL COPY MOVE OPTIONS HEAD LOCK UNLOCK)
+ CLASS_2 = METHODS
+ CLASS_1 = CLASS_2 - %w(LOCK UNLOCK)
before do
FileUtils.mkdir(DOC_ROOT) unless File.exists?(DOC_ROOT)
- @controller = RackDAV::Handler.new(:root => DOC_ROOT)
end
after do
@@ -33,224 +35,329 @@ def initialize_with_original(*args)
attr_reader :response
+ context "Given a Lockable resource" do
+ before do
+ @controller = RackDAV::Handler.new(
+ :root => DOC_ROOT,
+ :resource_class => RackDAV::LockableFileResource
+ )
+ end
- describe "OPTIONS" do
- context "/" do
+ describe "OPTIONS" do
it "is successful" do
options('/').should be_ok
end
- it "sets the allow header with all options" do
+ it "sets the allow header with class 2 methods" do
options('/')
- METHODS.each do |method|
+ CLASS_2.each do |method|
response.headers['allow'].should include(method)
end
end
end
- end
+ describe "LOCK" do
+ before(:each) do
+ put("/test", :input => "body").should be_ok
+ lock("/test", :input => File.read(fixture("requests/lock.xml")))
+ end
- describe "LOCK" do
- before(:each) do
- put("/test", :input => "body").should be_ok
- end
+ describe "creation" do
+ it "succeeds" do
+ response.should be_ok
+ end
+
+ it "sets a compliant rack response" do
+ body = response.original_response.body
+ body.should be_a(Array)
+ body.should have(1).part
+ end
+
+ it "prints the lockdiscovery" do
+ lockdiscovery_response response_locktoken
+ end
+ end
+
+ describe "refreshing" do
+ context "a valid locktoken" do
+ it "prints the lockdiscovery" do
+ token = response_locktoken
+ lock("/test", 'HTTP_IF' => "(#{token})").should be_ok
+ lockdiscovery_response token
+ end
- it "sets a compliant rack response" do
- lock("/test", :input => File.read(fixture("requests/lock.xml")))
- body = response.original_response.body
- body.should be_a(Array)
- body.should have(1).part
+ it "accepts it without parenthesis" do
+ token = response_locktoken
+ lock("/test", 'HTTP_IF' => token).should be_ok
+ lockdiscovery_response token
+ end
+
+ it "accepts it with excess angular braces (office 2003)" do
+ token = response_locktoken
+ lock("/test", 'HTTP_IF' => "(<#{token}>)").should be_ok
+ lockdiscovery_response token
+ end
+ end
+
+ context "an invalid locktoken" do
+ it "bails out" do
+ lock("/test", 'HTTP_IF' => '123')
+ response.should be_forbidden
+ response.body.should be_empty
+ end
+ end
+
+ context "no locktoken" do
+ it "bails out" do
+ lock("/test")
+ response.should be_bad_request
+ response.body.should be_empty
+ end
+ end
+
+ end
end
- end
+ describe "UNLOCK" do
+ before(:each) do
+ put("/test", :input => "body").should be_ok
+ lock("/test", :input => File.read(fixture("requests/lock.xml"))).should be_ok
+ end
- it 'should return headers' do
- put('/test.html', :input => '<html/>').should be_ok
- head('/test.html').should be_ok
+ context "given a valid token" do
+ before(:each) do
+ token = response_locktoken
+ unlock("/test", 'HTTP_LOCK_TOKEN' => "(#{token})")
+ end
- response.headers['etag'].should_not be_nil
- response.headers['content-type'].should match(/html/)
- response.headers['last-modified'].should_not be_nil
- end
+ it "unlocks the resource" do
+ response.should be_no_content
+ end
+ end
- it 'should not find a nonexistent resource' do
- get('/not_found').should be_not_found
- end
+ context "given an invalid token" do
+ before(:each) do
+ unlock("/test", 'HTTP_LOCK_TOKEN' => '(123)')
+ end
- it 'should not allow directory traversal' do
- get('/../htdocs').should be_forbidden
- end
+ it "bails out" do
+ response.should be_forbidden
+ end
+ end
- it 'should create a resource and allow its retrieval' do
- put('/test', :input => 'body').should be_ok
- get('/test').should be_ok
- response.body.should == 'body'
- end
- it 'should create and find a url with escaped characters' do
- put(url_escape('/a b'), :input => 'body').should be_ok
- get(url_escape('/a b')).should be_ok
- response.body.should == 'body'
- end
+ context "given no token" do
+ before(:each) do
+ unlock("/test")
+ end
- it 'should delete a single resource' do
- put('/test', :input => 'body').should be_ok
- delete('/test').should be_no_content
+ it "bails out" do
+ response.should be_bad_request
+ end
+ end
+
+ end
end
- it 'should delete recursively' do
- mkcol('/folder').should be_created
- put('/folder/a', :input => 'body').should be_ok
- put('/folder/b', :input => 'body').should be_ok
+ context "Given a not lockable resource" do
+ before do
+ @controller = RackDAV::Handler.new(
+ :root => DOC_ROOT,
+ :resource_class => RackDAV::FileResource
+ )
+ end
- delete('/folder').should be_no_content
- get('/folder').should be_not_found
- get('/folder/a').should be_not_found
- get('/folder/b').should be_not_found
- end
+ describe "OPTIONS" do
+ it "is successful" do
+ options('/').should be_ok
+ end
- it 'should not allow copy to another domain' do
- put('/test', :input => 'body').should be_ok
- copy('http://localhost/', 'HTTP_DESTINATION' => 'http://another/').should be_bad_gateway
- end
+ it "sets the allow header with class 2 methods" do
+ options('/')
+ CLASS_1.each do |method|
+ response.headers['allow'].should include(method)
+ end
+ end
+ end
- it 'should not allow copy to the same resource' do
- put('/test', :input => 'body').should be_ok
- copy('/test', 'HTTP_DESTINATION' => '/test').should be_forbidden
- end
+ it 'should return headers' do
+ put('/test.html', :input => '<html/>').should be_ok
+ head('/test.html').should be_ok
- it 'should not allow an invalid destination uri' do
- put('/test', :input => 'body').should be_ok
- copy('/test', 'HTTP_DESTINATION' => '%').should be_bad_request
- end
+ response.headers['etag'].should_not be_nil
+ response.headers['content-type'].should match(/html/)
+ response.headers['last-modified'].should_not be_nil
+ end
- it 'should copy a single resource' do
- put('/test', :input => 'body').should be_ok
- copy('/test', 'HTTP_DESTINATION' => '/copy').should be_created
- get('/copy').body.should == 'body'
- end
+ it 'should not find a nonexistent resource' do
+ get('/not_found').should be_not_found
+ end
- it 'should copy a resource with escaped characters' do
- put(url_escape('/a b'), :input => 'body').should be_ok
- copy(url_escape('/a b'), 'HTTP_DESTINATION' => url_escape('/a c')).should be_created
- get(url_escape('/a c')).should be_ok
- response.body.should == 'body'
- end
+ it 'should not allow directory traversal' do
+ get('/../htdocs').should be_forbidden
+ end
- it 'should deny a copy without overwrite' do
- put('/test', :input => 'body').should be_ok
- put('/copy', :input => 'copy').should be_ok
- copy('/test', 'HTTP_DESTINATION' => '/copy', 'HTTP_OVERWRITE' => 'F')
+ it 'should create a resource and allow its retrieval' do
+ put('/test', :input => 'body').should be_ok
+ get('/test').should be_ok
+ response.body.should == 'body'
+ end
+ it 'should create and find a url with escaped characters' do
+ put(url_escape('/a b'), :input => 'body').should be_ok
+ get(url_escape('/a b')).should be_ok
+ response.body.should == 'body'
+ end
- multistatus_response('/href').first.text.should == 'http://localhost/test'
- multistatus_response('/status').first.text.should match(/412 Precondition Failed/)
+ it 'should delete a single resource' do
+ put('/test', :input => 'body').should be_ok
+ delete('/test').should be_no_content
+ end
- get('/copy').body.should == 'copy'
- end
+ it 'should delete recursively' do
+ mkcol('/folder').should be_created
+ put('/folder/a', :input => 'body').should be_ok
+ put('/folder/b', :input => 'body').should be_ok
- it 'should allow a copy with overwrite' do
- put('/test', :input => 'body').should be_ok
- put('/copy', :input => 'copy').should be_ok
- copy('/test', 'HTTP_DESTINATION' => '/copy', 'HTTP_OVERWRITE' => 'T').should be_no_content
- get('/copy').body.should == 'body'
- end
+ delete('/folder').should be_no_content
+ get('/folder').should be_not_found
+ get('/folder/a').should be_not_found
+ get('/folder/b').should be_not_found
+ end
- it 'should copy a collection' do
- mkcol('/folder').should be_created
- copy('/folder', 'HTTP_DESTINATION' => '/copy').should be_created
- propfind('/copy', :input => propfind_xml(:resourcetype))
- multistatus_response('/propstat/prop/resourcetype/collection').should_not be_empty
- end
+ it 'should not allow copy to another domain' do
+ put('/test', :input => 'body').should be_ok
+ copy('http://localhost/', 'HTTP_DESTINATION' => 'http://another/').should be_bad_gateway
+ end
- it 'should copy a collection resursively' do
- mkcol('/folder').should be_created
- put('/folder/a', :input => 'A').should be_ok
- put('/folder/b', :input => 'B').should be_ok
+ it 'should not allow copy to the same resource' do
+ put('/test', :input => 'body').should be_ok
+ copy('/test', 'HTTP_DESTINATION' => '/test').should be_forbidden
+ end
- copy('/folder', 'HTTP_DESTINATION' => '/copy').should be_created
- propfind('/copy', :input => propfind_xml(:resourcetype))
- multistatus_response('/propstat/prop/resourcetype/collection').should_not be_empty
+ it 'should not allow an invalid destination uri' do
+ put('/test', :input => 'body').should be_ok
+ copy('/test', 'HTTP_DESTINATION' => '%').should be_bad_request
+ end
- get('/copy/a').body.should == 'A'
- get('/copy/b').body.should == 'B'
- end
+ it 'should copy a single resource' do
+ put('/test', :input => 'body').should be_ok
+ copy('/test', 'HTTP_DESTINATION' => '/copy').should be_created
+ get('/copy').body.should == 'body'
+ end
- it 'should move a collection recursively' do
- mkcol('/folder').should be_created
- put('/folder/a', :input => 'A').should be_ok
- put('/folder/b', :input => 'B').should be_ok
+ it 'should copy a resource with escaped characters' do
+ put(url_escape('/a b'), :input => 'body').should be_ok
+ copy(url_escape('/a b'), 'HTTP_DESTINATION' => url_escape('/a c')).should be_created
+ get(url_escape('/a c')).should be_ok
+ response.body.should == 'body'
+ end
- move('/folder', 'HTTP_DESTINATION' => '/move').should be_created
- propfind('/move', :input => propfind_xml(:resourcetype))
- multistatus_response('/propstat/prop/resourcetype/collection').should_not be_empty
+ it 'should deny a copy without overwrite' do
+ put('/test', :input => 'body').should be_ok
+ put('/copy', :input => 'copy').should be_ok
+ copy('/test', 'HTTP_DESTINATION' => '/copy', 'HTTP_OVERWRITE' => 'F')
- get('/move/a').body.should == 'A'
- get('/move/b').body.should == 'B'
- get('/folder/a').should be_not_found
- get('/folder/b').should be_not_found
- end
+ multistatus_response('/href').first.text.should == 'http://localhost/test'
+ multistatus_response('/status').first.text.should match(/412 Precondition Failed/)
- it 'should create a collection' do
- mkcol('/folder').should be_created
- propfind('/folder', :input => propfind_xml(:resourcetype))
- multistatus_response('/propstat/prop/resourcetype/collection').should_not be_empty
- end
+ get('/copy').body.should == 'copy'
+ end
- it 'should not find properties for nonexistent resources' do
- propfind('/non').should be_not_found
- end
+ it 'should allow a copy with overwrite' do
+ put('/test', :input => 'body').should be_ok
+ put('/copy', :input => 'copy').should be_ok
+ copy('/test', 'HTTP_DESTINATION' => '/copy', 'HTTP_OVERWRITE' => 'T').should be_no_content
+ get('/copy').body.should == 'body'
+ end
- it 'should find all properties' do
- xml = render do |xml|
- xml.propfind('xmlns:d' => "DAV:") do
- xml.allprop
- end
+ it 'should copy a collection' do
+ mkcol('/folder').should be_created
+ copy('/folder', 'HTTP_DESTINATION' => '/copy').should be_created
+ propfind('/copy', :input => propfind_xml(:resourcetype))
+ multistatus_response('/propstat/prop/resourcetype/collection').should_not be_empty
end
- propfind('http://localhost/', :input => xml)
+ it 'should copy a collection resursively' do
+ mkcol('/folder').should be_created
+ put('/folder/a', :input => 'A').should be_ok
+ put('/folder/b', :input => 'B').should be_ok
- multistatus_response('/href').first.text.strip.should == 'http://localhost/'
+ copy('/folder', 'HTTP_DESTINATION' => '/copy').should be_created
+ propfind('/copy', :input => propfind_xml(:resourcetype))
+ multistatus_response('/propstat/prop/resourcetype/collection').should_not be_empty
- props = %w(creationdate displayname getlastmodified getetag resourcetype getcontenttype getcontentlength)
- props.each do |prop|
- multistatus_response('/propstat/prop/' + prop).should_not be_empty
+ get('/copy/a').body.should == 'A'
+ get('/copy/b').body.should == 'B'
end
- end
- it 'should find named properties' do
- put('/test.html', :input => '<html/>').should be_ok
- propfind('/test.html', :input => propfind_xml(:getcontenttype, :getcontentlength))
+ it 'should move a collection recursively' do
+ mkcol('/folder').should be_created
+ put('/folder/a', :input => 'A').should be_ok
+ put('/folder/b', :input => 'B').should be_ok
- multistatus_response('/propstat/prop/getcontenttype').first.text.should == 'text/html'
- multistatus_response('/propstat/prop/getcontentlength').first.text.should == '7'
- end
+ move('/folder', 'HTTP_DESTINATION' => '/move').should be_created
+ propfind('/move', :input => propfind_xml(:resourcetype))
+ multistatus_response('/propstat/prop/resourcetype/collection').should_not be_empty
+
+ get('/move/a').body.should == 'A'
+ get('/move/b').body.should == 'B'
+ get('/folder/a').should be_not_found
+ get('/folder/b').should be_not_found
+ end
+
+ it 'should create a collection' do
+ mkcol('/folder').should be_created
+ propfind('/folder', :input => propfind_xml(:resourcetype))
+ multistatus_response('/propstat/prop/resourcetype/collection').should_not be_empty
+ end
+
+ it 'should not find properties for nonexistent resources' do
+ propfind('/non').should be_not_found
+ end
+
+ it 'should find all properties' do
+ xml = render do |xml|
+ xml.propfind('xmlns:d' => "DAV:") do
+ xml.allprop
+ end
+ end
+
+ propfind('http://localhost/', :input => xml)
- it 'should lock a resource' do
- put('/test', :input => 'body').should be_ok
+ multistatus_response('/href').first.text.strip.should == 'http://localhost/'
- xml = render do |xml|
- xml.lockinfo('xmlns:d' => "DAV:") do
- xml.lockscope { xml.exclusive }
- xml.locktype { xml.write }
- xml.owner { xml.href "http://test.de/" }
+ props = %w(creationdate displayname getlastmodified getetag resourcetype getcontenttype getcontentlength)
+ props.each do |prop|
+ multistatus_response('/propstat/prop/' + prop).should_not be_empty
end
end
- lock('/test', :input => xml)
+ it 'should find named properties' do
+ put('/test.html', :input => '<html/>').should be_ok
+ propfind('/test.html', :input => propfind_xml(:getcontenttype, :getcontentlength))
- response.should be_ok
+ multistatus_response('/propstat/prop/getcontenttype').first.text.should == 'text/html'
+ multistatus_response('/propstat/prop/getcontentlength').first.text.should == '7'
+ end
+
+ it 'should not support LOCK' do
+ put('/test', :input => 'body').should be_ok
- match = lambda do |pattern|
- REXML::XPath::match(response_xml, "/prop/lockdiscovery/activelock" + pattern, '' => 'DAV:')
+ xml = render do |xml|
+ xml.lockinfo('xmlns:d' => "DAV:") do
+ xml.lockscope { xml.exclusive }
+ xml.locktype { xml.write }
+ xml.owner { xml.href "http://test.de/" }
+ end
+ end
+
+ lock('/test', :input => xml).should be_method_not_allowed
end
- match[''].should_not be_empty
+ it 'should not support UNLOCK' do
+ put('/test', :input => 'body').should be_ok
+ unlock('/test', :input => '').should be_method_not_allowed
+ end
- match['/locktype'].should_not be_empty
- match['/lockscope'].should_not be_empty
- match['/depth'].should_not be_empty
- match['/owner'].should_not be_empty
- match['/timeout'].should_not be_empty
- match['/locktoken'].should_not be_empty
end
@@ -288,7 +395,29 @@ def url_escape(string)
end
def response_xml
- REXML::Document.new(@response.body)
+ @response_xml ||= REXML::Document.new(@response.body)
+ end
+
+ def response_locktoken
+ REXML::XPath::match(response_xml,
+ "/prop/lockdiscovery/activelock/locktoken/href", '' => 'DAV:'
+ ).first.text
+ end
+
+ def lockdiscovery_response(token)
+ match = lambda do |pattern|
+ REXML::XPath::match(response_xml, "/prop/lockdiscovery/activelock" + pattern, '' => 'DAV:')
+ end
+
+ match[''].should_not be_empty
+
+ match['/locktype'].should_not be_empty
+ match['/lockscope'].should_not be_empty
+ match['/depth'].should_not be_empty
+ match['/owner'].should_not be_empty
+ match['/timeout'].should_not be_empty
+ match['/locktoken/href'].should_not be_empty
+ match['/locktoken/href'].first.text.should == token
end
def multistatus_response(pattern)
View
30 spec/support/lockable_file_resource.rb
@@ -0,0 +1,30 @@
+module RackDAV
+
+ # Quick & Dirty
+ class LockableFileResource < FileResource
+ @@locks = {}
+
+ def lock(token, timeout, scope = nil, type = nil, owner = nil)
+ if scope && type && owner
+ # Create lock
+ @@locks[token] = {
+ :timeout => timeout,
+ :scope => scope,
+ :type => type,
+ :owner => owner
+ }
+ return true
+ else
+ # Refresh lock
+ lock = @@locks[token]
+ return false unless lock
+ return [ lock[:timeout], lock[:scope], lock[:type], lock[:owner] ]
+ end
+ end
+
+ def unlock(token)
+ !!@@locks.delete(token)
+ end
+ end
+
+end

0 comments on commit c81f440

Please sign in to comment.
Something went wrong with that request. Please try again.