$:.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib')) require 'rubygems' require 'dav4rack' require 'fileutils' require 'nokogiri' require 'rspec' describe DAV4Rack::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) before do FileUtils.mkdir(DOC_ROOT) unless File.exists?(DOC_ROOT) @controller = DAV4Rack::Handler.new(:root => DOC_ROOT) end after do FileUtils.rm_rf(DOC_ROOT) if File.exists?(DOC_ROOT) end attr_reader :response def request(method, uri, options={}) options = { 'HTTP_HOST' => 'localhost', 'REMOTE_USER' => 'user' }.merge(options) request = Rack::MockRequest.new(@controller) @response = request.request(method, uri, options) end METHODS.each do |method| define_method(method.downcase) do |*args| request(method, *args) end end def render(root_type) raise ArgumentError.new 'Expecting block' unless block_given? doc = Nokogiri::XML::Builder.new do |xml_base| xml_base.send(root_type.to_s, 'xmlns:D' => 'D:') do xml_base.parent.namespace = xml_base.parent.namespace_definitions.first xml = xml_base['D'] yield xml end end doc.to_xml end def url_escape(string) URI.escape(string) end def response_xml Nokogiri.XML(@response.body) end def multistatus_response(pattern) @response.should be_multi_status response_xml.xpath('//D:multistatus/D:response', response_xml.root.namespaces).should_not be_empty response_xml.xpath("//D:multistatus/D:response#{pattern}", response_xml.root.namespaces) end def multi_status_created response_xml.xpath('//D:multistatus/D:response/D:status').should_not be_empty response_xml.xpath('//D:multistatus/D:response/D:status').text.should =~ /Created/ end def multi_status_ok response_xml.xpath('//D:multistatus/D:response/D:status').should_not be_empty response_xml.xpath('//D:multistatus/D:response/D:status').text.should =~ /OK/ end def multi_status_no_content response_xml.xpath('//D:multistatus/D:response/D:status').should_not be_empty response_xml.xpath('//D:multistatus/D:response/D:status').text.should =~ /No Content/ end def propfind_xml(*props) render(:propfind) do |xml| xml.prop do props.each do |prop| xml.send(prop.to_sym) end end end end it 'should return all options' do options('/').should be_ok METHODS.each do |method| response.headers['allow'].should include(method) end end it 'should return headers' do put('/test.html', :input => '').should be_created head('/test.html').should be_ok 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 not find a nonexistent resource' do get('/not_found').should be_not_found end it 'should not allow directory traversal' do get('/../htdocs').should be_forbidden end it 'should create a resource and allow its retrieval' do put('/test', :input => 'body').should be_created get('/test').should be_ok response.body.should == 'body' end it 'should return an absolute url after a put request' do put('/test', :input => 'body').should be_created response['location'].should =~ /http:\/\/localhost(:\d+)?\/test/ end it 'should create and find a url with escaped characters' do put(url_escape('/a b'), :input => 'body').should be_created get(url_escape('/a b')).should be_ok response.body.should == 'body' end it 'should delete a single resource' do put('/test', :input => 'body').should be_created delete('/test').should be_no_content end it 'should delete recursively' do mkcol('/folder').should be_created put('/folder/a', :input => 'body').should be_created put('/folder/b', :input => 'body').should be_created 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 not allow copy to another domain' do put('/test', :input => 'body').should be_created copy('http://localhost/', 'HTTP_DESTINATION' => 'http://another/').should be_bad_gateway end it 'should not allow copy to the same resource' do put('/test', :input => 'body').should be_created copy('/test', 'HTTP_DESTINATION' => '/test').should be_forbidden end it 'should copy a single resource' do put('/test', :input => 'body').should be_created copy('/test', 'HTTP_DESTINATION' => '/copy').should be_created get('/copy').body.should == 'body' end it 'should copy a resource with escaped characters' do put(url_escape('/a b'), :input => 'body').should be_created 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 deny a copy without overwrite' do put('/test', :input => 'body').should be_created put('/copy', :input => 'copy').should be_created copy('/test', 'HTTP_DESTINATION' => '/copy', 'HTTP_OVERWRITE' => 'F').should be_precondition_failed get('/copy').body.should == 'copy' end it 'should allow a copy with overwrite' do put('/test', :input => 'body').should be_created put('/copy', :input => 'copy').should be_created copy('/test', 'HTTP_DESTINATION' => '/copy', 'HTTP_OVERWRITE' => 'T').should be_no_content get('/copy').body.should == 'body' end it 'should copy a collection' do mkcol('/folder').should be_created copy('/folder', 'HTTP_DESTINATION' => '/copy') multi_status_created.should eq true propfind('/copy', :input => propfind_xml(:resourcetype)) multistatus_response('/D:propstat/D:prop/D:resourcetype/D:collection').should_not be_empty end it 'should copy a collection resursively' do mkcol('/folder').should be_created put('/folder/a', :input => 'A').should be_created put('/folder/b', :input => 'B').should be_created copy('/folder', 'HTTP_DESTINATION' => '/copy') multi_status_created.should eq true propfind('/copy', :input => propfind_xml(:resourcetype)) multistatus_response('/D:propstat/D:prop/D:resourcetype/D:collection').should_not be_empty get('/copy/a').body.should == 'A' get('/copy/b').body.should == 'B' end it 'should move a collection recursively' do mkcol('/folder').should be_created put('/folder/a', :input => 'A').should be_created put('/folder/b', :input => 'B').should be_created move('/folder', 'HTTP_DESTINATION' => '/move') multi_status_created.should eq true propfind('/move', :input => propfind_xml(:resourcetype)) multistatus_response('/D:propstat/D:prop/D:resourcetype/D: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('/D:propstat/D:prop/D:resourcetype/D:collection').should_not be_empty end it 'should return full urls after creating a collection' do mkcol('/folder').should be_created propfind('/folder', :input => propfind_xml(:resourcetype)) multistatus_response('/D:propstat/D:prop/D:resourcetype/D:collection').should_not be_empty multistatus_response('/D:href').first.text.should =~ /http:\/\/localhost(:\d+)?\/folder/ 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(:propfind) do |xml| xml.allprop end propfind('http://localhost/', :input => xml) multistatus_response('/D:href').first.text.strip.should =~ /http:\/\/localhost(:\d+)?\// props = %w(creationdate displayname getlastmodified getetag resourcetype getcontenttype getcontentlength) props.each do |prop| multistatus_response("/D:propstat/D:prop/D:#{prop}").should_not be_empty end end it 'should find named properties' do put('/test.html', :input => '').should be_created propfind('/test.html', :input => propfind_xml(:getcontenttype, :getcontentlength)) multistatus_response('/D:propstat/D:prop/D:getcontenttype').first.text.should == 'text/html' multistatus_response('/D:propstat/D:prop/D:getcontentlength').first.text.should == '7' end it 'should lock a resource' do put('/test', :input => 'body').should be_created xml = render(:lockinfo) do |xml| xml.lockscope { xml.exclusive } xml.locktype { xml.write } xml.owner { xml.href "http://test.de/" } end lock('/test', :input => xml) response.should be_ok match = lambda do |pattern| response_xml.xpath "/D:prop/D:lockdiscovery/D:activelock#{pattern}" end match[''].should_not be_empty match['/D:locktype'].should_not be_empty match['/D:lockscope'].should_not be_empty match['/D:depth'].should_not be_empty match['/D:timeout'].should_not be_empty match['/D:locktoken'].should_not be_empty match['/D:owner'].should_not be_empty end context "when mapping a path" do before do @controller = DAV4Rack::Handler.new(:root => DOC_ROOT, :root_uri_path => '/webdav/') end it "should return correct urls" do # FIXME: a put to '/test' works, too -- should it? put('/webdav/test', :input => 'body').should be_created response.headers['location'].should =~ /http:\/\/localhost(:\d+)?\/webdav\/test/ end end end