calendav 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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