calendav 0.0.1 → 0.1.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/CHANGELOG.md +16 -2
- data/README.md +183 -39
- data/lib/calendav/calendar.rb +18 -42
- data/lib/calendav/client.rb +3 -5
- data/lib/calendav/clients/calendars_client.rb +60 -18
- data/lib/calendav/clients/events_client.rb +32 -13
- data/lib/calendav/contextual_url.rb +6 -3
- data/lib/calendav/endpoint.rb +43 -13
- data/lib/calendav/errors.rb +28 -0
- data/lib/calendav/event.rb +13 -35
- data/lib/calendav/multi_response.rb +27 -0
- data/lib/calendav/namespaces.rb +10 -0
- data/lib/calendav/parsers/calendar_xml.rb +58 -0
- data/lib/calendav/parsers/event_xml.rb +37 -0
- data/lib/calendav/parsers/response_xml.rb +48 -0
- data/lib/calendav/parsers/sync_xml.rb +57 -0
- data/lib/calendav/requests/calendar_home_set.rb +2 -2
- data/lib/calendav/requests/current_user_principal.rb +2 -2
- data/lib/calendav/requests/list_calendars.rb +32 -7
- data/lib/calendav/requests/list_events.rb +22 -10
- data/lib/calendav/requests/make_calendar.rb +8 -2
- data/lib/calendav/requests/sync_collection.rb +36 -0
- data/lib/calendav/requests/update_calendar.rb +63 -0
- data/lib/calendav/sync_collection.rb +13 -0
- data/spec/acceptance/apple_spec.rb +217 -0
- data/spec/acceptance/google_spec.rb +167 -26
- data/spec/spec_helper.rb +2 -0
- data/spec/support/encoded_matchers.rb +29 -0
- metadata +34 -7
- data/lib/calendav/xml_processor.rb +0 -42
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "googleauth"
|
4
4
|
require "icalendar"
|
5
|
+
require "securerandom"
|
5
6
|
require "uri"
|
6
7
|
|
7
8
|
RSpec.describe "Google" do
|
@@ -27,68 +28,208 @@ RSpec.describe "Google" do
|
|
27
28
|
end
|
28
29
|
|
29
30
|
it "determines the user's principal URL" do
|
30
|
-
expect(subject.principal_url)
|
31
|
-
|
31
|
+
expect(subject.principal_url).to eq_encoded_url(
|
32
|
+
"https://apidata.googleusercontent.com/caldav/v2/#{username}/user"
|
33
|
+
)
|
32
34
|
end
|
33
35
|
|
34
36
|
it "determines the user's calendar URL" do
|
35
|
-
expect(subject.calendars.home_url)
|
36
|
-
|
37
|
-
end
|
38
|
-
|
39
|
-
it "can create and delete calendars" do
|
40
|
-
skip "Not supported by Google"
|
41
|
-
url = subject.calendars.create(
|
42
|
-
display_name: "Calendav Test"
|
37
|
+
expect(subject.calendars.home_url).to eq_encoded_url(
|
38
|
+
"https://apidata.googleusercontent.com/caldav/v2/#{username}/"
|
43
39
|
)
|
40
|
+
end
|
44
41
|
|
45
|
-
|
42
|
+
it "cannot create calendars" do
|
43
|
+
expect(subject.calendars.create?).to eq(false)
|
46
44
|
end
|
47
45
|
|
48
|
-
it "can find calendars" do
|
46
|
+
it "can find and update calendars" do
|
49
47
|
calendars = subject.calendars.list
|
48
|
+
calendar = calendars.detect { |cal| cal.display_name == "Calendav Test" }
|
49
|
+
|
50
|
+
expect(calendar).to_not be_nil
|
50
51
|
|
51
|
-
|
52
|
+
subject.calendars.update(calendar.url, display_name: "Calendav Update")
|
53
|
+
|
54
|
+
calendars = subject.calendars.list
|
55
|
+
calendar = calendars.detect { |cal| cal.display_name == "Calendav Update" }
|
56
|
+
|
57
|
+
subject.calendars.update(calendar.url, display_name: "Calendav Test")
|
52
58
|
end
|
53
59
|
|
54
60
|
context "with a calendar" do
|
55
61
|
let(:calendars) { subject.calendars.list }
|
56
|
-
let(:
|
57
|
-
calendars.detect { |cal| cal.display_name == "Calendav Test" }
|
58
|
-
subject.calendars.create(display_name: "Calendav Test")
|
62
|
+
let(:calendar) do
|
63
|
+
calendars.detect { |cal| cal.display_name == "Calendav Test" }
|
59
64
|
end
|
60
65
|
let(:identifier) { "#{SecureRandom.uuid}.ics" }
|
61
66
|
let(:start) { Time.new 2021, 6, 1, 10, 30 }
|
62
67
|
let(:finish) { Time.new 2021, 6, 1, 12, 30 }
|
63
68
|
|
64
|
-
|
65
|
-
|
69
|
+
it "supports events" do
|
70
|
+
expect(calendar.components).to include("VEVENT")
|
71
|
+
end
|
66
72
|
|
67
|
-
|
73
|
+
it "supports WebDAV-Sync" do
|
74
|
+
expect(calendar.reports).to include("sync-collection")
|
68
75
|
end
|
69
76
|
|
70
|
-
it "can create, find and delete events" do
|
77
|
+
it "can create, find, update and delete events" do
|
71
78
|
ics = Icalendar::Calendar.new
|
72
79
|
ics.event do |event|
|
73
|
-
event.dtstart = start
|
74
|
-
event.dtend = finish
|
80
|
+
event.dtstart = start.utc
|
81
|
+
event.dtend = finish.utc
|
75
82
|
event.summary = "Brunch"
|
76
83
|
end
|
77
84
|
ics.publish
|
78
85
|
|
79
86
|
# Create an event
|
80
|
-
event_url = subject.events.create(
|
81
|
-
expect(event_url).to include(URI.decode_www_form_component(
|
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))
|
82
89
|
|
83
90
|
# Search for the event
|
84
91
|
events = subject.events.list(
|
85
|
-
|
92
|
+
calendar.url, from: Time.new(2021, 6, 1), to: Time.new(2021, 6, 2)
|
86
93
|
)
|
87
94
|
expect(events.length).to eq(1)
|
88
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")
|
89
159
|
|
90
|
-
# Delete the event
|
91
160
|
expect(subject.events.delete(event_url)).to eq(true)
|
92
161
|
end
|
162
|
+
|
163
|
+
it "does not respect etag conditions for deletions" do
|
164
|
+
ics = Icalendar::Calendar.new
|
165
|
+
ics.event do |event|
|
166
|
+
event.dtstart = start.utc
|
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)
|
173
|
+
event = subject.events.find(event_url)
|
174
|
+
|
175
|
+
ics.events.first.summary = "Coffee"
|
176
|
+
expect(
|
177
|
+
subject.events.update(event_url, ics.to_ical, etag: event.etag)
|
178
|
+
).to eq(true)
|
179
|
+
|
180
|
+
# Google doesn't care about the If-Match header on DELETE requests :(
|
181
|
+
expect(subject.events.delete(event_url, etag: event.etag)).to eq(true)
|
182
|
+
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
|
93
234
|
end
|
94
235
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EncodedMatchers
|
4
|
+
def eq_encoded_url(expected)
|
5
|
+
eq(RecodeURI.call(expected))
|
6
|
+
end
|
7
|
+
|
8
|
+
def match_encoded_urls(expected)
|
9
|
+
match_array(expected.collect { |uri| RecodeURI.call(uri) })
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class RecodeURI
|
14
|
+
def self.call(uri)
|
15
|
+
encoded = URI(uri)
|
16
|
+
|
17
|
+
encoded.path = encoded.path.split("/").collect do |piece|
|
18
|
+
URI.encode_www_form_component(piece)
|
19
|
+
end.join("/")
|
20
|
+
|
21
|
+
encoded.path += "/" if uri.end_with?("/")
|
22
|
+
|
23
|
+
encoded.to_s
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
RSpec.configure do |config|
|
28
|
+
config.include EncodedMatchers
|
29
|
+
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.0
|
4
|
+
version: 0.1.0
|
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-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: http
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: icalendar
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: nokogiri
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -67,7 +81,7 @@ dependencies:
|
|
67
81
|
- !ruby/object:Gem::Version
|
68
82
|
version: '0'
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
84
|
+
name: rake
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
72
86
|
requirements:
|
73
87
|
- - ">="
|
@@ -81,7 +95,7 @@ dependencies:
|
|
81
95
|
- !ruby/object:Gem::Version
|
82
96
|
version: '0'
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
98
|
+
name: rspec
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
86
100
|
requirements:
|
87
101
|
- - ">="
|
@@ -95,7 +109,7 @@ dependencies:
|
|
95
109
|
- !ruby/object:Gem::Version
|
96
110
|
version: '0'
|
97
111
|
- !ruby/object:Gem::Dependency
|
98
|
-
name:
|
112
|
+
name: rubocop
|
99
113
|
requirement: !ruby/object:Gem::Requirement
|
100
114
|
requirements:
|
101
115
|
- - ">="
|
@@ -109,7 +123,7 @@ dependencies:
|
|
109
123
|
- !ruby/object:Gem::Version
|
110
124
|
version: '0'
|
111
125
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
126
|
+
name: tzinfo
|
113
127
|
requirement: !ruby/object:Gem::Requirement
|
114
128
|
requirements:
|
115
129
|
- - ">="
|
@@ -146,15 +160,26 @@ files:
|
|
146
160
|
- lib/calendav/credentials/google.rb
|
147
161
|
- lib/calendav/credentials/standard.rb
|
148
162
|
- lib/calendav/endpoint.rb
|
163
|
+
- lib/calendav/errors.rb
|
149
164
|
- lib/calendav/event.rb
|
165
|
+
- lib/calendav/multi_response.rb
|
166
|
+
- lib/calendav/namespaces.rb
|
167
|
+
- lib/calendav/parsers/calendar_xml.rb
|
168
|
+
- lib/calendav/parsers/event_xml.rb
|
169
|
+
- lib/calendav/parsers/response_xml.rb
|
170
|
+
- lib/calendav/parsers/sync_xml.rb
|
150
171
|
- lib/calendav/requests/calendar_home_set.rb
|
151
172
|
- lib/calendav/requests/current_user_principal.rb
|
152
173
|
- lib/calendav/requests/list_calendars.rb
|
153
174
|
- lib/calendav/requests/list_events.rb
|
154
175
|
- lib/calendav/requests/make_calendar.rb
|
155
|
-
- lib/calendav/
|
176
|
+
- lib/calendav/requests/sync_collection.rb
|
177
|
+
- lib/calendav/requests/update_calendar.rb
|
178
|
+
- lib/calendav/sync_collection.rb
|
179
|
+
- spec/acceptance/apple_spec.rb
|
156
180
|
- spec/acceptance/google_spec.rb
|
157
181
|
- spec/spec_helper.rb
|
182
|
+
- spec/support/encoded_matchers.rb
|
158
183
|
homepage: https://github.com/pat/calendav
|
159
184
|
licenses:
|
160
185
|
- Hippocratic-2.1
|
@@ -182,8 +207,10 @@ signing_key:
|
|
182
207
|
specification_version: 4
|
183
208
|
summary: CalDAV client
|
184
209
|
test_files:
|
210
|
+
- spec/acceptance/apple_spec.rb
|
185
211
|
- spec/acceptance/google_spec.rb
|
186
212
|
- spec/spec_helper.rb
|
213
|
+
- spec/support/encoded_matchers.rb
|
187
214
|
- ".rspec"
|
188
215
|
- Gemfile
|
189
216
|
- Rakefile
|
@@ -1,42 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Calendav
|
4
|
-
class XMLProcessor
|
5
|
-
NAMESPACES = {
|
6
|
-
"xmlns:dav" => "DAV:",
|
7
|
-
"xmlns:caldav" => "urn:ietf:params:xml:ns:caldav",
|
8
|
-
"xmlns:cs" => "http://calendarserver.org/ns/",
|
9
|
-
"xmlns:apple" => "http://apple.com/ns/ical/"
|
10
|
-
}.freeze
|
11
|
-
|
12
|
-
def self.call(...)
|
13
|
-
new(...).call
|
14
|
-
end
|
15
|
-
|
16
|
-
def initialize(raw, namespaces = NAMESPACES)
|
17
|
-
@raw = raw
|
18
|
-
@namespaces = namespaces
|
19
|
-
end
|
20
|
-
|
21
|
-
def call
|
22
|
-
document = parse(raw)
|
23
|
-
|
24
|
-
namespaces.each do |key, value|
|
25
|
-
document.root[key] = value unless document.namespaces[key]
|
26
|
-
end
|
27
|
-
|
28
|
-
parse(document.to_xml)
|
29
|
-
end
|
30
|
-
|
31
|
-
private
|
32
|
-
|
33
|
-
attr_reader :raw, :namespaces
|
34
|
-
|
35
|
-
def parse(string)
|
36
|
-
Nokogiri::XML(string) { |config| config.strict.noblanks }
|
37
|
-
rescue Nokogiri::XML::SyntaxError
|
38
|
-
puts string
|
39
|
-
raise
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|