async-caldav 1.2.3 → 1.3.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.
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/setup"
4
- require "scampi"
5
- require "async/caldav"
6
3
  require "net/http"
7
4
  require "uri"
8
5
  require "rexml/document"
@@ -32,17 +29,21 @@ module Async
32
29
 
33
30
  def self.open(base_url, **opts)
34
31
  client = new(base_url, **opts)
35
- return client unless block_given?
36
-
37
- begin
38
- yield client
39
- ensure
40
- client.close
32
+ if block_given?
33
+ begin
34
+ yield client
35
+ ensure
36
+ client.close
37
+ end
38
+ else
39
+ client
41
40
  end
42
41
  end
43
42
 
44
43
  def close
45
- @http&.finish if @http&.started?
44
+ if @http&.started?
45
+ @http&.finish
46
+ end
46
47
  @http = nil
47
48
  end
48
49
 
@@ -57,7 +58,9 @@ module Async
57
58
  def calendars
58
59
  path = "/calendars/#{@user}/"
59
60
  status, _, body = request('PROPFIND', path, headers: { 'Depth' => '1' })
60
- raise Error, "PROPFIND failed: #{status}" unless status == 207
61
+ unless status == 207
62
+ raise Error, "PROPFIND failed: #{status}"
63
+ end
61
64
 
62
65
  parse_collections(body, path, type: :calendar)
63
66
  end
@@ -65,7 +68,9 @@ module Async
65
68
  def addressbooks
66
69
  path = "/addressbooks/#{@user}/"
67
70
  status, _, body = request('PROPFIND', path, headers: { 'Depth' => '1' })
68
- raise Error, "PROPFIND failed: #{status}" unless status == 207
71
+ unless status == 207
72
+ raise Error, "PROPFIND failed: #{status}"
73
+ end
69
74
 
70
75
  parse_collections(body, path, type: :addressbook)
71
76
  end
@@ -87,14 +92,20 @@ module Async
87
92
  x.tag!("c:mkcalendar", "xmlns:d" => "DAV:", "xmlns:c" => "urn:ietf:params:xml:ns:caldav") do
88
93
  x.tag!("d:set") do
89
94
  x.tag!("d:prop") do
90
- x.tag!("d:displayname", displayname || name) if displayname || name
91
- x.tag!("c:calendar-description", description) if description
95
+ if displayname || name
96
+ x.tag!("d:displayname", displayname || name)
97
+ end
98
+ if description
99
+ x.tag!("c:calendar-description", description)
100
+ end
92
101
  end
93
102
  end
94
103
  end
95
104
 
96
105
  status, = request('MKCALENDAR', path, body: x.target!, headers: { 'Content-Type' => 'text/xml' })
97
- raise Error, "MKCALENDAR failed: #{status}" unless status == 201
106
+ unless status == 201
107
+ raise Error, "MKCALENDAR failed: #{status}"
108
+ end
98
109
 
99
110
  Calendar.new(self, path, displayname: displayname || name, description: description, color: color)
100
111
  end
@@ -113,7 +124,9 @@ module Async
113
124
  end
114
125
 
115
126
  status, = request('MKCOL', path, body: x.target!, headers: { 'Content-Type' => 'text/xml' })
116
- raise Error, "MKCOL failed: #{status}" unless status == 201
127
+ unless status == 201
128
+ raise Error, "MKCOL failed: #{status}"
129
+ end
117
130
 
118
131
  Addressbook.new(self, path, displayname: displayname || name)
119
132
  end
@@ -122,14 +135,21 @@ module Async
122
135
 
123
136
  def request(method, path, body: nil, headers: {})
124
137
  http = connect
125
- req = build_request(method, path, body, headers)
138
+ req = build_request(
139
+ method,
140
+ path,
141
+ body,
142
+ headers,
143
+ )
126
144
  response = http.request(req)
127
145
 
128
146
  status = response.code.to_i
129
147
  resp_headers = {}
130
148
  response.each_header { |k, v| resp_headers[k] = v }
131
149
 
132
- raise Unauthorized, "401 Unauthorized" if status == 401
150
+ if status == 401
151
+ raise Unauthorized, "401 Unauthorized"
152
+ end
133
153
 
134
154
  [status, resp_headers, response.body || '']
135
155
  end
@@ -137,445 +157,477 @@ module Async
137
157
  # --- Response parsing (used by Calendar/Addressbook) ---
138
158
 
139
159
  def parse_multistatus_items(xml, data_tag:)
140
- items = []
141
- xml.scan(/<[^>]*response[^>]*>(.*?)<\/[^>]*response>/m).each do |match|
142
- resp = match[0]
143
- href = resp.match(/<[^>]*href[^>]*>([^<]+)</)[1]&.strip rescue nil
144
- next unless href
160
+ [].tap do |items|
161
+ xml.scan(/<[^>]*response[^>]*>(.*?)<\/[^>]*response>/m).each do |match|
162
+ resp = match[0]
163
+ href = resp.match(/<[^>]*href[^>]*>([^<]+)</)[1]&.strip rescue nil
164
+ unless href
165
+ next
166
+ end
145
167
 
146
- etag = resp.match(/<[^>]*getetag[^>]*>([^<]+)</)[1]&.strip rescue nil
147
- data = resp.match(/<[^>]*#{data_tag}[^>]*>(.*?)<\/[^>]*#{data_tag}>/m)
148
- body = data ? data[1].strip : nil
168
+ etag = resp.match(/<[^>]*getetag[^>]*>([^<]+)</)[1]&.strip rescue nil
169
+ data = resp.match(/<[^>]*#{data_tag}[^>]*>(.*?)<\/[^>]*#{data_tag}>/m)
170
+ if data
171
+ body = data[1].strip
172
+ else
173
+ body = nil
174
+ end
149
175
 
150
- # Unescape XML entities in the body
151
- if body
152
- body = body.gsub('&amp;', '&').gsub('&lt;', '<').gsub('&gt;', '>').gsub('&quot;', '"')
153
- end
176
+ # Unescape XML entities in the body
177
+ if body
178
+ body = body.gsub('&amp;', '&').gsub('&lt;', '<').gsub('&gt;', '>').gsub('&quot;', '"')
179
+ end
154
180
 
155
- items << { path: href, body: body, etag: etag }
181
+ items << { path: href, body: body, etag: etag }
182
+ end
156
183
  end
157
- items
158
184
  end
159
185
 
160
186
  def parse_sync_items(xml)
161
- items = []
162
- xml.scan(/<[^>]*response[^>]*>(.*?)<\/[^>]*response>/m).each do |match|
163
- resp = match[0]
164
- href = resp.match(/<[^>]*href[^>]*>([^<]+)</)[1]&.strip rescue nil
165
- next unless href
166
-
167
- if resp.include?('404')
168
- items << { path: href, status: 404 }
169
- else
170
- etag = resp.match(/<[^>]*getetag[^>]*>([^<]+)</)[1]&.strip rescue nil
171
- items << { path: href, status: 200, etag: etag }
187
+ [].tap do |items|
188
+ xml.scan(/<[^>]*response[^>]*>(.*?)<\/[^>]*response>/m).each do |match|
189
+ resp = match[0]
190
+ href = resp.match(/<[^>]*href[^>]*>([^<]+)</)[1]&.strip rescue nil
191
+ unless href
192
+ next
193
+ end
194
+
195
+ if resp.include?('404')
196
+ items << { path: href, status: 404 }
197
+ else
198
+ etag = resp.match(/<[^>]*getetag[^>]*>([^<]+)</)[1]&.strip rescue nil
199
+ items << { path: href, status: 200, etag: etag }
200
+ end
172
201
  end
173
202
  end
174
- items
175
203
  end
176
204
 
177
205
  def parse_collection_props(xml)
178
- props = {}
179
- props[:displayname] = Protocol::Caldav::Xml.extract_value(xml, 'displayname')
180
- props[:description] = Protocol::Caldav::Xml.extract_value(xml, 'calendar-description')
181
- props[:color] = Protocol::Caldav::Xml.extract_value(xml, 'calendar-color')
182
- ctag = Protocol::Caldav::Xml.extract_value(xml, 'getctag')
183
- props[:ctag] = ctag if ctag
184
- props
206
+ {}.tap do |props|
207
+ props[:displayname] = Protocol::Caldav::Xml.extract_value(xml, 'displayname')
208
+ props[:description] = Protocol::Caldav::Xml.extract_value(xml, 'calendar-description')
209
+ props[:color] = Protocol::Caldav::Xml.extract_value(xml, 'calendar-color')
210
+ ctag = Protocol::Caldav::Xml.extract_value(xml, 'getctag')
211
+ if ctag
212
+ props[:ctag] = ctag
213
+ end
214
+ end
185
215
  end
186
216
 
187
217
  private
188
218
 
189
- def connect
190
- return @http if @http&.started?
191
-
192
- @http = Net::HTTP.new(@uri.host, @uri.port)
193
- @http.use_ssl = (@uri.scheme == 'https')
194
- @http.start
195
- @http
196
- end
197
-
198
- def build_request(method, path, body, headers)
199
- klass = case method
200
- when 'GET' then Net::HTTP::Get
201
- when 'PUT' then Net::HTTP::Put
202
- when 'DELETE' then Net::HTTP::Delete
203
- when 'HEAD' then Net::HTTP::Head
204
- when 'OPTIONS' then Net::HTTP::Options
205
- when 'PROPFIND' then propfind_class
206
- when 'PROPPATCH' then proppatch_class
207
- when 'MKCOL' then mkcol_class
208
- when 'MKCALENDAR' then mkcalendar_class
209
- when 'REPORT' then report_class
210
- when 'MOVE' then move_class
211
- else raise Error, "Unknown method: #{method}"
219
+ def connect
220
+ if @http&.started?
221
+ @http
222
+ else
223
+ @http = Net::HTTP.new(@uri.host, @uri.port)
224
+ @http.use_ssl = (@uri.scheme == 'https')
225
+ @http.start
226
+ @http
227
+ end
212
228
  end
213
229
 
214
- req = klass.new(path)
215
- req.body = body if body
230
+ def build_request(method, path, body, headers)
231
+ case method
232
+ when 'GET'
233
+ klass = Net::HTTP::Get
234
+ when 'PUT'
235
+ klass = Net::HTTP::Put
236
+ when 'DELETE'
237
+ klass = Net::HTTP::Delete
238
+ when 'HEAD'
239
+ klass = Net::HTTP::Head
240
+ when 'OPTIONS'
241
+ klass = Net::HTTP::Options
242
+ when 'PROPFIND'
243
+ klass = propfind_class
244
+ when 'PROPPATCH'
245
+ klass = proppatch_class
246
+ when 'MKCOL'
247
+ klass = mkcol_class
248
+ when 'MKCALENDAR'
249
+ klass = mkcalendar_class
250
+ when 'REPORT'
251
+ klass = report_class
252
+ when 'MOVE'
253
+ klass = move_class
254
+ else
255
+ klass = raise Error, "Unknown method: #{method}"
256
+ end
257
+
258
+ req = klass.new(path)
259
+ if body
260
+ req.body = body
261
+ end
216
262
 
217
- # Auth
218
- if @password
219
- req.basic_auth(@user, @password)
220
- else
221
- req['Remote-User'] = @user
222
- end
263
+ # Auth
264
+ if @password
265
+ req.basic_auth(@user, @password)
266
+ else
267
+ req['Remote-User'] = @user
268
+ end
223
269
 
224
- # Default + extra + per-request headers
225
- @extra_headers.each { |k, v| req[k] = v }
226
- headers.each { |k, v| req[k] = v }
270
+ # Default + extra + per-request headers
271
+ @extra_headers.each { |k, v| req[k] = v }
272
+ headers.each { |k, v| req[k] = v }
227
273
 
228
- req
229
- end
274
+ req
275
+ end
230
276
 
231
- def parse_collections(xml, parent_path, type:)
232
- collections = []
233
- xml.scan(/<[^>]*response[^>]*>(.*?)<\/[^>]*response>/m).each do |match|
234
- resp = match[0]
235
- href = resp.match(/<[^>]*href[^>]*>([^<]+)</)[1]&.strip rescue nil
236
- next unless href
237
- next if href == parent_path # skip the parent itself
238
-
239
- # Check resource type
240
- is_calendar = resp.include?('calendar/')
241
- is_addressbook = resp.include?('addressbook/')
242
-
243
- if type == :calendar && is_calendar
244
- displayname = Protocol::Caldav::Xml.extract_value(resp, 'displayname')
245
- description = Protocol::Caldav::Xml.extract_value(resp, 'calendar-description')
246
- color = Protocol::Caldav::Xml.extract_value(resp, 'calendar-color')
247
- collections << Calendar.new(self, href, displayname: displayname, description: description, color: color)
248
- elsif type == :addressbook && is_addressbook
249
- displayname = Protocol::Caldav::Xml.extract_value(resp, 'displayname')
250
- collections << Addressbook.new(self, href, displayname: displayname)
277
+ def parse_collections(xml, parent_path, type:)
278
+ [].tap do |collections|
279
+ xml.scan(/<[^>]*response[^>]*>(.*?)<\/[^>]*response>/m).each do |match|
280
+ resp = match[0]
281
+ href = resp.match(/<[^>]*href[^>]*>([^<]+)</)[1]&.strip rescue nil
282
+ unless href
283
+ next
284
+ end
285
+ if href == parent_path
286
+ next
287
+ end # skip the parent itself
288
+
289
+ # Check resource type
290
+ is_calendar = resp.include?('calendar/')
291
+ is_addressbook = resp.include?('addressbook/')
292
+
293
+ if type == :calendar && is_calendar
294
+ displayname = Protocol::Caldav::Xml.extract_value(resp, 'displayname')
295
+ description = Protocol::Caldav::Xml.extract_value(resp, 'calendar-description')
296
+ color = Protocol::Caldav::Xml.extract_value(resp, 'calendar-color')
297
+ collections << Calendar.new(self, href, displayname: displayname, description: description, color: color)
298
+ elsif type == :addressbook && is_addressbook
299
+ displayname = Protocol::Caldav::Xml.extract_value(resp, 'displayname')
300
+ collections << Addressbook.new(self, href, displayname: displayname)
301
+ end
302
+ end
251
303
  end
252
304
  end
253
- collections
254
- end
255
305
 
256
306
  # Custom HTTP method classes for WebDAV
257
- def propfind_class
258
- @propfind_class ||= Class.new(Net::HTTPRequest) do
259
- const_set(:METHOD, 'PROPFIND')
260
- const_set(:REQUEST_HAS_BODY, true)
261
- const_set(:RESPONSE_HAS_BODY, true)
307
+ def propfind_class
308
+ @propfind_class ||= Class.new(Net::HTTPRequest) do
309
+ const_set(:METHOD, 'PROPFIND')
310
+ const_set(:REQUEST_HAS_BODY, true)
311
+ const_set(:RESPONSE_HAS_BODY, true)
312
+ end
262
313
  end
263
- end
264
314
 
265
- def proppatch_class
266
- @proppatch_class ||= Class.new(Net::HTTPRequest) do
267
- const_set(:METHOD, 'PROPPATCH')
268
- const_set(:REQUEST_HAS_BODY, true)
269
- const_set(:RESPONSE_HAS_BODY, true)
315
+ def proppatch_class
316
+ @proppatch_class ||= Class.new(Net::HTTPRequest) do
317
+ const_set(:METHOD, 'PROPPATCH')
318
+ const_set(:REQUEST_HAS_BODY, true)
319
+ const_set(:RESPONSE_HAS_BODY, true)
320
+ end
270
321
  end
271
- end
272
322
 
273
- def mkcol_class
274
- @mkcol_class ||= Class.new(Net::HTTPRequest) do
275
- const_set(:METHOD, 'MKCOL')
276
- const_set(:REQUEST_HAS_BODY, true)
277
- const_set(:RESPONSE_HAS_BODY, true)
323
+ def mkcol_class
324
+ @mkcol_class ||= Class.new(Net::HTTPRequest) do
325
+ const_set(:METHOD, 'MKCOL')
326
+ const_set(:REQUEST_HAS_BODY, true)
327
+ const_set(:RESPONSE_HAS_BODY, true)
328
+ end
278
329
  end
279
- end
280
330
 
281
- def mkcalendar_class
282
- @mkcalendar_class ||= Class.new(Net::HTTPRequest) do
283
- const_set(:METHOD, 'MKCALENDAR')
284
- const_set(:REQUEST_HAS_BODY, true)
285
- const_set(:RESPONSE_HAS_BODY, true)
331
+ def mkcalendar_class
332
+ @mkcalendar_class ||= Class.new(Net::HTTPRequest) do
333
+ const_set(:METHOD, 'MKCALENDAR')
334
+ const_set(:REQUEST_HAS_BODY, true)
335
+ const_set(:RESPONSE_HAS_BODY, true)
336
+ end
286
337
  end
287
- end
288
338
 
289
- def report_class
290
- @report_class ||= Class.new(Net::HTTPRequest) do
291
- const_set(:METHOD, 'REPORT')
292
- const_set(:REQUEST_HAS_BODY, true)
293
- const_set(:RESPONSE_HAS_BODY, true)
339
+ def report_class
340
+ @report_class ||= Class.new(Net::HTTPRequest) do
341
+ const_set(:METHOD, 'REPORT')
342
+ const_set(:REQUEST_HAS_BODY, true)
343
+ const_set(:RESPONSE_HAS_BODY, true)
344
+ end
294
345
  end
295
- end
296
346
 
297
- def move_class
298
- @move_class ||= Class.new(Net::HTTPRequest) do
299
- const_set(:METHOD, 'MOVE')
300
- const_set(:REQUEST_HAS_BODY, false)
301
- const_set(:RESPONSE_HAS_BODY, true)
347
+ def move_class
348
+ @move_class ||= Class.new(Net::HTTPRequest) do
349
+ const_set(:METHOD, 'MOVE')
350
+ const_set(:REQUEST_HAS_BODY, false)
351
+ const_set(:RESPONSE_HAS_BODY, true)
352
+ end
302
353
  end
303
- end
304
354
  end
305
355
  end
306
356
  end
307
357
 
308
358
 
309
- test do
310
- # Mock transport that routes requests through the Server directly
311
- class MockTransport
312
- def initialize
313
- @storage = Async::Caldav::Storage::Mock.new
314
- @server = Async::Caldav::Server.new(storage: @storage)
315
- # Pre-create parent collections
316
- %w[/calendars/ /calendars/admin/ /addressbooks/ /addressbooks/admin/].each do |p|
317
- @storage.create_collection(p)
318
- end
319
- end
359
+ __END__
320
360
 
321
- attr_reader :storage
322
-
323
- def request(method, path, body, headers, user)
324
- env = {
325
- 'REQUEST_METHOD' => method,
326
- 'PATH_INFO' => path,
327
- 'rack.input' => StringIO.new(body || ''),
328
- 'dav.user' => user
329
- }
330
- headers.each do |k, v|
331
- rack_key = "HTTP_#{k.upcase.tr('-', '_')}"
332
- env[rack_key] = v
333
- end
334
- env['CONTENT_TYPE'] = headers['Content-Type'] if headers['Content-Type']
335
- @server.call(env)
361
+ require_relative "../caldav"
362
+
363
+ # Mock transport that routes requests through the Server directly
364
+ class MockTransport
365
+ def initialize
366
+ @storage = Async::Caldav::Storage::Mock.new
367
+ @server = Async::Caldav::Server.new(storage: @storage)
368
+ # Pre-create parent collections
369
+ %w[/calendars/ /calendars/admin/ /addressbooks/ /addressbooks/admin/].each do |p|
370
+ @storage.create_collection(p)
336
371
  end
337
372
  end
338
373
 
339
- # Client subclass that uses MockTransport instead of Net::HTTP
340
- class TestClient < Async::Caldav::Client
341
- def initialize(transport, user:)
342
- @transport = transport
343
- @user = user
344
- @extra_headers = {}
374
+ attr_reader :storage
375
+
376
+ def request(method, path, body, headers, user)
377
+ env = {
378
+ 'REQUEST_METHOD' => method,
379
+ 'PATH_INFO' => path,
380
+ 'rack.input' => StringIO.new(body || ''),
381
+ 'dav.user' => user
382
+ }
383
+ headers.each do |k, v|
384
+ rack_key = "HTTP_#{k.upcase.tr('-', '_')}"
385
+ env[rack_key] = v
345
386
  end
387
+ env['CONTENT_TYPE'] = headers['Content-Type'] if headers['Content-Type']
388
+ @server.call(env)
389
+ end
390
+ end
346
391
 
347
- def request(method, path, body: nil, headers: {})
348
- status, resp_headers, resp_body = @transport.request(method, path, body, headers, @user)
349
- body_str = resp_body.is_a?(Array) ? resp_body.join : resp_body
350
- [status, resp_headers, body_str]
351
- end
392
+ # Client subclass that uses MockTransport instead of Net::HTTP
393
+ class TestClient < Async::Caldav::Client
394
+ def initialize(transport, user:)
395
+ @transport = transport
396
+ @user = user
397
+ @extra_headers = {}
398
+ end
352
399
 
353
- def close; end
400
+ def request(method, path, body: nil, headers: {})
401
+ status, resp_headers, resp_body = @transport.request(method, path, body, headers, @user)
402
+ body_str = resp_body.is_a?(Array) ? resp_body.join : resp_body
403
+ [status, resp_headers, body_str]
354
404
  end
355
405
 
356
- require 'stringio'
406
+ def close; end
407
+ end
357
408
 
358
- describe "Async::Caldav::Client" do
359
- def make_client
360
- transport = MockTransport.new
361
- [TestClient.new(transport, user: 'admin'), transport]
362
- end
409
+ require 'stringio'
363
410
 
364
- it "principal returns user path" do
365
- client, = make_client
366
- client.principal.should.include '/admin/'
367
- end
411
+ describe "Async::Caldav::Client" do
412
+ def make_client
413
+ transport = MockTransport.new
414
+ [TestClient.new(transport, user: 'admin'), transport]
415
+ end
368
416
 
369
- it "create_calendar returns Calendar" do
370
- client, = make_client
371
- cal = client.create_calendar('work', displayname: 'Work')
372
- cal.should.be.instance_of Async::Caldav::Client::Calendar
373
- cal.path.should.equal '/calendars/admin/work/'
374
- cal.displayname.should.equal 'Work'
375
- end
417
+ it "principal returns user path" do
418
+ client, = make_client
419
+ client.principal.should.include '/admin/'
420
+ end
376
421
 
377
- it "calendars returns list of Calendar objects" do
378
- client, = make_client
379
- client.create_calendar('cal1', displayname: 'Cal 1')
380
- client.create_calendar('cal2', displayname: 'Cal 2')
381
- cals = client.calendars
382
- cals.length.should.equal 2
383
- cals.all? { |c| c.is_a?(Async::Caldav::Client::Calendar) }.should.equal true
384
- end
422
+ it "create_calendar returns Calendar" do
423
+ client, = make_client
424
+ cal = client.create_calendar('work', displayname: 'Work')
425
+ cal.should.be.instance_of Async::Caldav::Client::Calendar
426
+ cal.path.should.equal '/calendars/admin/work/'
427
+ cal.displayname.should.equal 'Work'
428
+ end
385
429
 
386
- it "create_addressbook returns Addressbook" do
387
- client, = make_client
388
- ab = client.create_addressbook('contacts', displayname: 'Contacts')
389
- ab.should.be.instance_of Async::Caldav::Client::Addressbook
390
- ab.path.should.equal '/addressbooks/admin/contacts/'
391
- end
430
+ it "calendars returns list of Calendar objects" do
431
+ client, = make_client
432
+ client.create_calendar('cal1', displayname: 'Cal 1')
433
+ client.create_calendar('cal2', displayname: 'Cal 2')
434
+ cals = client.calendars
435
+ cals.length.should.equal 2
436
+ cals.all? { |c| c.is_a?(Async::Caldav::Client::Calendar) }.should.equal true
437
+ end
392
438
 
393
- it "addressbooks returns list of Addressbook objects" do
394
- client, = make_client
395
- client.create_addressbook('ab1', displayname: 'AB 1')
396
- abs = client.addressbooks
397
- abs.length.should.equal 1
398
- abs[0].displayname.should.equal 'AB 1'
399
- end
439
+ it "create_addressbook returns Addressbook" do
440
+ client, = make_client
441
+ ab = client.create_addressbook('contacts', displayname: 'Contacts')
442
+ ab.should.be.instance_of Async::Caldav::Client::Addressbook
443
+ ab.path.should.equal '/addressbooks/admin/contacts/'
444
+ end
400
445
 
401
- it "calendar returns Calendar for name" do
402
- client, = make_client
403
- cal = client.calendar('work')
404
- cal.path.should.equal '/calendars/admin/work/'
405
- end
446
+ it "addressbooks returns list of Addressbook objects" do
447
+ client, = make_client
448
+ client.create_addressbook('ab1', displayname: 'AB 1')
449
+ abs = client.addressbooks
450
+ abs.length.should.equal 1
451
+ abs[0].displayname.should.equal 'AB 1'
452
+ end
406
453
 
407
- it "open with block yields client and closes" do
408
- transport = MockTransport.new
409
- yielded = nil
410
- TestClient.open('http://localhost', user: 'admin') do |c|
411
- # We can't use TestClient.open directly since it calls new with base_url
412
- # but this tests the block pattern
413
- yielded = true
414
- end
415
- yielded.should.equal true
416
- end
454
+ it "calendar returns Calendar for name" do
455
+ client, = make_client
456
+ cal = client.calendar('work')
457
+ cal.path.should.equal '/calendars/admin/work/'
417
458
  end
418
459
 
419
- describe "Async::Caldav::Client::Calendar" do
420
- def make_client
421
- transport = MockTransport.new
422
- [TestClient.new(transport, user: 'admin'), transport]
460
+ it "open with block yields client and closes" do
461
+ transport = MockTransport.new
462
+ yielded = nil
463
+ TestClient.open('http://localhost', user: 'admin') do |c|
464
+ # We can't use TestClient.open directly since it calls new with base_url
465
+ # but this tests the block pattern
466
+ yielded = true
423
467
  end
468
+ yielded.should.equal true
469
+ end
470
+ end
424
471
 
425
- it "put_event creates event and returns etag" do
426
- client, = make_client
427
- cal = client.create_calendar('work')
428
- result = cal.put_event('ev.ics', "BEGIN:VCALENDAR\r\nUID:1\r\nEND:VCALENDAR")
429
- result[:status].should.equal 201
430
- result[:etag].should.not.be.nil
431
- end
472
+ describe "Async::Caldav::Client::Calendar" do
473
+ def make_client
474
+ transport = MockTransport.new
475
+ [TestClient.new(transport, user: 'admin'), transport]
476
+ end
432
477
 
433
- it "get_event retrieves event body and etag" do
434
- client, = make_client
435
- cal = client.create_calendar('work')
436
- cal.put_event('ev.ics', "BEGIN:VCALENDAR\r\nUID:1\r\nSUMMARY:Test\r\nEND:VCALENDAR")
437
- result = cal.get_event('ev.ics')
438
- result[:body].should.include 'SUMMARY:Test'
439
- result[:etag].should.not.be.nil
440
- end
478
+ it "put_event creates event and returns etag" do
479
+ client, = make_client
480
+ cal = client.create_calendar('work')
481
+ result = cal.put_event('ev.ics', "BEGIN:VCALENDAR\r\nUID:1\r\nEND:VCALENDAR")
482
+ result[:status].should.equal 201
483
+ result[:etag].should.not.be.nil
484
+ end
441
485
 
442
- it "events returns items from REPORT" do
443
- client, = make_client
444
- cal = client.create_calendar('work')
445
- cal.put_event('ev.ics', "BEGIN:VCALENDAR\r\nUID:1\r\nSUMMARY:Meeting\r\nEND:VCALENDAR")
446
- items = cal.events
447
- items.length.should.equal 1
448
- items[0][:path].should.include 'ev.ics'
449
- end
486
+ it "get_event retrieves event body and etag" do
487
+ client, = make_client
488
+ cal = client.create_calendar('work')
489
+ cal.put_event('ev.ics', "BEGIN:VCALENDAR\r\nUID:1\r\nSUMMARY:Test\r\nEND:VCALENDAR")
490
+ result = cal.get_event('ev.ics')
491
+ result[:body].should.include 'SUMMARY:Test'
492
+ result[:etag].should.not.be.nil
493
+ end
450
494
 
451
- it "delete_event removes event" do
452
- client, = make_client
453
- cal = client.create_calendar('work')
454
- cal.put_event('ev.ics', "BEGIN:VCALENDAR\r\nUID:1\r\nEND:VCALENDAR")
455
- cal.delete_event('ev.ics').should.equal true
456
- lambda { cal.get_event('ev.ics') }.should.raise Async::Caldav::Client::NotFound
457
- end
495
+ it "events returns items from REPORT" do
496
+ client, = make_client
497
+ cal = client.create_calendar('work')
498
+ cal.put_event('ev.ics', "BEGIN:VCALENDAR\r\nUID:1\r\nSUMMARY:Meeting\r\nEND:VCALENDAR")
499
+ items = cal.events
500
+ items.length.should.equal 1
501
+ items[0][:path].should.include 'ev.ics'
502
+ end
458
503
 
459
- it "delete removes collection" do
460
- client, = make_client
461
- cal = client.create_calendar('work')
462
- cal.delete.should.equal true
463
- end
504
+ it "delete_event removes event" do
505
+ client, = make_client
506
+ cal = client.create_calendar('work')
507
+ cal.put_event('ev.ics', "BEGIN:VCALENDAR\r\nUID:1\r\nEND:VCALENDAR")
508
+ cal.delete_event('ev.ics').should.equal true
509
+ lambda { cal.get_event('ev.ics') }.should.raise Async::Caldav::Client::NotFound
510
+ end
464
511
 
465
- it "put_event with if_match sends precondition" do
466
- client, = make_client
467
- cal = client.create_calendar('work')
468
- result = cal.put_event('ev.ics', "BEGIN:VCALENDAR\r\nUID:1\r\nEND:VCALENDAR")
469
- etag = result[:etag]
512
+ it "delete removes collection" do
513
+ client, = make_client
514
+ cal = client.create_calendar('work')
515
+ cal.delete.should.equal true
516
+ end
470
517
 
471
- # Update with correct etag works
472
- result2 = cal.put_event('ev.ics', "BEGIN:VCALENDAR\r\nUID:1\r\nSUMMARY:V2\r\nEND:VCALENDAR", if_match: etag)
473
- result2[:status].should.equal 204
518
+ it "put_event with if_match sends precondition" do
519
+ client, = make_client
520
+ cal = client.create_calendar('work')
521
+ result = cal.put_event('ev.ics', "BEGIN:VCALENDAR\r\nUID:1\r\nEND:VCALENDAR")
522
+ etag = result[:etag]
474
523
 
475
- # Update with wrong etag fails
476
- lambda {
477
- cal.put_event('ev.ics', "BEGIN:VCALENDAR\r\nUID:1\r\nSUMMARY:V3\r\nEND:VCALENDAR", if_match: '"wrong"')
478
- }.should.raise Async::Caldav::Client::PreconditionFailed
479
- end
524
+ # Update with correct etag works
525
+ result2 = cal.put_event('ev.ics', "BEGIN:VCALENDAR\r\nUID:1\r\nSUMMARY:V2\r\nEND:VCALENDAR", if_match: etag)
526
+ result2[:status].should.equal 204
480
527
 
481
- it "put_event with if_none_match * prevents overwrite" do
482
- client, = make_client
483
- cal = client.create_calendar('work')
484
- cal.put_event('ev.ics', "BEGIN:VCALENDAR\r\nUID:1\r\nEND:VCALENDAR")
528
+ # Update with wrong etag fails
529
+ lambda {
530
+ cal.put_event('ev.ics', "BEGIN:VCALENDAR\r\nUID:1\r\nSUMMARY:V3\r\nEND:VCALENDAR", if_match: '"wrong"')
531
+ }.should.raise Async::Caldav::Client::PreconditionFailed
532
+ end
485
533
 
486
- lambda {
487
- cal.put_event('ev.ics', "BEGIN:VCALENDAR\r\nUID:1\r\nEND:VCALENDAR", if_none_match: '*')
488
- }.should.raise Async::Caldav::Client::PreconditionFailed
489
- end
534
+ it "put_event with if_none_match * prevents overwrite" do
535
+ client, = make_client
536
+ cal = client.create_calendar('work')
537
+ cal.put_event('ev.ics', "BEGIN:VCALENDAR\r\nUID:1\r\nEND:VCALENDAR")
490
538
 
491
- it "put_event with duplicate UID raises Conflict" do
492
- client, = make_client
493
- cal = client.create_calendar('work')
494
- cal.put_event('a.ics', "BEGIN:VCALENDAR\r\nUID:same\r\nEND:VCALENDAR")
539
+ lambda {
540
+ cal.put_event('ev.ics', "BEGIN:VCALENDAR\r\nUID:1\r\nEND:VCALENDAR", if_none_match: '*')
541
+ }.should.raise Async::Caldav::Client::PreconditionFailed
542
+ end
495
543
 
496
- lambda {
497
- cal.put_event('b.ics', "BEGIN:VCALENDAR\r\nUID:same\r\nEND:VCALENDAR")
498
- }.should.raise Async::Caldav::Client::Conflict
499
- end
544
+ it "put_event with duplicate UID raises Conflict" do
545
+ client, = make_client
546
+ cal = client.create_calendar('work')
547
+ cal.put_event('a.ics', "BEGIN:VCALENDAR\r\nUID:same\r\nEND:VCALENDAR")
500
548
 
501
- it "sync returns items and token" do
502
- client, = make_client
503
- cal = client.create_calendar('work')
504
- cal.put_event('ev.ics', "BEGIN:VCALENDAR\r\nUID:1\r\nEND:VCALENDAR")
549
+ lambda {
550
+ cal.put_event('b.ics', "BEGIN:VCALENDAR\r\nUID:same\r\nEND:VCALENDAR")
551
+ }.should.raise Async::Caldav::Client::Conflict
552
+ end
505
553
 
506
- items, token = cal.sync
507
- items.length.should.equal 1
508
- token.should.not.be.nil
509
- end
554
+ it "sync returns items and token" do
555
+ client, = make_client
556
+ cal = client.create_calendar('work')
557
+ cal.put_event('ev.ics', "BEGIN:VCALENDAR\r\nUID:1\r\nEND:VCALENDAR")
510
558
 
511
- it "sync with token returns incremental changes" do
512
- client, = make_client
513
- cal = client.create_calendar('work')
514
- cal.put_event('ev.ics', "BEGIN:VCALENDAR\r\nUID:1\r\nEND:VCALENDAR")
559
+ items, token = cal.sync
560
+ items.length.should.equal 1
561
+ token.should.not.be.nil
562
+ end
515
563
 
516
- _, token = cal.sync
564
+ it "sync with token returns incremental changes" do
565
+ client, = make_client
566
+ cal = client.create_calendar('work')
567
+ cal.put_event('ev.ics', "BEGIN:VCALENDAR\r\nUID:1\r\nEND:VCALENDAR")
517
568
 
518
- # No changes
519
- items, token2 = cal.sync(token: token)
520
- items.length.should.equal 0
569
+ _, token = cal.sync
521
570
 
522
- # Add item
523
- cal.put_event('ev2.ics', "BEGIN:VCALENDAR\r\nUID:2\r\nEND:VCALENDAR")
524
- items, = cal.sync(token: token2)
525
- items.length.should.equal 1
526
- items[0][:path].should.include 'ev2.ics'
527
- end
571
+ # No changes
572
+ items, token2 = cal.sync(token: token)
573
+ items.length.should.equal 0
528
574
 
529
- it "proppatch updates properties" do
530
- client, = make_client
531
- cal = client.create_calendar('work', displayname: 'Old')
532
- cal.proppatch(displayname: 'New')
533
- cal.displayname.should.equal 'New'
534
- end
575
+ # Add item
576
+ cal.put_event('ev2.ics', "BEGIN:VCALENDAR\r\nUID:2\r\nEND:VCALENDAR")
577
+ items, = cal.sync(token: token2)
578
+ items.length.should.equal 1
579
+ items[0][:path].should.include 'ev2.ics'
535
580
  end
536
581
 
537
- describe "Async::Caldav::Client::Addressbook" do
538
- def make_client
539
- transport = MockTransport.new
540
- [TestClient.new(transport, user: 'admin'), transport]
541
- end
582
+ it "proppatch updates properties" do
583
+ client, = make_client
584
+ cal = client.create_calendar('work', displayname: 'Old')
585
+ cal.proppatch(displayname: 'New')
586
+ cal.displayname.should.equal 'New'
587
+ end
588
+ end
542
589
 
543
- it "put_contact creates contact and returns etag" do
544
- client, = make_client
545
- ab = client.create_addressbook('contacts')
546
- result = ab.put_contact('alice.vcf', "BEGIN:VCARD\r\nUID:1\r\nFN:Alice\r\nEND:VCARD")
547
- result[:status].should.equal 201
548
- result[:etag].should.not.be.nil
549
- end
590
+ describe "Async::Caldav::Client::Addressbook" do
591
+ def make_client
592
+ transport = MockTransport.new
593
+ [TestClient.new(transport, user: 'admin'), transport]
594
+ end
550
595
 
551
- it "get_contact retrieves contact" do
552
- client, = make_client
553
- ab = client.create_addressbook('contacts')
554
- ab.put_contact('alice.vcf', "BEGIN:VCARD\r\nUID:1\r\nFN:Alice\r\nEND:VCARD")
555
- result = ab.get_contact('alice.vcf')
556
- result[:body].should.include 'FN:Alice'
557
- end
596
+ it "put_contact creates contact and returns etag" do
597
+ client, = make_client
598
+ ab = client.create_addressbook('contacts')
599
+ result = ab.put_contact('alice.vcf', "BEGIN:VCARD\r\nUID:1\r\nFN:Alice\r\nEND:VCARD")
600
+ result[:status].should.equal 201
601
+ result[:etag].should.not.be.nil
602
+ end
558
603
 
559
- it "contacts returns items from REPORT" do
560
- client, = make_client
561
- ab = client.create_addressbook('contacts')
562
- ab.put_contact('alice.vcf', "BEGIN:VCARD\r\nUID:1\r\nFN:Alice\r\nEND:VCARD")
563
- items = ab.contacts
564
- items.length.should.equal 1
565
- end
604
+ it "get_contact retrieves contact" do
605
+ client, = make_client
606
+ ab = client.create_addressbook('contacts')
607
+ ab.put_contact('alice.vcf', "BEGIN:VCARD\r\nUID:1\r\nFN:Alice\r\nEND:VCARD")
608
+ result = ab.get_contact('alice.vcf')
609
+ result[:body].should.include 'FN:Alice'
610
+ end
566
611
 
567
- it "delete_contact removes contact" do
568
- client, = make_client
569
- ab = client.create_addressbook('contacts')
570
- ab.put_contact('alice.vcf', "BEGIN:VCARD\r\nUID:1\r\nFN:Alice\r\nEND:VCARD")
571
- ab.delete_contact('alice.vcf').should.equal true
572
- lambda { ab.get_contact('alice.vcf') }.should.raise Async::Caldav::Client::NotFound
573
- end
612
+ it "contacts returns items from REPORT" do
613
+ client, = make_client
614
+ ab = client.create_addressbook('contacts')
615
+ ab.put_contact('alice.vcf', "BEGIN:VCARD\r\nUID:1\r\nFN:Alice\r\nEND:VCARD")
616
+ items = ab.contacts
617
+ items.length.should.equal 1
618
+ end
574
619
 
575
- it "delete removes collection" do
576
- client, = make_client
577
- ab = client.create_addressbook('contacts')
578
- ab.delete.should.equal true
579
- end
620
+ it "delete_contact removes contact" do
621
+ client, = make_client
622
+ ab = client.create_addressbook('contacts')
623
+ ab.put_contact('alice.vcf', "BEGIN:VCARD\r\nUID:1\r\nFN:Alice\r\nEND:VCARD")
624
+ ab.delete_contact('alice.vcf').should.equal true
625
+ lambda { ab.get_contact('alice.vcf') }.should.raise Async::Caldav::Client::NotFound
580
626
  end
581
- end
627
+
628
+ it "delete removes collection" do
629
+ client, = make_client
630
+ ab = client.create_addressbook('contacts')
631
+ ab.delete.should.equal true
632
+ end
633
+ end