webdav 0.0.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1721f681cfb816034b039f48242101233a096ac8ca65eee54ff8e7cac6e419eb
4
+ data.tar.gz: 3c8cd82ee3191f3cb957b63c4be8c2d86f362fb470bbd9f5989f8548a417580a
5
+ SHA512:
6
+ metadata.gz: f8af2a0aaa9a4388ede4af98c93a0303b27c803a6233ec357e0a118098bd95afdd032f005e8c075d0133ad6fc9159871bf24007b1165b0dbadf5b1b281cb06a3
7
+ data.tar.gz: 7b680af432e9913a56c163c199d9908cdbc657bf4246175e8a3cd76030116adb66764c8cfc8b270c5a6e575875032d597a63be23fad374a3f6b48e57c85cd800
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 thoran
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # webdav
2
+
3
+ A Ruby WebDAV client library.
4
+
5
+ ## Installation
6
+
7
+ ```
8
+ gem install webdav
9
+ ```
10
+
11
+ Or in your Gemfile:
12
+
13
+ ```ruby
14
+ gem 'webdav'
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```ruby
20
+ require 'webdav'
21
+
22
+ dav = WebDAV.new('https://dav.example.com/files/', username: 'user', password: 'pass')
23
+ ```
24
+
25
+ ### Discovering resources
26
+
27
+ ```ruby
28
+ response = dav.propfind('/', depth: '1')
29
+ response.resources.each do |resource|
30
+ puts resource[:href]
31
+ puts resource[:properties]
32
+ end
33
+ ```
34
+
35
+ ### Reading a resource
36
+
37
+ ```ruby
38
+ response = dav.get('/documents/report.txt')
39
+ puts response.body
40
+ ```
41
+
42
+ ### Writing a resource
43
+
44
+ ```ruby
45
+ dav.put('/documents/report.txt', body: 'Hello, world.', content_type: 'text/plain')
46
+ ```
47
+
48
+ ### Deleting a resource
49
+
50
+ ```ruby
51
+ dav.delete('/documents/report.txt')
52
+ ```
53
+
54
+ ### Creating a collection
55
+
56
+ ```ruby
57
+ dav.mkcol('/documents/archive/')
58
+ ```
59
+
60
+ ### Copying and moving
61
+
62
+ ```ruby
63
+ dav.copy('/documents/report.txt', to: '/archive/report.txt')
64
+ dav.move('/documents/draft.txt', to: '/documents/final.txt')
65
+ ```
66
+
67
+ ### Locking and unlocking
68
+
69
+ ```ruby
70
+ lock_body = <<~XML
71
+ <?xml version="1.0" encoding="UTF-8"?>
72
+ <d:lockinfo xmlns:d="DAV:">
73
+ <d:lockscope><d:exclusive/></d:lockscope>
74
+ <d:locktype><d:write/></d:locktype>
75
+ </d:lockinfo>
76
+ XML
77
+
78
+ response = dav.lock('/documents/report.txt', body: lock_body)
79
+ # ...
80
+ dav.unlock('/documents/report.txt', token: 'urn:uuid:...')
81
+ ```
82
+
83
+ ### REPORT
84
+
85
+ ```ruby
86
+ response = dav.report('/calendars/user/', body: report_xml, depth: '1')
87
+ response.resources.each do |resource|
88
+ puts resource[:href]
89
+ end
90
+ ```
91
+
92
+ ## Verbs
93
+
94
+ ### WebDAV (RFC 4918 / RFC 3253)
95
+
96
+ - `propfind(path, body:, depth:)` — Retrieve properties
97
+ - `proppatch(path, body:)` — Modify properties
98
+ - `report(path, body:, depth:)` — Run a report query
99
+ - `mkcol(path)` — Create a collection
100
+ - `copy(path, to:, depth:, overwrite:)` — Copy a resource
101
+ - `move(path, to:, overwrite:)` — Move a resource
102
+ - `lock(path, body:)` — Lock a resource
103
+ - `unlock(path, token:)` — Unlock a resource
104
+
105
+ ### Standard HTTP
106
+
107
+ - `get(path)`
108
+ - `head(path)`
109
+ - `post(path, body:, content_type:)`
110
+ - `put(path, body:, content_type:)`
111
+ - `patch(path, body:, content_type:)`
112
+ - `delete(path)`
113
+ - `options(path)`
114
+ - `trace(path)`
115
+
116
+ ## Responses
117
+
118
+ All methods return either a `WebDAV::Response` or a `WebDAV::MultiStatus`.
119
+
120
+ `WebDAV::Response` provides
121
+
122
+ - `code`
123
+ - `message`
124
+ - `headers`
125
+ - `body`
126
+ - `etag`
127
+ - `content_type`
128
+ - `success?`
129
+
130
+ `WebDAV::MultiStatus` additionally provides:
131
+
132
+ - `resources`
133
+ — an array of hashes, each with:
134
+ - `href`
135
+ - `properties`
136
+ - `status`
137
+
138
+ ## Errors
139
+
140
+ Responses with status >= 400 raise `WebDAV::Error`, which has `code`, `message`, and `body`.
141
+
142
+ ## Dependencies
143
+
144
+ - [http.rb](https://github.com/thoran/http.rb)
145
+
146
+ ## Licence
147
+
148
+ MIT
@@ -0,0 +1,16 @@
1
+ # Net/HTTP/Report.rb
2
+ # Net::HTTP::Report
3
+
4
+ require 'net/http'
5
+
6
+ module Net
7
+ class HTTP
8
+ class Report < Net::HTTPRequest
9
+
10
+ METHOD = 'REPORT'
11
+ REQUEST_HAS_BODY = true
12
+ RESPONSE_HAS_BODY = true
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,7 @@
1
+ # String/to_const.rb
2
+ # String#to_const
3
+
4
+ # 20250819
5
+ # 0.2.0
6
+
7
+ require 'Thoran/String/ToConst/to_const'
@@ -0,0 +1,28 @@
1
+ # Thoran/Array/AllButFirst/all_but_first.rb
2
+ # Thoran::Array::AllButFirst#all_but_first
3
+
4
+ # 20141223
5
+ # 0.1.0
6
+
7
+ # Description: This returns a copy of the receiving array with the first element removed.
8
+
9
+ # Changes:
10
+ # 1. + Thoran namespace.
11
+
12
+ require 'Thoran/Array/FirstX/firstX'
13
+
14
+ module Thoran
15
+ module Array
16
+ module AllButFirst
17
+
18
+ def all_but_first
19
+ d = self.dup
20
+ d.first!
21
+ d
22
+ end
23
+
24
+ end
25
+ end
26
+ end
27
+
28
+ Array.send(:include, Thoran::Array::AllButFirst)
@@ -0,0 +1,32 @@
1
+ # Thoran/Array/FirstX/firstX.rb
2
+ # Thoran::Array::FirstX#first!
3
+
4
+ # 20180804
5
+ # 0.3.3
6
+
7
+ # Description: Sometimes it makes more sense to treat arrays this way.
8
+
9
+ # Changes since 0.2:
10
+ # 1. Added the original version 0.1.0 of the implementation to the later 0.1.0!
11
+ # 0/1
12
+ # 2. Switched the tests to spec-style.
13
+ # 1/2
14
+ # 3. Added a test for the state of the array afterward, since this is meant to be an in place change.
15
+ # 2/3
16
+ # 4. Added tests for the extended functionality introduced in the first version 0.1.0.
17
+
18
+ module Thoran
19
+ module Array
20
+ module FirstX
21
+
22
+ def first!(n = 1)
23
+ return_value = []
24
+ n.times{return_value << self.shift}
25
+ return_value.size == 1 ? return_value[0] : return_value
26
+ end
27
+
28
+ end
29
+ end
30
+ end
31
+
32
+ Array.send(:include, Thoran::Array::FirstX)
@@ -0,0 +1,47 @@
1
+ # Thoran/String/ToConst/to_const.rb
2
+ # Thoran::String::ToConst#to_const
3
+
4
+ # 20141223
5
+ # 0.2.0
6
+
7
+ # Description: This takes a string and returns a constant, with unlimited namespacing.
8
+
9
+ # History: Derived from Object#to_const 0.3.0, and superceding Object#to_const.
10
+
11
+ # Changes:
12
+ # 1. + Thoran namespace.
13
+
14
+ # Todo:
15
+ # 1. This only works for two levels of constants. Three and you're stuffed. So, this needs to be recursive...
16
+ # Done iteratively as of 0.1.0.
17
+ # 2. Make this work for symbols. However, this will only work if there's no namespacing. ie. :A is OK, but :A::B is not.
18
+
19
+ # Discussion:
20
+ # 1. Should this go separately into classes for which ::const_get will work and be removed from Object? Done as of 0.1.0.
21
+
22
+ require 'Thoran/Array/AllButFirst/all_but_first'
23
+
24
+ module Thoran
25
+ module String
26
+ module ToConst
27
+
28
+ def to_const
29
+ if self =~ /::/
30
+ constants = self.split('::')
31
+ constant = Object.const_get(constants.first)
32
+ constants = constants.all_but_first
33
+ until constants.empty? do
34
+ constant = constant.const_get(constants.shift)
35
+ end
36
+ else
37
+ constant = Object.const_get(self)
38
+ end
39
+ constant
40
+ end
41
+ alias_method :to_constant, :to_const
42
+
43
+ end
44
+ end
45
+ end
46
+
47
+ String.send(:include, Thoran::String::ToConst)
@@ -0,0 +1,17 @@
1
+ # WebDAV/Error.rb
2
+ # WebDAV::Error.rb
3
+
4
+ class WebDAV
5
+ class Error < StandardError
6
+ attr_reader :code, :message, :body
7
+
8
+ private
9
+
10
+ def initialize(response)
11
+ @code = response.code.to_i
12
+ @message = response.message
13
+ @body = response.body
14
+ super("HTTP #{@code}: #{@message}")
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,32 @@
1
+ # WebDAV/MultiStatus.rb
2
+ # WebDAV::MultiStatus
3
+
4
+ require_relative './Response'
5
+
6
+ class WebDAV
7
+ class MultiStatus < Response
8
+ attr_reader :resources
9
+
10
+ def parse
11
+ doc = REXML::Document.new(body)
12
+ resources = []
13
+ doc.elements.each('//d:response') do |resp|
14
+ href = resp.elements['.//d:href']&.text
15
+ properties = {}
16
+ resp.elements.each('.//d:prop/*') do |prop|
17
+ properties[prop.name] = prop.text || prop.to_s
18
+ end
19
+ status = resp.elements['.//d:status']&.text
20
+ resources << {href: href, properties: properties, status: status}
21
+ end
22
+ resources
23
+ end
24
+
25
+ private
26
+
27
+ def initialize(response)
28
+ super
29
+ @resources = parse
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,29 @@
1
+ # WebDAV/Response.rb
2
+ # WebDAV::Response.rb
3
+
4
+ class WebDAV
5
+ class Response
6
+ attr_reader :code, :message, :headers, :body
7
+
8
+ def success?
9
+ code < 400
10
+ end
11
+
12
+ def etag
13
+ headers['ETag']
14
+ end
15
+
16
+ def content_type
17
+ headers['Content-Type']
18
+ end
19
+
20
+ private
21
+
22
+ def initialize(response)
23
+ @code = response.code.to_i
24
+ @message = response.message
25
+ @headers = response
26
+ @body = response.body
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,6 @@
1
+ # WebDAV/VERSION.rb
2
+ # WebDAV::VERSION
3
+
4
+ class WebDAV
5
+ VERSION = '0.0.0'
6
+ end
data/lib/webdav.rb ADDED
@@ -0,0 +1,158 @@
1
+ # WebDAV.rb
2
+ # WebDAV
3
+
4
+ gem 'http.rb'; require 'http.rb'
5
+ require 'net/http'
6
+ require 'rexml/document'
7
+ require 'uri'
8
+
9
+ require_relative './Net/HTTP/Report'
10
+ require_relative './String/to_const'
11
+ require_relative './WebDAV/Error'
12
+ require_relative './WebDAV/MultiStatus'
13
+ require_relative './WebDAV/Response'
14
+
15
+ class WebDAV
16
+
17
+ # Properties
18
+
19
+ def propfind(path = '/', body: nil, depth: '1')
20
+ response = request(:propfind, path, body: body, headers: {'Depth' => depth})
21
+ handle_response(response)
22
+ end
23
+
24
+ def proppatch(path, body:)
25
+ response = request(:proppatch, path, body: body)
26
+ handle_response(response)
27
+ end
28
+
29
+ # Reports
30
+
31
+ def report(path, body:, depth: '1')
32
+ response = request(:report, path, body: body, headers: {'Depth' => depth})
33
+ handle_response(response)
34
+ end
35
+
36
+ # Collections
37
+
38
+ def mkcol(path)
39
+ response = request(:mkcol, path)
40
+ handle_response(response)
41
+ end
42
+
43
+ # Namespace
44
+
45
+ def copy(path, to:, depth: 'infinity', overwrite: true)
46
+ response = request(:copy, path, headers: {
47
+ 'Destination' => resolve_uri(to).to_s,
48
+ 'Depth' => depth,
49
+ 'Overwrite' => overwrite ? 'T' : 'F'
50
+ })
51
+ handle_response(response)
52
+ end
53
+
54
+ def move(path, to:, overwrite: true)
55
+ response = request(:move, path, headers: {
56
+ 'Destination' => resolve_uri(to).to_s,
57
+ 'Overwrite' => overwrite ? 'T' : 'F'
58
+ })
59
+ handle_response(response)
60
+ end
61
+
62
+ # Locking
63
+
64
+ def lock(path, body:)
65
+ response = request(:lock, path, body: body)
66
+ handle_response(response)
67
+ end
68
+
69
+ def unlock(path, token:)
70
+ response = request(:unlock, path, headers: {'Lock-Token' => "<#{token}>"})
71
+ handle_response(response)
72
+ end
73
+
74
+ # Standard HTTP
75
+
76
+ def get(path)
77
+ response = request(:get, path)
78
+ handle_response(response)
79
+ end
80
+
81
+ def head(path)
82
+ response = request(:head, path)
83
+ handle_response(response)
84
+ end
85
+
86
+ def post(path, body:, content_type: 'application/xml')
87
+ response = request(:post, path, body: body, headers: {'Content-Type' => content_type})
88
+ handle_response(response)
89
+ end
90
+
91
+ def put(path, body:, content_type: 'application/octet-stream')
92
+ response = request(:put, path, body: body, headers: {'Content-Type' => content_type})
93
+ handle_response(response)
94
+ end
95
+
96
+ def patch(path, body:, content_type: 'application/xml')
97
+ response = request(:patch, path, body: body, headers: {'Content-Type' => content_type})
98
+ handle_response(response)
99
+ end
100
+
101
+ def delete(path)
102
+ response = request(:delete, path)
103
+ handle_response(response)
104
+ end
105
+
106
+ def options(path = '/')
107
+ response = request(:options, path)
108
+ handle_response(response)
109
+ end
110
+
111
+ def trace(path)
112
+ response = request(:trace, path)
113
+ handle_response(response)
114
+ end
115
+
116
+ private
117
+
118
+ def initialize(uri, username:, password:)
119
+ @uri = uri.is_a?(URI) ? uri : URI.parse(uri)
120
+ @username = username
121
+ @password = password
122
+ end
123
+
124
+ def resolve_uri(path)
125
+ URI.join(@uri, path)
126
+ end
127
+
128
+ def request_uri(path)
129
+ resolve_uri(path).request_uri
130
+ end
131
+
132
+ def default_headers
133
+ {'Content-Type' => 'application/xml; charset=utf-8'}
134
+ end
135
+
136
+ def request_class(verb)
137
+ "Net::HTTP::#{verb.to_s.capitalize}".to_const
138
+ end
139
+
140
+ def request(verb, path, body: nil, headers: {})
141
+ request_object = request_class(verb).new(request_uri(path))
142
+ request_object.basic_auth(@username, @password)
143
+ request_object.body = body if body
144
+ merged_headers = default_headers.merge(headers)
145
+ merged_headers.each{|k, v| request_object[k] = v}
146
+ HTTP.request(resolve_uri(path), request_object)
147
+ end
148
+
149
+ def handle_response(response)
150
+ raise WebDAV::Error.new(response) if response.code.to_i >= 400
151
+ case response.code.to_i
152
+ when 207
153
+ MultiStatus.new(response)
154
+ else
155
+ Response.new(response)
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,85 @@
1
+ # test/WebDAV/Error_test.rb
2
+
3
+ require_relative '../helper'
4
+
5
+ describe WebDAV::Error do
6
+ let(:mock_response) do
7
+ MockResponse.new(
8
+ code: '404',
9
+ message: 'Not Found',
10
+ body: '<!DOCTYPE html><html><body>Not Found</body></html>'
11
+ )
12
+ end
13
+
14
+ let(:error){WebDAV::Error.new(mock_response)}
15
+
16
+ describe "#code" do
17
+ it "returns the status code as an integer" do
18
+ _(error.code).must_equal 404
19
+ end
20
+ end
21
+
22
+ describe "#message" do
23
+ it "returns the status message" do
24
+ _(error.message).must_equal 'Not Found'
25
+ end
26
+ end
27
+
28
+ describe "#body" do
29
+ it "returns the response body" do
30
+ _(error.body).must_include 'Not Found'
31
+ end
32
+ end
33
+
34
+ describe "#to_s" do
35
+ it "includes the status code and message" do
36
+ _(error.to_s).must_equal 'HTTP 404: Not Found'
37
+ end
38
+ end
39
+
40
+ describe "inheritance" do
41
+ it "is a StandardError" do
42
+ _(error).must_be_kind_of StandardError
43
+ end
44
+
45
+ it "can be raised and rescued" do
46
+ assert_raises(WebDAV::Error) do
47
+ raise WebDAV::Error.new(mock_response)
48
+ end
49
+ end
50
+ end
51
+
52
+ describe "with a 401 status code" do
53
+ let(:mock_response) do
54
+ MockResponse.new(
55
+ code: '401',
56
+ message: 'Unauthorized',
57
+ body: ''
58
+ )
59
+ end
60
+
61
+ let(:error){WebDAV::Error.new(mock_response)}
62
+
63
+ it "handles a 401 error" do
64
+ _(error.code).must_equal 401
65
+ _(error.to_s).must_equal 'HTTP 401: Unauthorized'
66
+ end
67
+ end
68
+
69
+ describe "with a 500 status code" do
70
+ let(:mock_response) do
71
+ MockResponse.new(
72
+ code: '500',
73
+ message: 'Internal Server Error',
74
+ body: ''
75
+ )
76
+ end
77
+
78
+ let(:error){WebDAV::Error.new(mock_response)}
79
+
80
+ it "handles a 500 error" do
81
+ _(error.code).must_equal 500
82
+ _(error.to_s).must_equal 'HTTP 500: Internal Server Error'
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,102 @@
1
+ # test/WebDAV/MultiStatus_test.rb
2
+
3
+ require_relative '../helper'
4
+
5
+ describe WebDAV::MultiStatus do
6
+ let(:multistatus_xml) do
7
+ <<~XML
8
+ <?xml version="1.0" encoding="UTF-8"?>
9
+ <d:multistatus xmlns:d="DAV:">
10
+ <d:response>
11
+ <d:href>/dav/calendars/user/test/calendar1/</d:href>
12
+ <d:propstat>
13
+ <d:prop>
14
+ <d:displayname>Work</d:displayname>
15
+ <d:resourcetype><d:collection/></d:resourcetype>
16
+ </d:prop>
17
+ <d:status>HTTP/1.1 200 OK</d:status>
18
+ </d:propstat>
19
+ </d:response>
20
+ <d:response>
21
+ <d:href>/dav/calendars/user/test/calendar2/</d:href>
22
+ <d:propstat>
23
+ <d:prop>
24
+ <d:displayname>Personal</d:displayname>
25
+ <d:resourcetype><d:collection/></d:resourcetype>
26
+ </d:prop>
27
+ <d:status>HTTP/1.1 200 OK</d:status>
28
+ </d:propstat>
29
+ </d:response>
30
+ </d:multistatus>
31
+ XML
32
+ end
33
+
34
+ let(:mock_response) do
35
+ MockResponse.new(
36
+ code: '207',
37
+ message: 'Multi-Status',
38
+ body: multistatus_xml
39
+ )
40
+ end
41
+
42
+ let(:response){WebDAV::MultiStatus.new(mock_response)}
43
+
44
+ describe "#code" do
45
+ it "returns 207" do
46
+ _(response.code).must_equal 207
47
+ end
48
+ end
49
+
50
+ describe "#success?" do
51
+ it "returns true" do
52
+ _(response.success?).must_equal true
53
+ end
54
+ end
55
+
56
+ describe "#resources" do
57
+ it "returns an array" do
58
+ _(response.resources).must_be_kind_of Array
59
+ end
60
+
61
+ it "parses the correct number of resources" do
62
+ _(response.resources.length).must_equal 2
63
+ end
64
+
65
+ it "extracts href from each resource" do
66
+ _(response.resources[0][:href]).must_equal '/dav/calendars/user/test/calendar1/'
67
+ _(response.resources[1][:href]).must_equal '/dav/calendars/user/test/calendar2/'
68
+ end
69
+
70
+ it "extracts properties from each resource" do
71
+ _(response.resources[0][:properties]['displayname']).must_equal 'Work'
72
+ _(response.resources[1][:properties]['displayname']).must_equal 'Personal'
73
+ end
74
+
75
+ it "extracts status from each resource" do
76
+ _(response.resources[0][:status]).must_equal 'HTTP/1.1 200 OK'
77
+ end
78
+ end
79
+
80
+ describe "with empty multistatus" do
81
+ let(:empty_xml) do
82
+ <<~XML
83
+ <?xml version="1.0" encoding="UTF-8"?>
84
+ <d:multistatus xmlns:d="DAV:">
85
+ </d:multistatus>
86
+ XML
87
+ end
88
+
89
+ let(:empty_response) do
90
+ MockResponse.new(
91
+ code: '207',
92
+ message: 'Multi-Status',
93
+ body: empty_xml
94
+ )
95
+ end
96
+
97
+ it "returns an empty array" do
98
+ r = WebDAV::MultiStatus.new(empty_response)
99
+ _(r.resources).must_equal []
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,60 @@
1
+ # test/WebDAV/Response_test.rb
2
+
3
+ require_relative '../helper'
4
+
5
+ describe WebDAV::Response do
6
+ let(:mock_response) do
7
+ headers = {'ETag' => '"abc123"', 'Content-Type' => 'text/html'}
8
+ response = MockResponse.new(
9
+ code: '200',
10
+ message: 'OK',
11
+ body: '<html>Hello</html>',
12
+ headers_hash: headers
13
+ )
14
+ response
15
+ end
16
+
17
+ let(:response){WebDAV::Response.new(mock_response)}
18
+
19
+ describe "#code" do
20
+ it "returns the status code as an integer" do
21
+ _(response.code).must_equal 200
22
+ end
23
+ end
24
+
25
+ describe "#message" do
26
+ it "returns the status message" do
27
+ _(response.message).must_equal 'OK'
28
+ end
29
+ end
30
+
31
+ describe "#body" do
32
+ it "returns the response body" do
33
+ _(response.body).must_equal '<html>Hello</html>'
34
+ end
35
+ end
36
+
37
+ describe "#success?" do
38
+ it "returns true for 2xx responses" do
39
+ _(response.success?).must_equal true
40
+ end
41
+
42
+ it "returns false for 4xx responses" do
43
+ error_response = MockResponse.new(code: '404', message: 'Not Found', body: '')
44
+ r = WebDAV::Response.new(error_response)
45
+ _(r.success?).must_equal false
46
+ end
47
+ end
48
+
49
+ describe "#etag" do
50
+ it "returns the ETag header" do
51
+ _(response.etag).must_equal '"abc123"'
52
+ end
53
+ end
54
+
55
+ describe "#content_type" do
56
+ it "returns the Content-Type header" do
57
+ _(response.content_type).must_equal 'text/html'
58
+ end
59
+ end
60
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,13 @@
1
+ # test/helper.rb
2
+
3
+ require 'minitest/autorun'
4
+ require 'minitest/mock'
5
+ require 'minitest/spec'
6
+
7
+ require_relative '../lib/webdav'
8
+
9
+ MockResponse = Struct.new(:code, :message, :body, :headers_hash, keyword_init: true) do
10
+ def [](key)
11
+ headers_hash&.dig(key)
12
+ end
13
+ end
@@ -0,0 +1,254 @@
1
+ # test/webdav_test.rb
2
+
3
+ require_relative './helper'
4
+
5
+ describe WebDAV do
6
+ let(:base_uri){'https://dav.example.com/files/'}
7
+ let(:username){'user'}
8
+ let(:password){'pass'}
9
+
10
+ let(:dav) do
11
+ WebDAV.new(base_uri, username: username, password: password)
12
+ end
13
+
14
+ let(:ok_response) do
15
+ MockResponse.new(
16
+ code: '200',
17
+ message: 'OK',
18
+ body: 'Hello'
19
+ )
20
+ end
21
+
22
+ let(:created_response) do
23
+ MockResponse.new(
24
+ code: '201',
25
+ message: 'Created',
26
+ body: ''
27
+ )
28
+ end
29
+
30
+ let(:no_content_response) do
31
+ MockResponse.new(
32
+ code: '204',
33
+ message: 'No Content',
34
+ body: ''
35
+ )
36
+ end
37
+
38
+ let(:multistatus_response) do
39
+ MockResponse.new(
40
+ code: '207',
41
+ message: 'Multi-Status',
42
+ body: <<~XML,
43
+ <?xml version="1.0" encoding="UTF-8"?>
44
+ <d:multistatus xmlns:d="DAV:">
45
+ <d:response>
46
+ <d:href>/files/doc.txt</d:href>
47
+ <d:propstat>
48
+ <d:prop>
49
+ <d:displayname>doc.txt</d:displayname>
50
+ </d:prop>
51
+ <d:status>HTTP/1.1 200 OK</d:status>
52
+ </d:propstat>
53
+ </d:response>
54
+ </d:multistatus>
55
+ XML
56
+ )
57
+ end
58
+
59
+ let(:not_found_response) do
60
+ MockResponse.new(
61
+ code: '404',
62
+ message: 'Not Found',
63
+ body: 'Not Found'
64
+ )
65
+ end
66
+
67
+ # Properties
68
+
69
+ describe "#propfind" do
70
+ it "returns a MultiStatus response" do
71
+ dav.stub(:request, multistatus_response) do
72
+ result = dav.propfind('/')
73
+ _(result).must_be_kind_of WebDAV::MultiStatus
74
+ _(result.resources.length).must_equal 1
75
+ end
76
+ end
77
+ end
78
+
79
+ describe "#proppatch" do
80
+ it "returns a MultiStatus response" do
81
+ dav.stub(:request, multistatus_response) do
82
+ result = dav.proppatch('/doc.txt', body: '<d:propertyupdate/>')
83
+ _(result).must_be_kind_of WebDAV::MultiStatus
84
+ end
85
+ end
86
+ end
87
+
88
+ # Reports
89
+
90
+ describe "#report" do
91
+ it "returns a MultiStatus response" do
92
+ dav.stub(:request, multistatus_response) do
93
+ result = dav.report('/calendars/', body: '<c:calendar-query/>')
94
+ _(result).must_be_kind_of WebDAV::MultiStatus
95
+ end
96
+ end
97
+ end
98
+
99
+ # Collections
100
+
101
+ describe "#mkcol" do
102
+ it "returns a Response" do
103
+ dav.stub(:request, created_response) do
104
+ result = dav.mkcol('/new_folder/')
105
+ _(result).must_be_kind_of WebDAV::Response
106
+ _(result.code).must_equal 201
107
+ end
108
+ end
109
+ end
110
+
111
+ # Namespace
112
+
113
+ describe "#copy" do
114
+ it "returns a Response" do
115
+ dav.stub(:request, created_response) do
116
+ result = dav.copy('/doc.txt', to: '/archive/doc.txt')
117
+ _(result).must_be_kind_of WebDAV::Response
118
+ end
119
+ end
120
+ end
121
+
122
+ describe "#move" do
123
+ it "returns a Response" do
124
+ dav.stub(:request, created_response) do
125
+ result = dav.move('/draft.txt', to: '/final.txt')
126
+ _(result).must_be_kind_of WebDAV::Response
127
+ end
128
+ end
129
+ end
130
+
131
+ # Locking
132
+
133
+ describe "#lock" do
134
+ it "returns a Response" do
135
+ dav.stub(:request, ok_response) do
136
+ result = dav.lock('/doc.txt', body: '<d:lockinfo/>')
137
+ _(result).must_be_kind_of WebDAV::Response
138
+ end
139
+ end
140
+ end
141
+
142
+ describe "#unlock" do
143
+ it "returns a Response" do
144
+ dav.stub(:request, no_content_response) do
145
+ result = dav.unlock('/doc.txt', token: 'urn:uuid:abc123')
146
+ _(result).must_be_kind_of WebDAV::Response
147
+ _(result.code).must_equal 204
148
+ end
149
+ end
150
+ end
151
+
152
+ # Standard HTTP
153
+
154
+ describe "#get" do
155
+ it "returns a Response with the body" do
156
+ dav.stub(:request, ok_response) do
157
+ result = dav.get('/doc.txt')
158
+ _(result).must_be_kind_of WebDAV::Response
159
+ _(result.body).must_equal 'Hello'
160
+ end
161
+ end
162
+ end
163
+
164
+ describe "#head" do
165
+ it "returns a Response" do
166
+ dav.stub(:request, ok_response) do
167
+ result = dav.head('/doc.txt')
168
+ _(result).must_be_kind_of WebDAV::Response
169
+ end
170
+ end
171
+ end
172
+
173
+ describe "#post" do
174
+ it "returns a Response" do
175
+ dav.stub(:request, ok_response) do
176
+ result = dav.post('/endpoint', body: '<data/>')
177
+ _(result).must_be_kind_of WebDAV::Response
178
+ end
179
+ end
180
+ end
181
+
182
+ describe "#put" do
183
+ it "returns a Response" do
184
+ dav.stub(:request, created_response) do
185
+ result = dav.put('/doc.txt', body: 'New content', content_type: 'text/plain')
186
+ _(result).must_be_kind_of WebDAV::Response
187
+ _(result.code).must_equal 201
188
+ end
189
+ end
190
+ end
191
+
192
+ describe "#patch" do
193
+ it "returns a Response" do
194
+ dav.stub(:request, ok_response) do
195
+ result = dav.patch('/doc.txt', body: '<patch/>')
196
+ _(result).must_be_kind_of WebDAV::Response
197
+ end
198
+ end
199
+ end
200
+
201
+ describe "#delete" do
202
+ it "returns a Response" do
203
+ dav.stub(:request, no_content_response) do
204
+ result = dav.delete('/doc.txt')
205
+ _(result).must_be_kind_of WebDAV::Response
206
+ _(result.code).must_equal 204
207
+ end
208
+ end
209
+ end
210
+
211
+ describe "#options" do
212
+ it "returns a Response" do
213
+ dav.stub(:request, ok_response) do
214
+ result = dav.options('/')
215
+ _(result).must_be_kind_of WebDAV::Response
216
+ end
217
+ end
218
+ end
219
+
220
+ describe "#trace" do
221
+ it "returns a Response" do
222
+ dav.stub(:request, ok_response) do
223
+ result = dav.trace('/')
224
+ _(result).must_be_kind_of WebDAV::Response
225
+ end
226
+ end
227
+ end
228
+
229
+ # Error handling
230
+
231
+ describe "error responses" do
232
+ it "raises WebDAV::Error for 404" do
233
+ dav.stub(:request, not_found_response) do
234
+ assert_raises(WebDAV::Error) do
235
+ dav.get('/missing.txt')
236
+ end
237
+ end
238
+ end
239
+
240
+ it "includes the status code in the error" do
241
+ dav.stub(:request, not_found_response) do
242
+ error = assert_raises(WebDAV::Error){dav.get('/missing.txt')}
243
+ _(error.code).must_equal 404
244
+ end
245
+ end
246
+
247
+ it "includes the message in the error" do
248
+ dav.stub(:request, not_found_response) do
249
+ error = assert_raises(WebDAV::Error){dav.get('/missing.txt')}
250
+ _(error.message).must_equal 'Not Found'
251
+ end
252
+ end
253
+ end
254
+ end
data/webdav.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ require_relative './lib/WebDAV/VERSION'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'webdav'
5
+ spec.version = WebDAV::VERSION
6
+
7
+ spec.summary = "A Ruby WebDAV client library."
8
+ spec.description = "A Ruby WebDAV client library."
9
+
10
+ spec.author = 'thoran'
11
+ spec.email = 'code@thoran.com'
12
+ spec.homepage = 'https://github.com/thoran/webdav'
13
+ spec.license = 'MIT'
14
+
15
+ spec.required_ruby_version = '>= 2.7'
16
+
17
+ spec.add_dependency('http.rb')
18
+
19
+ spec.files = [
20
+ 'webdav.gemspec',
21
+ 'Gemfile',
22
+ Dir['lib/**/*.rb'],
23
+ 'LICENSE',
24
+ 'README.md',
25
+ Dir['test/**/*.rb']
26
+ ].flatten
27
+
28
+ spec.require_paths = ['lib']
29
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: webdav
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - thoran
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: http.rb
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ description: A Ruby WebDAV client library.
27
+ email: code@thoran.com
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - Gemfile
33
+ - LICENSE
34
+ - README.md
35
+ - lib/Net/HTTP/Report.rb
36
+ - lib/String/to_const.rb
37
+ - lib/Thoran/Array/AllButFirst/all_but_first.rb
38
+ - lib/Thoran/Array/FirstX/firstX.rb
39
+ - lib/Thoran/String/ToConst/to_const.rb
40
+ - lib/WebDAV/Error.rb
41
+ - lib/WebDAV/MultiStatus.rb
42
+ - lib/WebDAV/Response.rb
43
+ - lib/WebDAV/VERSION.rb
44
+ - lib/webdav.rb
45
+ - test/WebDAV/Error_test.rb
46
+ - test/WebDAV/MultiStatus_test.rb
47
+ - test/WebDAV/Response_test.rb
48
+ - test/helper.rb
49
+ - test/webdav_test.rb
50
+ - webdav.gemspec
51
+ homepage: https://github.com/thoran/webdav
52
+ licenses:
53
+ - MIT
54
+ metadata: {}
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '2.7'
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ requirements: []
69
+ rubygems_version: 4.0.8
70
+ specification_version: 4
71
+ summary: A Ruby WebDAV client library.
72
+ test_files: []