# frozen_string_literal: true require_relative './environment.rb' require 'rack/test' require 'nokogiri' require 'rack/utils' describe 'webdav' do include Rack::Test::Methods def app @app ||= begin builder = Rack::Builder.parse_file('config.ru') builder = builder.first if builder.is_a?(Array) builder = builder.to_app if builder.respond_to?(:to_app) builder end end before do @site = Fabricate(:site) @site.update(plan_type: 'supporter') ThumbnailWorker.jobs.clear PurgeCacheWorker.jobs.clear ScreenshotWorker.jobs.clear end def auth_put(path, body) basic_authorize @site.username, 'abcde' put "/webdav/#{path}", body end def auth_get(path) basic_authorize @site.username, 'abcde' get "/webdav/#{path}" end def auth_head(path) basic_authorize @site.username, 'abcde' head "/webdav/#{path}" end def auth_propfind(path, depth='1') basic_authorize @site.username, 'abcde' header 'Depth', depth request "/webdav/#{path}", method: 'PROPFIND' header 'Depth', nil end def auth_mkcol(path) basic_authorize @site.username, 'abcde' request "/webdav/#{path}", method: 'MKCOL' end def auth_delete(path) basic_authorize @site.username, 'abcde' delete "/webdav/#{path}" end def auth_move(from_path, to_path) basic_authorize @site.username, 'abcde' header 'Destination', "http://example.org/webdav/#{to_path}" request "/webdav/#{from_path}", method: 'MOVE' header 'Destination', nil end it 'creates files via PUT' do auth_put 'create.txt', 'hello webdav' _(last_response.status).must_equal 201 _(Site[@site.id].file_exists?('create.txt')).must_equal true end it 'decodes percent-encoded paths when storing files' do encoded_path = 'Alien%20%281%29.jpg' decoded_path = 'Alien (1).jpg' auth_put encoded_path, 'image data' _(last_response.status).must_equal 201 site = Site[@site.id] _(site.file_exists?(decoded_path)).must_equal true _(site.site_files_dataset.where(path: decoded_path).count).must_equal 1 _(site.site_files_dataset.where(path: encoded_path).count).must_equal 0 end it 'reads files via GET' do auth_put 'read.txt', 'read me' auth_get 'read.txt' _(last_response.status).must_equal 200 _(last_response.body).must_equal 'read me' end it 'provides metadata via HEAD without a body' do auth_put 'meta.txt', 'meta' auth_head 'meta.txt' _(last_response.status).must_equal 200 _(last_response.body).must_equal '' _(last_response.headers['Content-Length']).wont_be_nil _(last_response.headers['Content-Type']).must_equal 'text/plain' site_file = Site[@site.id].site_files_dataset.where(path: 'meta.txt').first _(site_file).wont_be_nil header_time = Time.httpdate(last_response.headers['Last-Modified']) _(header_time.to_i).must_equal site_file.updated_at.to_i end it 'updates files via MOVE' do auth_put 'update.txt', 'content' auth_move 'update.txt', 'renamed.txt' _(last_response.status).must_equal 201 site = Site[@site.id] _(site.file_exists?('update.txt')).must_equal false _(site.file_exists?('renamed.txt')).must_equal true end it 'strips leading and trailing slashes when moving files' do auth_put 'slashy.txt', 'content' auth_move 'slashy.txt/', '/renamed.txt/' _(last_response.status).must_equal 201 site = Site[@site.id] _(site.file_exists?('slashy.txt')).must_equal false _(site.file_exists?('renamed.txt')).must_equal true end it 'fails MOVE when destination header is empty' do auth_put 'move.txt', 'body' basic_authorize @site.username, 'abcde' header 'Destination', '' request '/webdav/move.txt', method: 'MOVE' header 'Destination', nil _(last_response.status).must_equal 400 end it 'deletes files via DELETE' do auth_put 'delete.txt', 'gone' auth_delete 'delete.txt' _(last_response.status).must_equal 201 _(Site[@site.id].file_exists?('delete.txt')).must_equal false end it 'rejects GET requests for directories' do auth_mkcol 'dir' auth_get 'dir' _(last_response.status).must_equal 403 _(last_response.body).must_include 'directory' end it 'rejects DELETE of index.html' do auth_put 'index.html', 'index' auth_delete 'index.html' _(last_response.status).must_equal 403 _(last_response.body).must_include 'index.html' end it 'prevents PUT when a directory exists at the path' do auth_mkcol 'dup' auth_put 'dup', 'nope' _(last_response.status).must_equal 409 _(last_response.body).must_include 'conflicts with an existing directory' end it 'rejects MKCOL with invalid paths' do auth_mkcol '../bad' _(last_response.status).must_equal 400 _(last_response.body).must_include 'Invalid path' end it 'returns 401 for unauthenticated requests' do get '/webdav/test.txt' _(last_response.status).must_equal 401 _(last_response.headers['WWW-Authenticate']).must_include 'Basic' end it 'requires supporter accounts' do free_site = Fabricate(:site) free_site.update(plan_type: 'free') basic_authorize free_site.username, 'abcde' get '/webdav/test.txt' _(last_response.status).must_equal 402 _(last_response.headers['X-Upgrade-Required']).must_include 'supporter' end it 'lists resources with PROPFIND depth 0 on root' do auth_propfind '', '0' _(last_response.status).must_equal 207 doc = Nokogiri::XML(last_response.body) hrefs = doc.xpath('//D:href', 'D' => 'DAV:').map(&:text) _(hrefs).must_include('/webdav/') end it 'lists children with PROPFIND depth 1' do auth_mkcol 'folder' auth_put 'folder/file.txt', 'data' auth_propfind 'folder', '1' _(last_response.status).must_equal 207 doc = Nokogiri::XML(last_response.body) hrefs = doc.xpath('//D:href', 'D' => 'DAV:').map(&:text) _(hrefs).must_include('/webdav/folder/') _(hrefs).must_include('/webdav/folder/file.txt') end it 'returns 404 for PROPFIND on missing paths' do auth_propfind 'missing.txt', '0' _(last_response.status).must_equal 404 end end