calendav 0.1.0 → 0.1.1
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/CHANGELOG.md +4 -0
- data/README.md +1 -1
- data/lib/calendav/endpoint.rb +11 -13
- data/lib/calendav/event.rb +8 -0
- data/lib/calendav/parsers/calendar_xml.rb +6 -6
- data/spec/acceptance/apple_spec.rb +7 -183
- data/spec/acceptance/google_spec.rb +14 -156
- data/spec/acceptance/radicale_spec.rb +48 -0
- data/spec/acceptance/shared.rb +175 -0
- data/spec/data/radicale/config.ini +10 -0
- data/spec/data/radicale/users +1 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/support/event_helpers.rb +42 -0
- metadata +12 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6564ba6fca723cd571c786aee0028dd36179fd90ee7bba76a7fbd3526a939b64
|
4
|
+
data.tar.gz: 670e804269e4f341fee602a5ffad34360ae2d8ae5c15e88167c354227848ce03
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 63cd2f961d63585f35c46f137b510b5d8378d699e94c54cb195d702961abb0a557d9bcc6a159cdd9d133318290c1cd71921c54f4fc8ed2d38763fe95ef193a06
|
7
|
+
data.tar.gz: 4fb05941471d086aa9c13aad6a3dca6349106e5d1d527f3b1fae1b7da02a6c8b993cfe55c718c0ddad7371f5db6c18228a4b8f850b1d3c13622f824bcf31c1fa
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -10,7 +10,7 @@ Calendav has support for a few calendar providers built-in by default: Apple, Fa
|
|
10
10
|
|
11
11
|
```ruby
|
12
12
|
credentials = Calendav.credentials(
|
13
|
-
:apple,
|
13
|
+
:apple, "example@icloud.com", "app-specific-password"
|
14
14
|
)
|
15
15
|
```
|
16
16
|
|
data/lib/calendav/endpoint.rb
CHANGED
@@ -22,11 +22,11 @@ module Calendav
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def get(url:)
|
25
|
-
request(:get, url: url)
|
25
|
+
parse request(:get, url: url)
|
26
26
|
end
|
27
27
|
|
28
28
|
def mkcalendar(body, url:)
|
29
|
-
request(
|
29
|
+
parse request(
|
30
30
|
:mkcalendar,
|
31
31
|
body,
|
32
32
|
url: url,
|
@@ -39,7 +39,7 @@ module Calendav
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def propfind(body, url: nil, depth: 0)
|
42
|
-
request(
|
42
|
+
parse request(
|
43
43
|
:propfind,
|
44
44
|
body,
|
45
45
|
url: url,
|
@@ -48,7 +48,7 @@ module Calendav
|
|
48
48
|
end
|
49
49
|
|
50
50
|
def proppatch(body, url: nil)
|
51
|
-
request(
|
51
|
+
parse request(
|
52
52
|
:proppatch,
|
53
53
|
body,
|
54
54
|
url: url,
|
@@ -57,7 +57,7 @@ module Calendav
|
|
57
57
|
end
|
58
58
|
|
59
59
|
def put(body, url:, content_type: nil, etag: nil)
|
60
|
-
request(
|
60
|
+
parse request(
|
61
61
|
:put,
|
62
62
|
body,
|
63
63
|
url: url,
|
@@ -66,7 +66,7 @@ module Calendav
|
|
66
66
|
end
|
67
67
|
|
68
68
|
def report(body, url: nil, depth: 0)
|
69
|
-
request(
|
69
|
+
parse request(
|
70
70
|
:report,
|
71
71
|
body,
|
72
72
|
url: url,
|
@@ -107,13 +107,11 @@ module Calendav
|
|
107
107
|
verb, ContextualURL.call(credentials.host, url), body: body
|
108
108
|
)
|
109
109
|
|
110
|
-
if response.status.success?
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
raise RequestError, response
|
116
|
-
end
|
110
|
+
return response if response.status.success?
|
111
|
+
|
112
|
+
raise PreconditionError, response if response.status.code == 412
|
113
|
+
|
114
|
+
raise RequestError, response
|
117
115
|
end
|
118
116
|
|
119
117
|
def parse(response)
|
data/lib/calendav/event.rb
CHANGED
@@ -32,15 +32,15 @@ module Calendav
|
|
32
32
|
attr_reader :element
|
33
33
|
|
34
34
|
def components
|
35
|
-
element
|
36
|
-
|
37
|
-
|
35
|
+
element.xpath(
|
36
|
+
".//caldav:supported-calendar-component-set/caldav:comp"
|
37
|
+
).collect { |node| node["name"] }
|
38
38
|
end
|
39
39
|
|
40
40
|
def reports
|
41
|
-
element
|
42
|
-
|
43
|
-
|
41
|
+
element.xpath(
|
42
|
+
".//dav:supported-report-set/dav:supported-report/dav:report/*"
|
43
|
+
).collect(&:name)
|
44
44
|
end
|
45
45
|
|
46
46
|
def value(xpath)
|
@@ -1,15 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "icalendar"
|
4
|
-
require "icalendar/tzinfo"
|
5
3
|
require "securerandom"
|
6
|
-
|
4
|
+
|
5
|
+
require_relative "./shared"
|
7
6
|
|
8
7
|
RSpec.describe "Apple" do
|
9
8
|
let(:provider) { :apple }
|
10
9
|
let(:username) { ENV.fetch("APPLE_USERNAME") }
|
11
10
|
let(:password) { ENV.fetch("APPLE_PASSWORD") }
|
12
11
|
let(:credentials) { Calendav.credentials(provider, username, password) }
|
12
|
+
let(:host) { "https://p49-caldav.icloud.com/20264203208/calendars/" }
|
13
13
|
|
14
14
|
subject { Calendav.client(credentials) }
|
15
15
|
|
@@ -23,195 +23,19 @@ RSpec.describe "Apple" do
|
|
23
23
|
.to eq_encoded_url("https://p49-caldav.icloud.com/20264203208/calendars/")
|
24
24
|
end
|
25
25
|
|
26
|
-
|
27
|
-
expect(subject.calendars.create?).to eq(true)
|
28
|
-
end
|
29
|
-
|
30
|
-
it "can create, find, update and delete calendars" do
|
31
|
-
identifier = SecureRandom.uuid
|
32
|
-
time_zone = TZInfo::Timezone.get "UTC"
|
33
|
-
ical_time_zone = time_zone.ical_timezone Time.now.utc
|
34
|
-
|
35
|
-
url = subject.calendars.create(
|
36
|
-
identifier,
|
37
|
-
display_name: "Calendav Test",
|
38
|
-
description: "For test purposes only",
|
39
|
-
color: "#00FF00",
|
40
|
-
time_zone: ical_time_zone.to_ical
|
41
|
-
)
|
42
|
-
expect(url).to include(URI.decode_www_form_component(identifier))
|
43
|
-
expect(url).to start_with("https://")
|
44
|
-
|
45
|
-
calendars = subject.calendars.list
|
46
|
-
expect(calendars.collect(&:display_name)).to include("Calendav Test")
|
47
|
-
|
48
|
-
expect(
|
49
|
-
subject.calendars.update(url, display_name: "Calendav Update")
|
50
|
-
).to eq(true)
|
51
|
-
|
52
|
-
calendars = subject.calendars.list
|
53
|
-
expect(calendars.collect(&:display_name)).to include("Calendav Update")
|
54
|
-
|
55
|
-
subject.calendars.delete(url)
|
56
|
-
end
|
26
|
+
it_behaves_like "supporting calendar management"
|
57
27
|
|
58
28
|
context "with a calendar" do
|
59
29
|
let(:calendar_url) do
|
60
30
|
subject.calendars.create(SecureRandom.uuid, display_name: "Calendav Test")
|
61
31
|
end
|
62
32
|
let(:calendar) { subject.calendars.find(calendar_url) }
|
63
|
-
let(:identifier) { "#{SecureRandom.uuid}.ics" }
|
64
|
-
let(:start) { Time.new 2021, 6, 1, 10, 30 }
|
65
|
-
let(:finish) { Time.new 2021, 6, 1, 12, 30 }
|
66
33
|
|
67
34
|
after :each do
|
68
|
-
subject.calendars.delete(
|
69
|
-
end
|
70
|
-
|
71
|
-
it "supports events" do
|
72
|
-
expect(calendar.components).to include("VEVENT")
|
35
|
+
subject.calendars.delete(calendar.url)
|
73
36
|
end
|
74
37
|
|
75
|
-
|
76
|
-
|
77
|
-
end
|
78
|
-
|
79
|
-
it "can create, find, update and delete events" do
|
80
|
-
ics = Icalendar::Calendar.new
|
81
|
-
ics.event do |event|
|
82
|
-
event.dtstart = start.utc
|
83
|
-
event.dtend = finish.utc
|
84
|
-
event.summary = "Brunch"
|
85
|
-
end
|
86
|
-
ics.publish
|
87
|
-
|
88
|
-
# Create an event
|
89
|
-
event_url = subject.events.create(calendar.url, identifier, ics.to_ical)
|
90
|
-
expect(event_url).to include(URI.decode_www_form_component(calendar.url))
|
91
|
-
|
92
|
-
# Search for the event
|
93
|
-
events = subject.events.list(
|
94
|
-
calendar.url, from: Time.new(2021, 6, 1), to: Time.new(2021, 6, 2)
|
95
|
-
)
|
96
|
-
expect(events.length).to eq(1)
|
97
|
-
expect(events.first.summary).to eq("Brunch")
|
98
|
-
expect(events.first.url).to eq_encoded_url(event_url)
|
99
|
-
|
100
|
-
# Update the event
|
101
|
-
ics.events.first.dtstart = Time.new(2021, 7, 1, 10, 30).utc
|
102
|
-
ics.events.first.dtend = Time.new(2021, 7, 1, 12, 30).utc
|
103
|
-
subject.events.update(event_url, ics.to_ical)
|
104
|
-
|
105
|
-
# Search again
|
106
|
-
events = subject.events.list(
|
107
|
-
calendar.url, from: Time.new(2021, 7, 1), to: Time.new(2021, 7, 2)
|
108
|
-
)
|
109
|
-
expect(events.length).to eq(1)
|
110
|
-
expect(events.first.summary).to eq("Brunch")
|
111
|
-
expect(events.first.url).to eq_encoded_url(event_url)
|
112
|
-
|
113
|
-
# Create another event
|
114
|
-
ics = Icalendar::Calendar.new
|
115
|
-
ics.event do |event|
|
116
|
-
event.dtstart = start.utc
|
117
|
-
event.dtend = finish.utc
|
118
|
-
event.summary = "Brunch"
|
119
|
-
end
|
120
|
-
ics.publish
|
121
|
-
|
122
|
-
another_url = subject.events.create(
|
123
|
-
calendar.url, "#{SecureRandom.uuid}.ics", ics.to_ical
|
124
|
-
)
|
125
|
-
|
126
|
-
# Search for all events
|
127
|
-
events = subject.events.list(calendar.url)
|
128
|
-
expect(events.length).to eq(2)
|
129
|
-
|
130
|
-
# Delete the events
|
131
|
-
expect(subject.events.delete(event_url)).to eq(true)
|
132
|
-
expect(subject.events.delete(another_url)).to eq(true)
|
133
|
-
end
|
134
|
-
|
135
|
-
it "respects etag conditions with updates and deletions" do
|
136
|
-
ics = Icalendar::Calendar.new
|
137
|
-
ics.event do |event|
|
138
|
-
event.dtstart = start.utc
|
139
|
-
event.dtend = finish.utc
|
140
|
-
event.summary = "Brunch"
|
141
|
-
end
|
142
|
-
ics.publish
|
143
|
-
|
144
|
-
event_url = subject.events.create(calendar.url, identifier, ics.to_ical)
|
145
|
-
event = subject.events.find(event_url)
|
146
|
-
|
147
|
-
ics.events.first.summary = "Coffee"
|
148
|
-
expect(
|
149
|
-
subject.events.update(event_url, ics.to_ical, etag: event.etag)
|
150
|
-
).to eq(true)
|
151
|
-
|
152
|
-
expect(subject.events.find(event_url).summary).to eq("Coffee")
|
153
|
-
|
154
|
-
# Updating with the old etag should fail
|
155
|
-
ics.events.first.summary = "Brunch"
|
156
|
-
expect(
|
157
|
-
subject.events.update(event_url, ics.to_ical, etag: event.etag)
|
158
|
-
).to eq(false)
|
159
|
-
|
160
|
-
expect(subject.events.find(event_url).summary).to eq("Coffee")
|
161
|
-
|
162
|
-
expect(subject.events.delete(event_url, etag: event.etag)).to eq(false)
|
163
|
-
expect(subject.events.delete(event_url)).to eq(true)
|
164
|
-
end
|
165
|
-
|
166
|
-
it "handles synchronisation requests" do
|
167
|
-
first = Icalendar::Calendar.new
|
168
|
-
first.event do |event|
|
169
|
-
event.dtstart = start.utc
|
170
|
-
event.dtend = finish.utc
|
171
|
-
event.summary = "Brunch"
|
172
|
-
end
|
173
|
-
first.publish
|
174
|
-
|
175
|
-
first_url = subject.events.create(calendar.url, identifier, first.to_ical)
|
176
|
-
token = subject.calendars.find(calendar.url, sync: true).sync_token
|
177
|
-
|
178
|
-
events = subject.events.list(calendar.url)
|
179
|
-
expect(events.length).to eq(1)
|
180
|
-
|
181
|
-
second = Icalendar::Calendar.new
|
182
|
-
second.event do |event|
|
183
|
-
event.dtstart = start.utc
|
184
|
-
event.dtend = finish.utc
|
185
|
-
event.summary = "Brunch Again"
|
186
|
-
end
|
187
|
-
second.publish
|
188
|
-
second_url = subject.events.create(
|
189
|
-
calendar.url, "#{SecureRandom.uuid}.ics", second.to_ical
|
190
|
-
)
|
191
|
-
|
192
|
-
first.events.first.summary = "Coffee"
|
193
|
-
subject.events.update(first_url, first.to_ical)
|
194
|
-
|
195
|
-
collection = subject.calendars.sync(calendar.url, token)
|
196
|
-
expect(collection.changes.collect(&:url))
|
197
|
-
.to match_encoded_urls([first_url, second_url])
|
198
|
-
|
199
|
-
expect(collection.deletions).to be_empty
|
200
|
-
|
201
|
-
first.events.first.summary = "Brunch"
|
202
|
-
subject.events.update(first_url, first.to_ical)
|
203
|
-
|
204
|
-
subject.events.delete(second_url)
|
205
|
-
|
206
|
-
collection = subject.calendars.sync(calendar.url, collection.sync_token)
|
207
|
-
urls = collection.changes.collect(&:url)
|
208
|
-
expect(urls.length).to eq(1)
|
209
|
-
expect(urls[0]).to eq_encoded_url(first_url)
|
210
|
-
|
211
|
-
expect(collection.deletions.length).to eq(1)
|
212
|
-
expect(collection.deletions.first).to eq_encoded_url(second_url)
|
213
|
-
|
214
|
-
subject.events.delete(first_url)
|
215
|
-
end
|
38
|
+
it_behaves_like "supporting event management"
|
39
|
+
it_behaves_like "supporting event deletion with etags"
|
216
40
|
end
|
217
41
|
end
|
@@ -5,6 +5,8 @@ require "icalendar"
|
|
5
5
|
require "securerandom"
|
6
6
|
require "uri"
|
7
7
|
|
8
|
+
require_relative "./shared"
|
9
|
+
|
8
10
|
RSpec.describe "Google" do
|
9
11
|
let(:provider) { :google }
|
10
12
|
let(:username) { ENV.fetch("GOOGLE_USERNAME") }
|
@@ -62,174 +64,30 @@ RSpec.describe "Google" do
|
|
62
64
|
let(:calendar) do
|
63
65
|
calendars.detect { |cal| cal.display_name == "Calendav Test" }
|
64
66
|
end
|
65
|
-
let(:identifier) { "#{SecureRandom.uuid}.ics" }
|
66
|
-
let(:start) { Time.new 2021, 6, 1, 10, 30 }
|
67
|
-
let(:finish) { Time.new 2021, 6, 1, 12, 30 }
|
68
|
-
|
69
|
-
it "supports events" do
|
70
|
-
expect(calendar.components).to include("VEVENT")
|
71
|
-
end
|
72
67
|
|
73
|
-
|
74
|
-
|
68
|
+
after :each do
|
69
|
+
subject
|
70
|
+
.events
|
71
|
+
.list(calendar.url)
|
72
|
+
.each { |event| subject.events.delete(event.url) }
|
75
73
|
end
|
76
74
|
|
77
|
-
|
78
|
-
ics = Icalendar::Calendar.new
|
79
|
-
ics.event do |event|
|
80
|
-
event.dtstart = start.utc
|
81
|
-
event.dtend = finish.utc
|
82
|
-
event.summary = "Brunch"
|
83
|
-
end
|
84
|
-
ics.publish
|
85
|
-
|
86
|
-
# Create an event
|
87
|
-
event_url = subject.events.create(calendar.url, identifier, ics.to_ical)
|
88
|
-
expect(event_url).to include(URI.decode_www_form_component(calendar.url))
|
89
|
-
|
90
|
-
# Search for the event
|
91
|
-
events = subject.events.list(
|
92
|
-
calendar.url, from: Time.new(2021, 6, 1), to: Time.new(2021, 6, 2)
|
93
|
-
)
|
94
|
-
expect(events.length).to eq(1)
|
95
|
-
expect(events.first.summary).to eq("Brunch")
|
96
|
-
expect(events.first.url).to eq_encoded_url(event_url)
|
97
|
-
|
98
|
-
# Update the event
|
99
|
-
ics.events.first.dtstart = Time.new(2021, 7, 1, 10, 30).utc
|
100
|
-
ics.events.first.dtend = Time.new(2021, 7, 1, 12, 30).utc
|
101
|
-
subject.events.update(event_url, ics.to_ical)
|
102
|
-
|
103
|
-
# Search again
|
104
|
-
events = subject.events.list(
|
105
|
-
calendar.url, from: Time.new(2021, 7, 1), to: Time.new(2021, 7, 2)
|
106
|
-
)
|
107
|
-
expect(events.length).to eq(1)
|
108
|
-
expect(events.first.summary).to eq("Brunch")
|
109
|
-
expect(events.first.url).to eq_encoded_url(event_url)
|
110
|
-
|
111
|
-
# Create another event
|
112
|
-
ics = Icalendar::Calendar.new
|
113
|
-
ics.event do |event|
|
114
|
-
event.dtstart = start.utc
|
115
|
-
event.dtend = finish.utc
|
116
|
-
event.summary = "Brunch"
|
117
|
-
end
|
118
|
-
ics.publish
|
119
|
-
|
120
|
-
another_url = subject.events.create(
|
121
|
-
calendar.url, "#{SecureRandom.uuid}.ics", ics.to_ical
|
122
|
-
)
|
123
|
-
|
124
|
-
# Search for all events
|
125
|
-
events = subject.events.list(calendar.url)
|
126
|
-
expect(events.length).to eq(2)
|
127
|
-
|
128
|
-
# Delete the events
|
129
|
-
expect(subject.events.delete(event_url)).to eq(true)
|
130
|
-
expect(subject.events.delete(another_url)).to eq(true)
|
131
|
-
end
|
132
|
-
|
133
|
-
it "respects etag conditions with updates" do
|
134
|
-
ics = Icalendar::Calendar.new
|
135
|
-
ics.event do |event|
|
136
|
-
event.dtstart = start.utc
|
137
|
-
event.dtend = finish.utc
|
138
|
-
event.summary = "Brunch"
|
139
|
-
end
|
140
|
-
ics.publish
|
141
|
-
|
142
|
-
event_url = subject.events.create(calendar.url, identifier, ics.to_ical)
|
143
|
-
event = subject.events.find(event_url)
|
144
|
-
|
145
|
-
ics.events.first.summary = "Coffee"
|
146
|
-
expect(
|
147
|
-
subject.events.update(event_url, ics.to_ical, etag: event.etag)
|
148
|
-
).to eq(true)
|
149
|
-
|
150
|
-
expect(subject.events.find(event_url).summary).to eq("Coffee")
|
151
|
-
|
152
|
-
# Updating with the old etag should fail
|
153
|
-
ics.events.first.summary = "Brunch"
|
154
|
-
expect(
|
155
|
-
subject.events.update(event_url, ics.to_ical, etag: event.etag)
|
156
|
-
).to eq(false)
|
157
|
-
|
158
|
-
expect(subject.events.find(event_url).summary).to eq("Coffee")
|
159
|
-
|
160
|
-
expect(subject.events.delete(event_url)).to eq(true)
|
161
|
-
end
|
75
|
+
it_behaves_like "supporting event management"
|
162
76
|
|
163
77
|
it "does not respect etag conditions for deletions" do
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
event.dtend = finish.utc
|
168
|
-
event.summary = "Brunch"
|
169
|
-
end
|
170
|
-
ics.publish
|
171
|
-
|
172
|
-
event_url = subject.events.create(calendar.url, identifier, ics.to_ical)
|
78
|
+
event_url = subject.events.create(
|
79
|
+
calendar.url, event_identifier, ical_event("Brunch", 10, 30)
|
80
|
+
)
|
173
81
|
event = subject.events.find(event_url)
|
174
82
|
|
175
|
-
ics.events.first.summary = "Coffee"
|
176
83
|
expect(
|
177
|
-
subject.events.update(
|
84
|
+
subject.events.update(
|
85
|
+
event_url, update_summary(event, "Coffee"), etag: event.etag
|
86
|
+
)
|
178
87
|
).to eq(true)
|
179
88
|
|
180
89
|
# Google doesn't care about the If-Match header on DELETE requests :(
|
181
90
|
expect(subject.events.delete(event_url, etag: event.etag)).to eq(true)
|
182
91
|
end
|
183
|
-
|
184
|
-
it "handles synchronisation requests" do
|
185
|
-
first = Icalendar::Calendar.new
|
186
|
-
first.event do |event|
|
187
|
-
event.dtstart = start.utc
|
188
|
-
event.dtend = finish.utc
|
189
|
-
event.summary = "Brunch"
|
190
|
-
end
|
191
|
-
first.publish
|
192
|
-
|
193
|
-
first_url = subject.events.create(calendar.url, identifier, first.to_ical)
|
194
|
-
token = subject.calendars.find(calendar.url, sync: true).sync_token
|
195
|
-
|
196
|
-
events = subject.events.list(calendar.url)
|
197
|
-
expect(events.length).to eq(1)
|
198
|
-
|
199
|
-
second = Icalendar::Calendar.new
|
200
|
-
second.event do |event|
|
201
|
-
event.dtstart = start.utc
|
202
|
-
event.dtend = finish.utc
|
203
|
-
event.summary = "Brunch Again"
|
204
|
-
end
|
205
|
-
second.publish
|
206
|
-
second_url = subject.events.create(
|
207
|
-
calendar.url, "#{SecureRandom.uuid}.ics", second.to_ical
|
208
|
-
)
|
209
|
-
|
210
|
-
first.events.first.summary = "Coffee"
|
211
|
-
subject.events.update(first_url, first.to_ical)
|
212
|
-
|
213
|
-
collection = subject.calendars.sync(calendar.url, token)
|
214
|
-
expect(collection.changes.collect(&:url))
|
215
|
-
.to match_encoded_urls([first_url, second_url])
|
216
|
-
|
217
|
-
expect(collection.deletions).to be_empty
|
218
|
-
|
219
|
-
first.events.first.summary = "Brunch"
|
220
|
-
subject.events.update(first_url, first.to_ical)
|
221
|
-
|
222
|
-
subject.events.delete(second_url)
|
223
|
-
|
224
|
-
collection = subject.calendars.sync(calendar.url, collection.sync_token)
|
225
|
-
urls = collection.changes.collect(&:url)
|
226
|
-
expect(urls.length).to eq(1)
|
227
|
-
expect(urls[0]).to eq_encoded_url(first_url)
|
228
|
-
|
229
|
-
expect(collection.deletions.length).to eq(1)
|
230
|
-
expect(collection.deletions.first).to eq_encoded_url(second_url)
|
231
|
-
|
232
|
-
subject.events.delete(first_url)
|
233
|
-
end
|
234
92
|
end
|
235
93
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
require_relative "./shared"
|
6
|
+
|
7
|
+
RSpec.describe "Radicale" do
|
8
|
+
let(:provider) { :radicale }
|
9
|
+
let(:username) { ENV.fetch("RADICALE_USERNAME") }
|
10
|
+
let(:password) { ENV.fetch("RADICALE_PASSWORD") }
|
11
|
+
let(:host) { ENV.fetch("RADICALE_HOST") }
|
12
|
+
let(:credentials) do
|
13
|
+
Calendav::Credentials::Standard.new(
|
14
|
+
host: host,
|
15
|
+
username: username,
|
16
|
+
password: password,
|
17
|
+
authentication: :basic_auth
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
subject { Calendav.client(credentials) }
|
22
|
+
|
23
|
+
it "determines the user's principal URL" do
|
24
|
+
expect(subject.principal_url)
|
25
|
+
.to eq_encoded_url("#{host}/test/")
|
26
|
+
end
|
27
|
+
|
28
|
+
it "determines the user's calendar URL" do
|
29
|
+
expect(subject.calendars.home_url)
|
30
|
+
.to eq_encoded_url("#{host}/test/")
|
31
|
+
end
|
32
|
+
|
33
|
+
it_behaves_like "supporting calendar management"
|
34
|
+
|
35
|
+
context "with a calendar" do
|
36
|
+
let(:calendar_url) do
|
37
|
+
subject.calendars.create(SecureRandom.uuid, display_name: "Calendav Test")
|
38
|
+
end
|
39
|
+
let(:calendar) { subject.calendars.find(calendar_url) }
|
40
|
+
|
41
|
+
after :each do
|
42
|
+
subject.calendars.delete(calendar.url)
|
43
|
+
end
|
44
|
+
|
45
|
+
it_behaves_like "supporting event management"
|
46
|
+
it_behaves_like "supporting event deletion with etags"
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "icalendar"
|
4
|
+
require "icalendar/tzinfo"
|
5
|
+
require "securerandom"
|
6
|
+
require "uri"
|
7
|
+
|
8
|
+
RSpec.shared_examples "supporting calendar management" do
|
9
|
+
it "supports calendar creation" do
|
10
|
+
expect(subject.calendars.create?).to eq(true)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "can create, find, update and delete calendars" do
|
14
|
+
identifier = SecureRandom.uuid
|
15
|
+
time_zone = TZInfo::Timezone.get "UTC"
|
16
|
+
ical_time_zone = time_zone.ical_timezone Time.now.utc
|
17
|
+
|
18
|
+
url = subject.calendars.create(
|
19
|
+
identifier,
|
20
|
+
display_name: "Calendav Test",
|
21
|
+
description: "For test purposes only",
|
22
|
+
color: "#00FF00",
|
23
|
+
time_zone: ical_time_zone.to_ical
|
24
|
+
)
|
25
|
+
expect(url).to include(URI.decode_www_form_component(identifier))
|
26
|
+
expect(url).to start_with(host)
|
27
|
+
|
28
|
+
calendars = subject.calendars.list
|
29
|
+
expect(calendars.collect(&:display_name)).to include("Calendav Test")
|
30
|
+
|
31
|
+
expect(
|
32
|
+
subject.calendars.update(url, display_name: "Calendav Update")
|
33
|
+
).to eq(true)
|
34
|
+
|
35
|
+
calendars = subject.calendars.list
|
36
|
+
expect(calendars.collect(&:display_name)).to include("Calendav Update")
|
37
|
+
|
38
|
+
subject.calendars.delete(url)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
RSpec.shared_examples "supporting event management" do
|
43
|
+
it "supports events" do
|
44
|
+
expect(calendar.components).to include("VEVENT")
|
45
|
+
end
|
46
|
+
|
47
|
+
it "supports WebDAV-Sync" do
|
48
|
+
expect(calendar.reports).to include("sync-collection")
|
49
|
+
end
|
50
|
+
|
51
|
+
it "can create, find, update and delete events" do
|
52
|
+
# Create an event
|
53
|
+
event_url = subject.events.create(
|
54
|
+
calendar.url, event_identifier, ical_event("Brunch", 10, 30)
|
55
|
+
)
|
56
|
+
expect(event_url).to include(URI.decode_www_form_component(calendar.url))
|
57
|
+
event = subject.events.find(event_url)
|
58
|
+
|
59
|
+
# Search for the event
|
60
|
+
events = subject.events.list(
|
61
|
+
calendar.url, from: time_at(0, 0), to: time_at(23, 59)
|
62
|
+
)
|
63
|
+
expect(events.length).to eq(1)
|
64
|
+
expect(events.first.summary).to eq("Brunch")
|
65
|
+
expect(events.first.dtstart.to_time).to eq(time_at(10, 30))
|
66
|
+
expect(events.first.url).to eq_encoded_url(event_url)
|
67
|
+
|
68
|
+
# Update the event
|
69
|
+
subject.events.update(event_url, update_summary(event, "Coffee"))
|
70
|
+
|
71
|
+
# Search again
|
72
|
+
events = subject.events.list(
|
73
|
+
calendar.url, from: time_at(0, 0), to: time_at(23, 59)
|
74
|
+
)
|
75
|
+
expect(events.length).to eq(1)
|
76
|
+
expect(events.first.summary).to eq("Coffee")
|
77
|
+
expect(events.first.dtstart.to_time).to eq(time_at(10, 30))
|
78
|
+
expect(events.first.url).to eq_encoded_url(event_url)
|
79
|
+
|
80
|
+
# Create another event
|
81
|
+
another_url = subject.events.create(
|
82
|
+
calendar.url, event_identifier, ical_event("Brunch", 10, 30)
|
83
|
+
)
|
84
|
+
|
85
|
+
# Search for all events
|
86
|
+
events = subject.events.list(calendar.url)
|
87
|
+
expect(events.length).to eq(2)
|
88
|
+
|
89
|
+
# Delete the events
|
90
|
+
expect(subject.events.delete(event_url)).to eq(true)
|
91
|
+
expect(subject.events.delete(another_url)).to eq(true)
|
92
|
+
end
|
93
|
+
|
94
|
+
it "respects etag conditions with updates" do
|
95
|
+
event_url = subject.events.create(
|
96
|
+
calendar.url, event_identifier, ical_event("Brunch", 10, 30)
|
97
|
+
)
|
98
|
+
event = subject.events.find(event_url)
|
99
|
+
|
100
|
+
expect(
|
101
|
+
subject.events.update(
|
102
|
+
event_url, update_summary(event, "Coffee"), etag: event.etag
|
103
|
+
)
|
104
|
+
).to eq(true)
|
105
|
+
|
106
|
+
expect(subject.events.find(event_url).summary).to eq("Coffee")
|
107
|
+
|
108
|
+
# Updating with the old etag should fail
|
109
|
+
expect(
|
110
|
+
subject.events.update(
|
111
|
+
event_url, update_summary(event, "Brunch"), etag: event.etag
|
112
|
+
)
|
113
|
+
).to eq(false)
|
114
|
+
|
115
|
+
expect(subject.events.find(event_url).summary).to eq("Coffee")
|
116
|
+
|
117
|
+
expect(subject.events.delete(event_url)).to eq(true)
|
118
|
+
end
|
119
|
+
|
120
|
+
it "handles synchronisation requests" do
|
121
|
+
first_url = subject.events.create(
|
122
|
+
calendar.url, event_identifier, ical_event("Brunch", 10, 30)
|
123
|
+
)
|
124
|
+
first = subject.events.find(first_url)
|
125
|
+
token = subject.calendars.find(calendar.url, sync: true).sync_token
|
126
|
+
|
127
|
+
events = subject.events.list(calendar.url)
|
128
|
+
expect(events.length).to eq(1)
|
129
|
+
|
130
|
+
second_url = subject.events.create(
|
131
|
+
calendar.url, event_identifier, ical_event("Brunch Again", 11, 30)
|
132
|
+
)
|
133
|
+
|
134
|
+
subject.events.update(first_url, update_summary(first, "Coffee"))
|
135
|
+
first = subject.events.find(first_url)
|
136
|
+
|
137
|
+
collection = subject.calendars.sync(calendar.url, token)
|
138
|
+
expect(collection.changes.collect(&:url))
|
139
|
+
.to match_encoded_urls([first_url, second_url])
|
140
|
+
|
141
|
+
expect(collection.deletions).to be_empty
|
142
|
+
|
143
|
+
subject.events.update(first_url, update_summary(first, "Brunch"))
|
144
|
+
subject.events.delete(second_url)
|
145
|
+
|
146
|
+
collection = subject.calendars.sync(calendar.url, collection.sync_token)
|
147
|
+
urls = collection.changes.collect(&:url)
|
148
|
+
expect(urls.length).to eq(1)
|
149
|
+
expect(urls[0]).to eq_encoded_url(first_url)
|
150
|
+
|
151
|
+
expect(collection.deletions.length).to eq(1)
|
152
|
+
expect(collection.deletions.first).to eq_encoded_url(second_url)
|
153
|
+
|
154
|
+
subject.events.delete(first_url)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
RSpec.shared_examples "supporting event deletion with etags" do
|
159
|
+
it "respects etag conditions with deletions" do
|
160
|
+
event_url = subject.events.create(
|
161
|
+
calendar.url, event_identifier, ical_event("Brunch", 10, 30)
|
162
|
+
)
|
163
|
+
event = subject.events.find(event_url)
|
164
|
+
|
165
|
+
expect(
|
166
|
+
subject.events.update(
|
167
|
+
event_url, update_summary(event, "Coffee"), etag: event.etag
|
168
|
+
)
|
169
|
+
).to eq(true)
|
170
|
+
expect(subject.events.find(event_url).summary).to eq("Coffee")
|
171
|
+
|
172
|
+
expect(subject.events.delete(event_url, etag: event.etag)).to eq(false)
|
173
|
+
expect(subject.events.delete(event_url)).to eq(true)
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
test:$2y$05$O.Z0365vM0GOv.YN1WlIEOnd0gaGEB/SmoRiSjIHnstXIm6ZURl/y
|
data/spec/spec_helper.rb
CHANGED
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "icalendar"
|
4
|
+
require "securerandom"
|
5
|
+
|
6
|
+
module EventHelpers
|
7
|
+
def event_identifier
|
8
|
+
"#{SecureRandom.uuid}.ics"
|
9
|
+
end
|
10
|
+
|
11
|
+
def ical_event(summary, hour, minute)
|
12
|
+
start = time_at(hour, minute)
|
13
|
+
|
14
|
+
ics = Icalendar::Calendar.new
|
15
|
+
|
16
|
+
ics.event do |event|
|
17
|
+
event.dtstart = start
|
18
|
+
event.dtend = start + 3600
|
19
|
+
event.summary = summary
|
20
|
+
end
|
21
|
+
|
22
|
+
ics.tap(&:publish).to_ical
|
23
|
+
end
|
24
|
+
|
25
|
+
def time_at(hour, minute = 0)
|
26
|
+
now = Time.now
|
27
|
+
|
28
|
+
Time.utc(now.year, now.month, now.day, hour, minute)
|
29
|
+
end
|
30
|
+
|
31
|
+
def update_summary(event, summary)
|
32
|
+
ics = Icalendar::Calendar.parse(event.calendar_data).first
|
33
|
+
|
34
|
+
ics.events.first.summary = summary
|
35
|
+
|
36
|
+
ics.to_ical
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
RSpec.configure do |config|
|
41
|
+
config.include EventHelpers
|
42
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: calendav
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Pat Allan
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-06-
|
11
|
+
date: 2021-06-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: http
|
@@ -178,8 +178,13 @@ files:
|
|
178
178
|
- lib/calendav/sync_collection.rb
|
179
179
|
- spec/acceptance/apple_spec.rb
|
180
180
|
- spec/acceptance/google_spec.rb
|
181
|
+
- spec/acceptance/radicale_spec.rb
|
182
|
+
- spec/acceptance/shared.rb
|
183
|
+
- spec/data/radicale/config.ini
|
184
|
+
- spec/data/radicale/users
|
181
185
|
- spec/spec_helper.rb
|
182
186
|
- spec/support/encoded_matchers.rb
|
187
|
+
- spec/support/event_helpers.rb
|
183
188
|
homepage: https://github.com/pat/calendav
|
184
189
|
licenses:
|
185
190
|
- Hippocratic-2.1
|
@@ -209,8 +214,13 @@ summary: CalDAV client
|
|
209
214
|
test_files:
|
210
215
|
- spec/acceptance/apple_spec.rb
|
211
216
|
- spec/acceptance/google_spec.rb
|
217
|
+
- spec/acceptance/radicale_spec.rb
|
218
|
+
- spec/acceptance/shared.rb
|
219
|
+
- spec/data/radicale/config.ini
|
220
|
+
- spec/data/radicale/users
|
212
221
|
- spec/spec_helper.rb
|
213
222
|
- spec/support/encoded_matchers.rb
|
223
|
+
- spec/support/event_helpers.rb
|
214
224
|
- ".rspec"
|
215
225
|
- Gemfile
|
216
226
|
- Rakefile
|