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.
@@ -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
- .to eq("https://apidata.googleusercontent.com/caldav/v2/#{URI.encode_www_form_component(username)}/user")
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
- .to eq("https://apidata.googleusercontent.com/caldav/v2/#{URI.encode_www_form_component(username)}/")
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
- subject.calendars.delete(url)
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
- expect(calendars.collect(&:display_name)).to include("Calendav Test")
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(:calendar_url) do
57
- calendars.detect { |cal| cal.display_name == "Calendav Test" }&.path ||
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
- after :each do
65
- next if calendars.detect { |cal| cal.display_name == "Calendav Test" }
69
+ it "supports events" do
70
+ expect(calendar.components).to include("VEVENT")
71
+ end
66
72
 
67
- subject.calendars.delete(calendar_url)
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(calendar_url, identifier, ics.to_ical)
81
- expect(event_url).to include(URI.decode_www_form_component(calendar_url))
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
- calendar_url, from: Time.new(2021, 6, 1), to: Time.new(2021, 6, 2)
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
@@ -5,6 +5,8 @@ require "dotenv"
5
5
 
6
6
  Dotenv.load
7
7
 
8
+ require_relative "support/encoded_matchers"
9
+
8
10
  RSpec.configure do |config|
9
11
  config.disable_monkey_patching!
10
12
 
@@ -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.1
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-13 00:00:00.000000000 Z
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: icalendar
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: rake
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: rspec
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: rubocop
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/xml_processor.rb
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