async-caldav 1.2.4 → 1.3.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 +4 -4
- data/lib/async/caldav/client/addressbook.rb +21 -9
- data/lib/async/caldav/client/calendar.rb +39 -15
- data/lib/async/caldav/client.rb +423 -371
- data/lib/async/caldav/forward_auth.rb +61 -54
- data/lib/async/caldav/handlers/delete.rb +28 -30
- data/lib/async/caldav/handlers/get.rb +45 -47
- data/lib/async/caldav/handlers/head.rb +16 -18
- data/lib/async/caldav/handlers/mkcol.rb +64 -68
- data/lib/async/caldav/handlers/move.rb +99 -90
- data/lib/async/caldav/handlers/options.rb +15 -17
- data/lib/async/caldav/handlers/propfind.rb +140 -139
- data/lib/async/caldav/handlers/proppatch.rb +95 -90
- data/lib/async/caldav/handlers/put.rb +139 -119
- data/lib/async/caldav/handlers/report.rb +192 -161
- data/lib/async/caldav/server.rb +1029 -994
- data/lib/async/caldav/storage/filesystem.rb +247 -220
- data/lib/async/caldav/storage/mock.rb +254 -246
- data/lib/async/caldav/version.rb +1 -1
- metadata +19 -5
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "bundler/setup"
|
|
4
|
-
require "scampi"
|
|
5
|
-
require "async/caldav"
|
|
6
|
-
|
|
7
3
|
module Async
|
|
8
4
|
module Caldav
|
|
9
5
|
module Handlers
|
|
@@ -14,105 +10,114 @@ module Async
|
|
|
14
10
|
col_path = path.ensure_trailing_slash
|
|
15
11
|
col = storage.get_collection(col_path.to_s)
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
13
|
+
if col
|
|
14
|
+
updates = {}
|
|
15
|
+
is_remove = body&.match?(/<[^>]*remove[^>]*>/m)
|
|
16
|
+
dn = Protocol::Caldav::Xml.extract_value(body, 'displayname')
|
|
17
|
+
desc = Protocol::Caldav::Xml.extract_value(body, 'calendar-description')
|
|
18
|
+
color = Protocol::Caldav::Xml.extract_value(body, 'calendar-color')
|
|
19
|
+
if is_remove
|
|
20
|
+
if body.match?(/displayname/i) && !dn
|
|
21
|
+
updates[:displayname] = nil
|
|
22
|
+
end
|
|
23
|
+
if body.match?(/calendar-description/i) && !desc
|
|
24
|
+
updates[:description] = nil
|
|
25
|
+
end
|
|
26
|
+
if body.match?(/calendar-color/i) && !color
|
|
27
|
+
updates[:color] = nil
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
if dn
|
|
31
|
+
updates[:displayname] = dn
|
|
32
|
+
end
|
|
33
|
+
if desc
|
|
34
|
+
updates[:description] = desc
|
|
35
|
+
end
|
|
36
|
+
if color
|
|
37
|
+
updates[:color] = color
|
|
38
|
+
end
|
|
39
|
+
storage.update_collection(col_path.to_s, updates)
|
|
40
|
+
xml = Protocol::Caldav::Multistatus.new.to_xml do |x|
|
|
41
|
+
Protocol::Caldav::XmlBuilder.response(x, href: col_path.to_s) do
|
|
42
|
+
x.tag!("d:propstat") do
|
|
43
|
+
x.tag!("d:prop")
|
|
44
|
+
x.tag!("d:status", "HTTP/1.1 200 OK")
|
|
45
|
+
end
|
|
45
46
|
end
|
|
46
47
|
end
|
|
48
|
+
[207, Protocol::Caldav::Constants::DAV_HEADERS, [xml]]
|
|
49
|
+
else
|
|
50
|
+
[404, { 'content-type' => 'text/plain' }, ['Not Found']]
|
|
47
51
|
end
|
|
48
|
-
[207, Protocol::Caldav::Constants::DAV_HEADERS, [xml]]
|
|
49
52
|
end
|
|
50
53
|
end
|
|
51
54
|
end
|
|
52
55
|
end
|
|
53
56
|
end
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
describe "Async::Caldav::Handlers::Proppatch" do
|
|
57
|
-
def call(**opts)
|
|
58
|
-
Async::Caldav::Handlers::Proppatch.call(**opts)
|
|
59
|
-
end
|
|
58
|
+
__END__
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
Protocol::Caldav::Path.new(p, storage_class: s)
|
|
63
|
-
end
|
|
60
|
+
require_relative "../../caldav"
|
|
64
61
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
path: path('/cal/', s), storage: s,
|
|
70
|
-
body: '<d:set><d:prop><d:displayname>New</d:displayname></d:prop></d:set>'
|
|
71
|
-
)
|
|
72
|
-
status.should.equal 207
|
|
73
|
-
s.get_collection('/cal/')[:displayname].should.equal 'New'
|
|
74
|
-
end
|
|
62
|
+
describe "Async::Caldav::Handlers::Proppatch" do
|
|
63
|
+
def call(**opts)
|
|
64
|
+
Async::Caldav::Handlers::Proppatch.call(**opts)
|
|
65
|
+
end
|
|
75
66
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
status.should.equal 404
|
|
80
|
-
end
|
|
67
|
+
def path(p, s)
|
|
68
|
+
Protocol::Caldav::Path.new(p, storage_class: s)
|
|
69
|
+
end
|
|
81
70
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
col[:description].should.equal 'NewDesc'
|
|
93
|
-
end
|
|
71
|
+
it "updates displayname and returns 207" do
|
|
72
|
+
s = Async::Caldav::Storage::Mock.new
|
|
73
|
+
s.create_collection('/cal/', displayname: 'Old')
|
|
74
|
+
status, = call(
|
|
75
|
+
path: path('/cal/', s), storage: s,
|
|
76
|
+
body: '<d:set><d:prop><d:displayname>New</d:displayname></d:prop></d:set>'
|
|
77
|
+
)
|
|
78
|
+
status.should.equal 207
|
|
79
|
+
s.get_collection('/cal/')[:displayname].should.equal 'New'
|
|
80
|
+
end
|
|
94
81
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
body: '<d:set><d:prop><d:displayname>Updated</d:displayname></d:prop></d:set><d:remove><d:prop><c:calendar-description/></d:prop></d:remove>'
|
|
101
|
-
)
|
|
102
|
-
col = s.get_collection('/cal/')
|
|
103
|
-
col[:displayname].should.equal 'Updated'
|
|
104
|
-
col[:description].should.be.nil
|
|
105
|
-
col[:color].should.equal '#ff0000'
|
|
106
|
-
end
|
|
82
|
+
it "returns 404 for non-existent collection" do
|
|
83
|
+
s = Async::Caldav::Storage::Mock.new
|
|
84
|
+
status, = call(path: path('/nope/', s), storage: s, body: '')
|
|
85
|
+
status.should.equal 404
|
|
86
|
+
end
|
|
107
87
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
88
|
+
it "updates multiple properties at once" do
|
|
89
|
+
s = Async::Caldav::Storage::Mock.new
|
|
90
|
+
s.create_collection('/cal/', displayname: 'Old', description: 'OldDesc')
|
|
91
|
+
status, = call(
|
|
92
|
+
path: path('/cal/', s), storage: s,
|
|
93
|
+
body: '<d:set><d:prop><d:displayname>New</d:displayname><c:calendar-description>NewDesc</c:calendar-description></d:prop></d:set>'
|
|
94
|
+
)
|
|
95
|
+
status.should.equal 207
|
|
96
|
+
col = s.get_collection('/cal/')
|
|
97
|
+
col[:displayname].should.equal 'New'
|
|
98
|
+
col[:description].should.equal 'NewDesc'
|
|
117
99
|
end
|
|
118
|
-
|
|
100
|
+
|
|
101
|
+
it "handles mixed set and remove in one request" do
|
|
102
|
+
s = Async::Caldav::Storage::Mock.new
|
|
103
|
+
s.create_collection('/cal/', displayname: 'Keep', description: 'Remove', color: '#ff0000')
|
|
104
|
+
call(
|
|
105
|
+
path: path('/cal/', s), storage: s,
|
|
106
|
+
body: '<d:set><d:prop><d:displayname>Updated</d:displayname></d:prop></d:set><d:remove><d:prop><c:calendar-description/></d:prop></d:remove>'
|
|
107
|
+
)
|
|
108
|
+
col = s.get_collection('/cal/')
|
|
109
|
+
col[:displayname].should.equal 'Updated'
|
|
110
|
+
col[:description].should.be.nil
|
|
111
|
+
col[:color].should.equal '#ff0000'
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "removes a property" do
|
|
115
|
+
s = Async::Caldav::Storage::Mock.new
|
|
116
|
+
s.create_collection('/cal/', displayname: 'Work')
|
|
117
|
+
call(
|
|
118
|
+
path: path('/cal/', s), storage: s,
|
|
119
|
+
body: '<d:remove><d:prop><d:displayname/></d:prop></d:remove>'
|
|
120
|
+
)
|
|
121
|
+
s.get_collection('/cal/')[:displayname].should.be.nil
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "bundler/setup"
|
|
4
|
-
require "scampi"
|
|
5
|
-
require "async/caldav"
|
|
6
|
-
|
|
7
3
|
module Async
|
|
8
4
|
module Caldav
|
|
9
5
|
module Handlers
|
|
@@ -11,153 +7,177 @@ module Async
|
|
|
11
7
|
module_function
|
|
12
8
|
|
|
13
9
|
def call(path:, body:, storage:, headers: {}, resource_type: nil, **)
|
|
14
|
-
|
|
10
|
+
rejected = rejection(body, resource_type)
|
|
11
|
+
if rejected
|
|
12
|
+
rejected
|
|
13
|
+
else
|
|
14
|
+
content_type = headers['content-type'] || default_content_type(resource_type)
|
|
15
|
+
store(
|
|
16
|
+
path,
|
|
17
|
+
body,
|
|
18
|
+
content_type,
|
|
19
|
+
storage,
|
|
20
|
+
headers,
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
15
24
|
|
|
16
|
-
|
|
25
|
+
# Everything judged from the body alone, before anything is looked up or
|
|
26
|
+
# written. nil when the body is acceptable, otherwise the response.
|
|
27
|
+
def rejection(body, resource_type)
|
|
28
|
+
if body.nil? || body.strip.empty?
|
|
29
|
+
[400, { 'content-type' => 'text/plain' }, ['Empty body']]
|
|
30
|
+
elsif resource_type == :calendar && !body.start_with?('BEGIN:VCALENDAR')
|
|
31
|
+
[400, { 'content-type' => 'text/plain' }, ['Invalid calendar data']]
|
|
32
|
+
elsif resource_type == :addressbook && !body.start_with?('BEGIN:VCARD')
|
|
33
|
+
[400, { 'content-type' => 'text/plain' }, ['Invalid vCard data']]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def default_content_type(resource_type)
|
|
17
38
|
if resource_type == :calendar
|
|
18
|
-
|
|
19
|
-
content_type = headers['content-type'] || 'text/calendar'
|
|
39
|
+
'text/calendar'
|
|
20
40
|
elsif resource_type == :addressbook
|
|
21
|
-
|
|
22
|
-
content_type = headers['content-type'] || 'text/vcard'
|
|
41
|
+
'text/vcard'
|
|
23
42
|
else
|
|
24
|
-
|
|
43
|
+
'application/octet-stream'
|
|
25
44
|
end
|
|
45
|
+
end
|
|
26
46
|
|
|
47
|
+
def store(path, body, content_type, storage, headers)
|
|
27
48
|
existing = storage.get_item(path.to_s)
|
|
28
|
-
|
|
29
|
-
# Precondition checks
|
|
30
49
|
if_match = headers['if-match']
|
|
31
50
|
if_none_match = headers['if-none-match']
|
|
32
|
-
|
|
33
51
|
if if_match && (!existing || existing[:etag] != if_match)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
collection_path = path.parent.to_s
|
|
46
|
-
items = storage.list_items(collection_path)
|
|
47
|
-
items.each do |item_path, item_data|
|
|
48
|
-
next if item_path == path.to_s
|
|
49
|
-
if extract_uid(item_data[:body]) == uid
|
|
50
|
-
return [409, { 'content-type' => 'text/plain' }, ['UID conflict']]
|
|
51
|
-
end
|
|
52
|
-
end
|
|
52
|
+
[412, { 'content-type' => 'text/plain' }, ['Precondition Failed']]
|
|
53
|
+
elsif if_none_match == '*' && existing
|
|
54
|
+
[412, { 'content-type' => 'text/plain' }, ['Precondition Failed']]
|
|
55
|
+
elsif !existing && uid_conflict?(path, body, storage)
|
|
56
|
+
[409, { 'content-type' => 'text/plain' }, ['UID conflict']]
|
|
57
|
+
else
|
|
58
|
+
item, is_new = storage.put_item(path.to_s, body, content_type)
|
|
59
|
+
if is_new
|
|
60
|
+
[201, { 'etag' => item[:etag], 'content-type' => 'text/plain' }, ['']]
|
|
61
|
+
else
|
|
62
|
+
[204, { 'etag' => item[:etag] }, ['']]
|
|
53
63
|
end
|
|
54
64
|
end
|
|
65
|
+
end
|
|
55
66
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
# A UID may appear once per collection. The item at `path` itself is
|
|
68
|
+
# skipped: replacing it with a body carrying its own UID is not a clash.
|
|
69
|
+
def uid_conflict?(path, body, storage)
|
|
70
|
+
uid = extract_uid(body)
|
|
71
|
+
if uid
|
|
72
|
+
storage.list_items(path.parent.to_s).any? do |item_path, item_data|
|
|
73
|
+
item_path != path.to_s && extract_uid(item_data[:body]) == uid
|
|
74
|
+
end
|
|
60
75
|
else
|
|
61
|
-
|
|
76
|
+
false
|
|
62
77
|
end
|
|
63
78
|
end
|
|
64
79
|
|
|
65
80
|
def extract_uid(body)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
if body
|
|
82
|
+
match = body.match(/^UID:(.+)/i)
|
|
83
|
+
match ? match[1].strip : nil
|
|
84
|
+
else
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
69
87
|
end
|
|
70
88
|
|
|
71
|
-
private_class_method :extract_uid
|
|
89
|
+
private_class_method :extract_uid, :rejection, :default_content_type, :store, :uid_conflict?
|
|
72
90
|
end
|
|
73
91
|
end
|
|
74
92
|
end
|
|
75
93
|
end
|
|
76
94
|
|
|
77
|
-
|
|
78
|
-
describe "Async::Caldav::Handlers::Put" do
|
|
79
|
-
def call(**opts)
|
|
80
|
-
Async::Caldav::Handlers::Put.call(**opts)
|
|
81
|
-
end
|
|
95
|
+
__END__
|
|
82
96
|
|
|
83
|
-
|
|
84
|
-
Protocol::Caldav::Path.new(p, storage_class: s)
|
|
85
|
-
end
|
|
97
|
+
require_relative "../../caldav"
|
|
86
98
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
path: path('/calendars/admin/cal/ev.ics', s), storage: s,
|
|
92
|
-
body: "BEGIN:VCALENDAR\r\nUID:123\r\nEND:VCALENDAR",
|
|
93
|
-
resource_type: :calendar
|
|
94
|
-
)
|
|
95
|
-
status.should.equal 201
|
|
96
|
-
headers['etag'].should.not.be.nil
|
|
97
|
-
end
|
|
99
|
+
describe "Async::Caldav::Handlers::Put" do
|
|
100
|
+
def call(**opts)
|
|
101
|
+
Async::Caldav::Handlers::Put.call(**opts)
|
|
102
|
+
end
|
|
98
103
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
s.put_item('/calendars/admin/cal/ev.ics', "BEGIN:VCALENDAR\r\nUID:123\r\nEND:VCALENDAR", 'text/calendar')
|
|
103
|
-
status, = call(
|
|
104
|
-
path: path('/calendars/admin/cal/ev.ics', s), storage: s,
|
|
105
|
-
body: "BEGIN:VCALENDAR\r\nUID:123\r\nSUMMARY:Updated\r\nEND:VCALENDAR",
|
|
106
|
-
resource_type: :calendar
|
|
107
|
-
)
|
|
108
|
-
status.should.equal 204
|
|
109
|
-
end
|
|
104
|
+
def path(p, s)
|
|
105
|
+
Protocol::Caldav::Path.new(p, storage_class: s)
|
|
106
|
+
end
|
|
110
107
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
108
|
+
it "creates a new calendar item and returns 201" do
|
|
109
|
+
s = Async::Caldav::Storage::Mock.new
|
|
110
|
+
s.create_collection('/calendars/admin/cal/')
|
|
111
|
+
status, headers, = call(
|
|
112
|
+
path: path('/calendars/admin/cal/ev.ics', s), storage: s,
|
|
113
|
+
body: "BEGIN:VCALENDAR\r\nUID:123\r\nEND:VCALENDAR",
|
|
114
|
+
resource_type: :calendar
|
|
115
|
+
)
|
|
116
|
+
status.should.equal 201
|
|
117
|
+
headers['etag'].should.not.be.nil
|
|
118
|
+
end
|
|
116
119
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
it "updates an existing item and returns 204" do
|
|
121
|
+
s = Async::Caldav::Storage::Mock.new
|
|
122
|
+
s.create_collection('/calendars/admin/cal/')
|
|
123
|
+
s.put_item('/calendars/admin/cal/ev.ics', "BEGIN:VCALENDAR\r\nUID:123\r\nEND:VCALENDAR", 'text/calendar')
|
|
124
|
+
status, = call(
|
|
125
|
+
path: path('/calendars/admin/cal/ev.ics', s), storage: s,
|
|
126
|
+
body: "BEGIN:VCALENDAR\r\nUID:123\r\nSUMMARY:Updated\r\nEND:VCALENDAR",
|
|
127
|
+
resource_type: :calendar
|
|
128
|
+
)
|
|
129
|
+
status.should.equal 204
|
|
130
|
+
end
|
|
122
131
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
132
|
+
it "returns 400 for empty body" do
|
|
133
|
+
s = Async::Caldav::Storage::Mock.new
|
|
134
|
+
status, = call(path: path('/cal/ev.ics', s), storage: s, body: "")
|
|
135
|
+
status.should.equal 400
|
|
136
|
+
end
|
|
128
137
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
body: "BEGIN:VCALENDAR\r\nNEW", resource_type: :calendar,
|
|
135
|
-
headers: { 'if-match' => '"wrong"' }
|
|
136
|
-
)
|
|
137
|
-
status.should.equal 412
|
|
138
|
-
end
|
|
138
|
+
it "returns 400 for invalid calendar data" do
|
|
139
|
+
s = Async::Caldav::Storage::Mock.new
|
|
140
|
+
status, = call(path: path('/cal/ev.ics', s), storage: s, body: "NOT ICAL", resource_type: :calendar)
|
|
141
|
+
status.should.equal 400
|
|
142
|
+
end
|
|
139
143
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
body: "BEGIN:VCALENDAR\r\nNEW", resource_type: :calendar,
|
|
146
|
-
headers: { 'if-none-match' => '*' }
|
|
147
|
-
)
|
|
148
|
-
status.should.equal 412
|
|
149
|
-
end
|
|
144
|
+
it "returns 400 for invalid vCard data" do
|
|
145
|
+
s = Async::Caldav::Storage::Mock.new
|
|
146
|
+
status, = call(path: path('/addr/c.vcf', s), storage: s, body: "NOT VCARD", resource_type: :addressbook)
|
|
147
|
+
status.should.equal 400
|
|
148
|
+
end
|
|
150
149
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
status.should.equal 409
|
|
161
|
-
end
|
|
150
|
+
it "returns 412 on If-Match mismatch" do
|
|
151
|
+
s = Async::Caldav::Storage::Mock.new
|
|
152
|
+
s.put_item('/cal/ev.ics', 'BEGIN:VCALENDAR', 'text/calendar')
|
|
153
|
+
status, = call(
|
|
154
|
+
path: path('/cal/ev.ics', s), storage: s,
|
|
155
|
+
body: "BEGIN:VCALENDAR\r\nNEW", resource_type: :calendar,
|
|
156
|
+
headers: { 'if-match' => '"wrong"' }
|
|
157
|
+
)
|
|
158
|
+
status.should.equal 412
|
|
162
159
|
end
|
|
163
|
-
|
|
160
|
+
|
|
161
|
+
it "returns 412 on If-None-Match=* when item exists" do
|
|
162
|
+
s = Async::Caldav::Storage::Mock.new
|
|
163
|
+
s.put_item('/cal/ev.ics', 'BEGIN:VCALENDAR', 'text/calendar')
|
|
164
|
+
status, = call(
|
|
165
|
+
path: path('/cal/ev.ics', s), storage: s,
|
|
166
|
+
body: "BEGIN:VCALENDAR\r\nNEW", resource_type: :calendar,
|
|
167
|
+
headers: { 'if-none-match' => '*' }
|
|
168
|
+
)
|
|
169
|
+
status.should.equal 412
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it "returns 409 on UID conflict" do
|
|
173
|
+
s = Async::Caldav::Storage::Mock.new
|
|
174
|
+
s.create_collection('/cal/')
|
|
175
|
+
s.put_item('/cal/a.ics', "BEGIN:VCALENDAR\r\nUID:same\r\nEND:VCALENDAR", 'text/calendar')
|
|
176
|
+
status, = call(
|
|
177
|
+
path: path('/cal/b.ics', s), storage: s,
|
|
178
|
+
body: "BEGIN:VCALENDAR\r\nUID:same\r\nEND:VCALENDAR",
|
|
179
|
+
resource_type: :calendar
|
|
180
|
+
)
|
|
181
|
+
status.should.equal 409
|
|
182
|
+
end
|
|
183
|
+
end
|