calendav 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 605ae687dccd58f74c2c2ec01b44bf27b2cc71de6f5453d26c398b7828325d40
4
- data.tar.gz: '08c0253e9cc5479f1370a2acb5973901a4d9079078a9f51aba2ac28fccf75b9d'
3
+ metadata.gz: fc244efe85f65a2f9c4da19099a2a2e564a0b4fda5a1fafdcf6f37dd436c1f12
4
+ data.tar.gz: 101adff024b6f5d6b9d6fca3ea96c273facacbd0f66989175c0be8171633c228
5
5
  SHA512:
6
- metadata.gz: 20d4fefd21bdf9d58a2c76e557b80be9a6f62c0a4c38667ab806f91e9081e34179941ad9238ca25007db41525a9ac21ec1fe2ce84ac311418e50458af55a413a
7
- data.tar.gz: e3584378cea92199c918f81df44394ccac34b20b2f963abd72d2488d0356dd2108e06ec038ea68e511e2548f9ee48684b3584950cd737d431530479d57430ae2
6
+ metadata.gz: 1ee524964c2799df6fdf47dce9d007c6332ebe8e6fe8d2f9265f57bfccba4117d7a508520267fa5e025dcb8c209efeb1bd01f1c73f31edf1ee4cded5d8259def
7
+ data.tar.gz: 2df0276f3030c0fd1ea0cc37a32833326e7e2a0072aa4c6260c92e0e35ef635816e1b047d65b9db63e8b9c937ce4edc2c86ecb45d34ad8410c49229b5fcd06ed
data/CHANGELOG.md CHANGED
@@ -1,6 +1,16 @@
1
1
  ## Unreleased
2
2
 
3
- ...
3
+ ## 0.4.0 - 2023-02-27
4
+
5
+ * **Breaking**: calls to create/update events return event objects that only have a URL and etag populated (so: no calendar data), or nil if the request failed due to an etag precondition. This is instead of returning true on success or false on failure.
6
+ * Removed test files from the gem/gemspec.
7
+ * Increased nuance of error handler, raising a RedirectError (with location method) if the CalDAV server returns a redirect status.
8
+
9
+ ## 0.3.0 - 2022-03-14
10
+
11
+ * Add location to event wrapper.
12
+ * Allow setting timeouts for client calls.
13
+ * Avoid external calls to calculate calendar URLs where possible.
4
14
 
5
15
  ## 0.2.0 - 2021-07-07
6
16
 
data/README.md CHANGED
@@ -121,7 +121,8 @@ end
121
121
  ics.publish
122
122
 
123
123
  identifier = "#{SecureRandom.uuid}.ics"
124
- event_url = client.events.create(calendar.url, identifier, ics.to_ical)
124
+ # The returned event has just the URL and the etag, no calendar data:
125
+ event_scaffold = client.events.create(calendar.url, identifier, ics.to_ical)
125
126
  ```
126
127
 
127
128
  *Please note*: some providers (definitely Google, possibly others) will not keep your supplied event identifier, but generate a new one. So, if you need an ongoing reference to the event, save the returned URL (rather than combining the calendar URL and your identifier).
@@ -129,17 +130,17 @@ event_url = client.events.create(calendar.url, identifier, ics.to_ical)
129
130
  Updating events is done in a similar manner - with the event's URL and the updated iCalendar content:
130
131
 
131
132
  ```ruby
132
- client.events.update(event_url, ics.to_ical)
133
+ event_scaffold = client.events.update(event_url, ics.to_ical)
133
134
  ```
134
135
 
135
- To ensure you're only changing a known version of an event, it's recommended you use that version's etag as a precondition check (the update call will return false if the event on the server has a different etag value):
136
+ To ensure you're only changing a known version of an event, it's recommended you use that version's etag as a precondition check (the update call will return nil if the event on the server has a different etag value):
136
137
 
137
138
  ```ruby
138
- event = client.events.find(event_url)
139
+ event = client.events.find(event_scaffold.url)
139
140
 
140
141
  # figure out the changes you want to make, generate the new ical data, and then:
141
142
 
142
- client.events.update(event_url, modified_ical, etag: event.etag)
143
+ client.events.update(event_scaffold.url, modified_ical, etag: event.etag)
143
144
  ```
144
145
 
145
146
  Deletion of events is done via the event's URL as well - some providers (including Apple) support the etag precondition check for this, but others (including Google) do not. In the latter situation, the deletion will happen regardless of the server event's etag value.
@@ -8,8 +8,9 @@ require_relative "./requests/current_user_principal"
8
8
 
9
9
  module Calendav
10
10
  class Client
11
- def initialize(credentials)
11
+ def initialize(credentials, timeout: nil)
12
12
  @credentials = credentials
13
+ @timeout = timeout
13
14
  end
14
15
 
15
16
  def calendars
@@ -34,10 +35,10 @@ module Calendav
34
35
 
35
36
  private
36
37
 
37
- attr_reader :credentials
38
+ attr_reader :credentials, :timeout
38
39
 
39
40
  def endpoint
40
- @endpoint ||= Endpoint.new(credentials)
41
+ @endpoint ||= Endpoint.new(credentials, timeout: timeout)
41
42
  end
42
43
  end
43
44
  end
@@ -64,7 +64,7 @@ module Calendav
64
64
  endpoint
65
65
  .propfind(request.to_xml, url: url, depth: depth)
66
66
  .select { |node| node.xpath(calendar_xpath).any? }
67
- .collect { |node| Calendar.from_xml(home_url, node) }
67
+ .collect { |node| Calendar.from_xml(url, node) }
68
68
  end
69
69
 
70
70
  def options
@@ -17,7 +17,10 @@ module Calendav
17
17
  event_url = merged_url(calendar_url, event_identifier)
18
18
  result = endpoint.put(ics, url: event_url, content_type: :ics)
19
19
 
20
- result.headers["Location"] || event_url
20
+ Event.new(
21
+ url: result.headers["Location"] || event_url,
22
+ etag: result.headers["ETag"]
23
+ )
21
24
  end
22
25
 
23
26
  def delete(event_url, etag: nil)
@@ -46,13 +49,16 @@ module Calendav
46
49
  end
47
50
 
48
51
  def update(event_url, ics, etag: nil)
49
- endpoint.put(
52
+ result = endpoint.put(
50
53
  ics, url: event_url, content_type: :ics, etag: etag
51
54
  )
52
55
 
53
- true
56
+ Event.new(
57
+ url: event_url,
58
+ etag: result.headers["ETag"]
59
+ )
54
60
  rescue PreconditionError
55
- false
61
+ nil
56
62
  end
57
63
 
58
64
  private
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "./contextual_url"
4
- require_relative "./errors"
4
+ require_relative "./error_handler"
5
5
  require_relative "./parsers/response_xml"
6
6
 
7
7
  module Calendav
@@ -11,8 +11,9 @@ module Calendav
11
11
  xml: "application/xml; charset=utf-8"
12
12
  }.freeze
13
13
 
14
- def initialize(credentials)
14
+ def initialize(credentials, timeout: nil)
15
15
  @credentials = credentials
16
+ @timeout = timeout
16
17
  end
17
18
 
18
19
  def delete(url:, etag: nil)
@@ -76,7 +77,7 @@ module Calendav
76
77
 
77
78
  private
78
79
 
79
- attr_reader :credentials
80
+ attr_reader :credentials, :timeout
80
81
 
81
82
  def authenticated
82
83
  case credentials.authentication
@@ -93,6 +94,7 @@ module Calendav
93
94
  def with_headers(content_type: nil, depth: nil, etag: nil)
94
95
  http = authenticated
95
96
 
97
+ http = http.timeout(timeout) if timeout
96
98
  http = http.headers(depth: depth) if depth
97
99
  http = http.headers("If-Match" => etag) if etag
98
100
  if content_type
@@ -107,11 +109,7 @@ module Calendav
107
109
  verb, ContextualURL.call(credentials.host, url), body: body
108
110
  )
109
111
 
110
- return response if response.status.success?
111
-
112
- raise PreconditionError, response if response.status.code == 412
113
-
114
- raise RequestError, response
112
+ response.status.success? ? response : ErrorHandler.call(response)
115
113
  end
116
114
 
117
115
  def parse(response)
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./errors"
4
+
5
+ module Calendav
6
+ class ErrorHandler
7
+ def self.call(...)
8
+ new(...).call
9
+ end
10
+
11
+ def initialize(response)
12
+ @response = response
13
+ end
14
+
15
+ def call
16
+ raise PreconditionError, response if status.code == 412
17
+ raise RedirectError, response if status.redirect?
18
+
19
+ raise RequestError, response
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :response
25
+
26
+ def headers
27
+ response.headers
28
+ end
29
+
30
+ def status
31
+ response.status
32
+ end
33
+ end
34
+ end
@@ -24,5 +24,11 @@ module Calendav
24
24
  end
25
25
  end
26
26
 
27
+ class RedirectError < RequestError
28
+ def location
29
+ response.headers["Location"]
30
+ end
31
+ end
32
+
27
33
  PreconditionError = Class.new(RequestError)
28
34
  end
@@ -33,6 +33,10 @@ module Calendav
33
33
  inner_event.summary
34
34
  end
35
35
 
36
+ def location
37
+ inner_event.location
38
+ end
39
+
36
40
  def dtstart
37
41
  inner_event.dtstart
38
42
  end
data/lib/calendav.rb CHANGED
@@ -16,7 +16,7 @@ module Calendav
16
16
  PROVIDERS.fetch(provider).new(username: username, password: password)
17
17
  end
18
18
 
19
- def self.client(credentials)
20
- Client.new(credentials)
19
+ def self.client(credentials, timeout: nil)
20
+ Client.new(credentials, timeout: timeout)
21
21
  end
22
22
  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.2.0
4
+ version: 0.4.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-07-07 00:00:00.000000000 Z
11
+ date: 2023-02-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http
@@ -136,6 +136,20 @@ dependencies:
136
136
  - - ">="
137
137
  - !ruby/object:Gem::Version
138
138
  version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: timecop
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
139
153
  - !ruby/object:Gem::Dependency
140
154
  name: tzinfo
141
155
  requirement: !ruby/object:Gem::Requirement
@@ -150,6 +164,48 @@ dependencies:
150
164
  - - ">="
151
165
  - !ruby/object:Gem::Version
152
166
  version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: vcr
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: vcr_assistant
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: webmock
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
153
209
  description:
154
210
  email:
155
211
  - pat@freelancing-gods.com
@@ -157,12 +213,9 @@ executables: []
157
213
  extensions: []
158
214
  extra_rdoc_files: []
159
215
  files:
160
- - ".rspec"
161
216
  - CHANGELOG.md
162
- - Gemfile
163
217
  - LICENSE.md
164
218
  - README.md
165
- - Rakefile
166
219
  - lib/calendav.rb
167
220
  - lib/calendav/calendar.rb
168
221
  - lib/calendav/client.rb
@@ -174,6 +227,7 @@ files:
174
227
  - lib/calendav/credentials/google.rb
175
228
  - lib/calendav/credentials/standard.rb
176
229
  - lib/calendav/endpoint.rb
230
+ - lib/calendav/error_handler.rb
177
231
  - lib/calendav/errors.rb
178
232
  - lib/calendav/event.rb
179
233
  - lib/calendav/multi_response.rb
@@ -190,15 +244,6 @@ files:
190
244
  - lib/calendav/requests/sync_collection.rb
191
245
  - lib/calendav/requests/update_calendar.rb
192
246
  - lib/calendav/sync_collection.rb
193
- - spec/acceptance/apple_spec.rb
194
- - spec/acceptance/google_spec.rb
195
- - spec/acceptance/radicale_spec.rb
196
- - spec/acceptance/shared.rb
197
- - spec/data/radicale/config.ini
198
- - spec/data/radicale/users
199
- - spec/spec_helper.rb
200
- - spec/support/encoded_matchers.rb
201
- - spec/support/event_helpers.rb
202
247
  homepage: https://github.com/pat/calendav
203
248
  licenses:
204
249
  - Hippocratic-2.1
@@ -206,6 +251,7 @@ metadata:
206
251
  homepage_uri: https://github.com/pat/calendav
207
252
  source_code_uri: https://github.com/pat/calendav
208
253
  changelog_uri: https://github.com/pat/calendav/blob/main/CHANGELOG.md
254
+ rubygems_mfa_required: 'true'
209
255
  post_install_message:
210
256
  rdoc_options: []
211
257
  require_paths:
@@ -221,20 +267,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
221
267
  - !ruby/object:Gem::Version
222
268
  version: '0'
223
269
  requirements: []
224
- rubygems_version: 3.2.15
270
+ rubygems_version: 3.2.32
225
271
  signing_key:
226
272
  specification_version: 4
227
273
  summary: CalDAV client
228
- test_files:
229
- - spec/acceptance/apple_spec.rb
230
- - spec/acceptance/google_spec.rb
231
- - spec/acceptance/radicale_spec.rb
232
- - spec/acceptance/shared.rb
233
- - spec/data/radicale/config.ini
234
- - spec/data/radicale/users
235
- - spec/spec_helper.rb
236
- - spec/support/encoded_matchers.rb
237
- - spec/support/event_helpers.rb
238
- - ".rspec"
239
- - Gemfile
240
- - Rakefile
274
+ test_files: []
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper
data/Gemfile DELETED
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source "https://rubygems.org"
4
-
5
- gemspec
data/Rakefile DELETED
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
5
-
6
- RSpec::Core::RakeTask.new(:spec)
7
-
8
- require "rubocop/rake_task"
9
-
10
- RuboCop::RakeTask.new
11
-
12
- task default: %i[spec rubocop]
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "securerandom"
4
-
5
- require_relative "./shared"
6
-
7
- RSpec.describe "Apple" do
8
- let(:provider) { :apple }
9
- let(:username) { ENV.fetch("APPLE_USERNAME") }
10
- let(:password) { ENV.fetch("APPLE_PASSWORD") }
11
- let(:credentials) { Calendav.credentials(provider, username, password) }
12
- let(:host) { "https://p49-caldav.icloud.com/20264203208/calendars/" }
13
-
14
- subject { Calendav.client(credentials) }
15
-
16
- it "determines the user's principal URL" do
17
- expect(subject.principal_url)
18
- .to eq_encoded_url("https://caldav.icloud.com/20264203208/principal/")
19
- end
20
-
21
- it "determines the user's calendar URL" do
22
- expect(subject.calendars.home_url)
23
- .to eq_encoded_url("https://p49-caldav.icloud.com/20264203208/calendars/")
24
- end
25
-
26
- it_behaves_like "supporting calendar management"
27
-
28
- context "with a calendar" do
29
- let(:calendar_url) do
30
- subject.calendars.create(SecureRandom.uuid, display_name: "Calendav Test")
31
- end
32
- let(:calendar) { subject.calendars.find(calendar_url) }
33
-
34
- after :each do
35
- subject.calendars.delete(calendar.url)
36
- end
37
-
38
- it_behaves_like "supporting event management"
39
- it_behaves_like "supporting event deletion with etags"
40
- end
41
- end
@@ -1,101 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "google/apis/calendar_v3"
4
- require "googleauth"
5
- require "icalendar"
6
- require "securerandom"
7
- require "uri"
8
-
9
- require_relative "./shared"
10
-
11
- RSpec.describe "Google" do
12
- let(:provider) { :google }
13
- let(:username) { ENV.fetch("GOOGLE_USERNAME") }
14
- let(:access_token) { @access_token }
15
- let(:credentials) { Calendav.credentials(provider, username, access_token) }
16
- let(:google_auth) { @google_auth }
17
-
18
- subject { Calendav.client(credentials) }
19
-
20
- before :context do
21
- @google_auth = Google::Auth::UserRefreshCredentials.new(
22
- client_id: ENV.fetch("GOOGLE_CLIENT_ID"),
23
- scope: [],
24
- client_secret: ENV.fetch("GOOGLE_CLIENT_SECRET"),
25
- refresh_token: ENV.fetch("GOOGLE_REFRESH_TOKEN"),
26
- additional_parameters: { "access_type" => "offline" }
27
- )
28
-
29
- @access_token = begin
30
- @google_auth.fetch_access_token!
31
- @google_auth.access_token
32
- end
33
- end
34
-
35
- it "determines the user's principal URL" do
36
- expect(subject.principal_url).to eq_encoded_url(
37
- "https://apidata.googleusercontent.com/caldav/v2/#{username}/user"
38
- )
39
- end
40
-
41
- it "determines the user's calendar URL" do
42
- expect(subject.calendars.home_url).to eq_encoded_url(
43
- "https://apidata.googleusercontent.com/caldav/v2/#{username}/"
44
- )
45
- end
46
-
47
- it "cannot create calendars" do
48
- expect(subject.calendars.create?).to eq(false)
49
- end
50
-
51
- it "can find and update calendars" do
52
- calendars = subject.calendars.list
53
- calendar = calendars.detect { |cal| cal.display_name == "Calendav Test" }
54
-
55
- expect(calendar).to_not be_nil
56
-
57
- subject.calendars.update(calendar.url, display_name: "Calendav Update")
58
-
59
- calendars = subject.calendars.list
60
- calendar = calendars.detect { |cal| cal.display_name == "Calendav Update" }
61
-
62
- subject.calendars.update(calendar.url, display_name: "Calendav Test")
63
- end
64
-
65
- context "with a calendar" do
66
- let(:calendars) { subject.calendars.list }
67
- let(:calendar) { calendars.detect { |cal| cal.display_name == name } }
68
- let(:name) { "Calendav Test #{Time.now.to_i}" }
69
- let(:service) { Google::Apis::CalendarV3::CalendarService.new }
70
- let(:entry) { Google::Apis::CalendarV3::Calendar.new(summary: name) }
71
-
72
- before :each do
73
- service.authorization = google_auth
74
-
75
- result = service.insert_calendar entry
76
- entry.update!(**result.to_h)
77
- end
78
-
79
- after :each do
80
- service.delete_calendar entry.id
81
- end
82
-
83
- it_behaves_like "supporting event management"
84
-
85
- it "does not respect etag conditions for deletions" do
86
- event_url = subject.events.create(
87
- calendar.url, event_identifier, ical_event("Brunch", 10, 30)
88
- )
89
- event = subject.events.find(event_url)
90
-
91
- expect(
92
- subject.events.update(
93
- event_url, update_summary(event, "Coffee"), etag: event.etag
94
- )
95
- ).to eq(true)
96
-
97
- # Google doesn't care about the If-Match header on DELETE requests :(
98
- expect(subject.events.delete(event_url, etag: event.etag)).to eq(true)
99
- end
100
- end
101
- end
@@ -1,48 +0,0 @@
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
@@ -1,180 +0,0 @@
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
- # Wait for server to catch up
109
- sleep 1
110
-
111
- # Updating with the old etag should fail
112
- expect(
113
- subject.events.update(
114
- event_url, update_summary(event, "Brunch"), etag: event.etag
115
- )
116
- ).to eq(false)
117
-
118
- expect(subject.events.find(event_url).summary).to eq("Coffee")
119
-
120
- expect(subject.events.delete(event_url)).to eq(true)
121
- end
122
-
123
- it "handles synchronisation requests" do
124
- first_url = subject.events.create(
125
- calendar.url, event_identifier, ical_event("Brunch", 10, 30)
126
- )
127
- first = subject.events.find(first_url)
128
- token = subject.calendars.find(calendar.url, sync: true).sync_token
129
-
130
- events = subject.events.list(calendar.url)
131
- expect(events.length).to eq(1)
132
-
133
- second_url = subject.events.create(
134
- calendar.url, event_identifier, ical_event("Brunch Again", 11, 30)
135
- )
136
-
137
- subject.events.update(first_url, update_summary(first, "Coffee"))
138
- first = subject.events.find(first_url)
139
-
140
- collection = subject.calendars.sync(calendar.url, token)
141
- expect(collection.changes.collect(&:url))
142
- .to match_encoded_urls([first_url, second_url])
143
-
144
- expect(collection.deletions).to be_empty
145
- expect(collection.more?).to eq(false)
146
-
147
- subject.events.update(first_url, update_summary(first, "Brunch"))
148
- subject.events.delete(second_url)
149
-
150
- collection = subject.calendars.sync(calendar.url, collection.sync_token)
151
- urls = collection.changes.collect(&:url)
152
- expect(urls.length).to eq(1)
153
- expect(urls[0]).to eq_encoded_url(first_url)
154
-
155
- expect(collection.deletions.length).to eq(1)
156
- expect(collection.deletions.first).to eq_encoded_url(second_url)
157
- expect(collection.more?).to eq(false)
158
-
159
- subject.events.delete(first_url)
160
- end
161
- end
162
-
163
- RSpec.shared_examples "supporting event deletion with etags" do
164
- it "respects etag conditions with deletions" do
165
- event_url = subject.events.create(
166
- calendar.url, event_identifier, ical_event("Brunch", 10, 30)
167
- )
168
- event = subject.events.find(event_url)
169
-
170
- expect(
171
- subject.events.update(
172
- event_url, update_summary(event, "Coffee"), etag: event.etag
173
- )
174
- ).to eq(true)
175
- expect(subject.events.find(event_url).summary).to eq("Coffee")
176
-
177
- expect(subject.events.delete(event_url, etag: event.etag)).to eq(false)
178
- expect(subject.events.delete(event_url)).to eq(true)
179
- end
180
- end
@@ -1,10 +0,0 @@
1
- [auth]
2
- type = htpasswd
3
- htpasswd_filename = /var/radicale/users
4
- htpasswd_encryption = bcrypt
5
-
6
- [server]
7
- hosts = 0.0.0.0:8000
8
-
9
- [storage]
10
- filesystem_folder = /var/radicale/collections
@@ -1 +0,0 @@
1
- test:$2y$05$O.Z0365vM0GOv.YN1WlIEOnd0gaGEB/SmoRiSjIHnstXIm6ZURl/y
data/spec/spec_helper.rb DELETED
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "calendav"
4
- require "dotenv"
5
-
6
- Dotenv.load
7
-
8
- require_relative "support/encoded_matchers"
9
- require_relative "support/event_helpers"
10
-
11
- RSpec.configure do |config|
12
- config.disable_monkey_patching!
13
-
14
- config.expect_with :rspec do |c|
15
- c.syntax = :expect
16
- end
17
- end
@@ -1,29 +0,0 @@
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
@@ -1,42 +0,0 @@
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