async-caldav 1.2.4 → 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,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/setup"
4
- require "scampi"
5
- require "async/caldav"
6
-
7
3
  module Async
8
4
  module Caldav
9
5
  module Storage
@@ -17,15 +13,15 @@ module Async
17
13
  # --- Collections ---
18
14
 
19
15
  def create_collection(path, props = {})
20
- col = {
21
- type: props[:type] || :collection,
16
+ {
17
+ type: props[:type] || :collection,
22
18
  displayname: props[:displayname],
23
19
  description: props[:description],
24
- color: props[:color],
25
- props: props[:props] || {}
26
- }
27
- @collections[path] = col
28
- col
20
+ color: props[:color],
21
+ props: props[:props] || {},
22
+ }.tap do |col|
23
+ @collections[path] = col
24
+ end
29
25
  end
30
26
 
31
27
  def get_collection(path)
@@ -42,17 +38,29 @@ module Async
42
38
  end
43
39
 
44
40
  def list_collections(parent_path)
45
- parent = parent_path.end_with?('/') ? parent_path : "#{parent_path}/"
41
+ if parent_path.end_with?('/')
42
+ parent = parent_path
43
+ else
44
+ parent = "#{parent_path}/"
45
+ end
46
46
  @collections.select { |k, _| direct_child?(k, parent) }.to_a
47
47
  end
48
48
 
49
49
  def update_collection(path, props)
50
50
  col = @collections[path]
51
51
  if col
52
- col[:displayname] = props[:displayname] if props.key?(:displayname)
53
- col[:description] = props[:description] if props.key?(:description)
54
- col[:color] = props[:color] if props.key?(:color)
55
- col[:props] = (col[:props] || {}).merge(props[:props]) if props.key?(:props)
52
+ if props.key?(:displayname)
53
+ col[:displayname] = props[:displayname]
54
+ end
55
+ if props.key?(:description)
56
+ col[:description] = props[:description]
57
+ end
58
+ if props.key?(:color)
59
+ col[:color] = props[:color]
60
+ end
61
+ if props.key?(:props)
62
+ col[:props] = (col[:props] || {}).merge(props[:props])
63
+ end
56
64
  col
57
65
  end
58
66
  end
@@ -80,7 +88,11 @@ module Async
80
88
  end
81
89
 
82
90
  def list_items(collection_path)
83
- prefix = collection_path.end_with?('/') ? collection_path : "#{collection_path}/"
91
+ if collection_path.end_with?('/')
92
+ prefix = collection_path
93
+ else
94
+ prefix = "#{collection_path}/"
95
+ end
84
96
  @items.select { |k, _| k.start_with?(prefix) && k != collection_path }.to_a
85
97
  end
86
98
 
@@ -107,11 +119,11 @@ module Async
107
119
  item_etags = items.map { |_, data| data[:etag] }
108
120
  col = @collections[collection_path] || {}
109
121
  ctag = Protocol::Caldav::CTag.compute(
110
- path: collection_path,
122
+ path: collection_path,
111
123
  displayname: col[:displayname],
112
124
  description: col[:description],
113
- color: col[:color],
114
- item_etags: item_etags
125
+ color: col[:color],
126
+ item_etags: item_etags,
115
127
  )
116
128
  token = "http://caldav.local/sync/#{ctag}"
117
129
 
@@ -121,33 +133,27 @@ module Async
121
133
 
122
134
  def sync_changes(collection_path, token)
123
135
  old_snapshot_entry = @sync_snapshots[token]
124
- return nil unless old_snapshot_entry
125
-
126
- old_snapshot = old_snapshot_entry[collection_path] || {}
127
-
128
- # Take new snapshot
129
- new_token = snapshot_sync(collection_path)
130
- current_items = list_items(collection_path)
131
- current = {}
132
- current_items.each { |path, data| current[path] = data[:etag] }
133
-
134
- changes = []
135
-
136
- # Items that are new or modified
137
- current.each do |path, etag|
138
- if !old_snapshot.key?(path) || old_snapshot[path] != etag
139
- changes << [path, :modified]
136
+ if old_snapshot_entry
137
+ old_snapshot = old_snapshot_entry[collection_path] || {}
138
+ new_token = snapshot_sync(collection_path)
139
+ current_items = list_items(collection_path)
140
+ current = {}
141
+ current_items.each { |path, data| current[path] = data[:etag] }
142
+ changes = []
143
+ current.each do |path, etag|
144
+ if !old_snapshot.key?(path) || old_snapshot[path] != etag
145
+ changes << [path, :modified]
146
+ end
140
147
  end
141
- end
142
-
143
- # Items that were deleted
144
- old_snapshot.each_key do |path|
145
- unless current.key?(path)
146
- changes << [path, :deleted]
148
+ old_snapshot.each_key do |path|
149
+ unless current.key?(path)
150
+ changes << [path, :deleted]
151
+ end
147
152
  end
153
+ [new_token, changes]
154
+ else
155
+ nil
148
156
  end
149
-
150
- [new_token, changes]
151
157
  end
152
158
 
153
159
  # --- General ---
@@ -163,240 +169,242 @@ module Async
163
169
 
164
170
  private
165
171
 
166
- def direct_child?(child, parent)
167
- if child.start_with?(parent)
168
- remainder = child[parent.length..]
169
- remainder.chomp('/').count('/').zero? && !remainder.chomp('/').empty?
170
- else
171
- false
172
+ def direct_child?(child, parent)
173
+ if child.start_with?(parent)
174
+ remainder = child[parent.length..]
175
+ remainder.chomp('/').count('/').zero? && !remainder.chomp('/').empty?
176
+ else
177
+ false
178
+ end
172
179
  end
173
- end
174
180
  end
175
181
  end
176
182
  end
177
183
  end
178
184
 
179
185
 
180
- test do
181
- describe "Async::Caldav::Storage::Mock" do
182
- it "creates and retrieves a collection" do
183
- s = Async::Caldav::Storage::Mock.new
184
- s.create_collection("/calendars/admin/cal1/", type: :calendar, displayname: "Cal 1")
185
- col = s.get_collection("/calendars/admin/cal1/")
186
- col.should.not.be.nil
187
- col[:displayname].should.equal "Cal 1"
188
- col[:type].should.equal :calendar
189
- end
186
+ __END__
190
187
 
191
- it "deletes a collection and its items" do
192
- s = Async::Caldav::Storage::Mock.new
193
- s.create_collection("/calendars/admin/cal/")
194
- s.put_item("/calendars/admin/cal/event.ics", "VCALENDAR", "text/calendar")
195
- s.delete_collection("/calendars/admin/cal/")
196
- s.get_collection("/calendars/admin/cal/").should.be.nil
197
- s.get_item("/calendars/admin/cal/event.ics").should.be.nil
198
- end
188
+ require_relative "../../caldav"
199
189
 
200
- it "delete_collection does not affect siblings" do
201
- s = Async::Caldav::Storage::Mock.new
202
- s.create_collection("/calendars/admin/a/")
203
- s.create_collection("/calendars/admin/b/")
204
- s.delete_collection("/calendars/admin/a/")
205
- s.get_collection("/calendars/admin/b/").should.not.be.nil
206
- end
190
+ describe "Async::Caldav::Storage::Mock" do
191
+ it "creates and retrieves a collection" do
192
+ s = Async::Caldav::Storage::Mock.new
193
+ s.create_collection("/calendars/admin/cal1/", type: :calendar, displayname: "Cal 1")
194
+ col = s.get_collection("/calendars/admin/cal1/")
195
+ col.should.not.be.nil
196
+ col[:displayname].should.equal "Cal 1"
197
+ col[:type].should.equal :calendar
198
+ end
207
199
 
208
- it "delete_collection returns true/false" do
209
- s = Async::Caldav::Storage::Mock.new
210
- s.create_collection("/col/")
211
- s.delete_collection("/col/").should.equal true
212
- s.delete_collection("/col/").should.equal false
213
- end
200
+ it "deletes a collection and its items" do
201
+ s = Async::Caldav::Storage::Mock.new
202
+ s.create_collection("/calendars/admin/cal/")
203
+ s.put_item("/calendars/admin/cal/event.ics", "VCALENDAR", "text/calendar")
204
+ s.delete_collection("/calendars/admin/cal/")
205
+ s.get_collection("/calendars/admin/cal/").should.be.nil
206
+ s.get_item("/calendars/admin/cal/event.ics").should.be.nil
207
+ end
214
208
 
215
- it "lists direct child collections only" do
216
- s = Async::Caldav::Storage::Mock.new
217
- s.create_collection("/calendars/admin/a/")
218
- s.create_collection("/calendars/admin/b/")
219
- s.create_collection("/calendars/admin/a/nested/")
220
- list = s.list_collections("/calendars/admin/")
221
- list.length.should.equal 2
222
- end
209
+ it "delete_collection does not affect siblings" do
210
+ s = Async::Caldav::Storage::Mock.new
211
+ s.create_collection("/calendars/admin/a/")
212
+ s.create_collection("/calendars/admin/b/")
213
+ s.delete_collection("/calendars/admin/a/")
214
+ s.get_collection("/calendars/admin/b/").should.not.be.nil
215
+ end
223
216
 
224
- it "list_collections returns empty on no children" do
225
- s = Async::Caldav::Storage::Mock.new
226
- s.list_collections("/nope/").should.equal []
227
- end
217
+ it "delete_collection returns true/false" do
218
+ s = Async::Caldav::Storage::Mock.new
219
+ s.create_collection("/col/")
220
+ s.delete_collection("/col/").should.equal true
221
+ s.delete_collection("/col/").should.equal false
222
+ end
228
223
 
229
- it "updates collection properties, leaves others untouched" do
230
- s = Async::Caldav::Storage::Mock.new
231
- s.create_collection("/cal/", displayname: "Old", description: "Desc")
232
- s.update_collection("/cal/", displayname: "New")
233
- col = s.get_collection("/cal/")
234
- col[:displayname].should.equal "New"
235
- col[:description].should.equal "Desc"
236
- end
224
+ it "lists direct child collections only" do
225
+ s = Async::Caldav::Storage::Mock.new
226
+ s.create_collection("/calendars/admin/a/")
227
+ s.create_collection("/calendars/admin/b/")
228
+ s.create_collection("/calendars/admin/a/nested/")
229
+ list = s.list_collections("/calendars/admin/")
230
+ list.length.should.equal 2
231
+ end
237
232
 
238
- it "update_collection returns nil for nonexistent" do
239
- s = Async::Caldav::Storage::Mock.new
240
- s.update_collection("/nope/", displayname: "X").should.be.nil
241
- end
233
+ it "list_collections returns empty on no children" do
234
+ s = Async::Caldav::Storage::Mock.new
235
+ s.list_collections("/nope/").should.equal []
236
+ end
242
237
 
243
- it "put_item returns is_new true for new, false for existing" do
244
- s = Async::Caldav::Storage::Mock.new
245
- _, is_new1 = s.put_item("/cal/ev.ics", "body1", "text/calendar")
246
- is_new1.should.equal true
247
- _, is_new2 = s.put_item("/cal/ev.ics", "body2", "text/calendar")
248
- is_new2.should.equal false
249
- end
238
+ it "updates collection properties, leaves others untouched" do
239
+ s = Async::Caldav::Storage::Mock.new
240
+ s.create_collection("/cal/", displayname: "Old", description: "Desc")
241
+ s.update_collection("/cal/", displayname: "New")
242
+ col = s.get_collection("/cal/")
243
+ col[:displayname].should.equal "New"
244
+ col[:description].should.equal "Desc"
245
+ end
250
246
 
251
- it "put_item overwrites the body" do
252
- s = Async::Caldav::Storage::Mock.new
253
- s.put_item("/cal/ev.ics", "old", "text/calendar")
254
- s.put_item("/cal/ev.ics", "new", "text/calendar")
255
- s.get_item("/cal/ev.ics")[:body].should.equal "new"
256
- end
247
+ it "update_collection returns nil for nonexistent" do
248
+ s = Async::Caldav::Storage::Mock.new
249
+ s.update_collection("/nope/", displayname: "X").should.be.nil
250
+ end
257
251
 
258
- it "get_item returns nil for nonexistent" do
259
- s = Async::Caldav::Storage::Mock.new
260
- s.get_item("/nope").should.be.nil
261
- end
252
+ it "put_item returns is_new true for new, false for existing" do
253
+ s = Async::Caldav::Storage::Mock.new
254
+ _, is_new1 = s.put_item("/cal/ev.ics", "body1", "text/calendar")
255
+ is_new1.should.equal true
256
+ _, is_new2 = s.put_item("/cal/ev.ics", "body2", "text/calendar")
257
+ is_new2.should.equal false
258
+ end
262
259
 
263
- it "delete_item returns true/false" do
264
- s = Async::Caldav::Storage::Mock.new
265
- s.put_item("/cal/ev.ics", "d", "text/calendar")
266
- s.delete_item("/cal/ev.ics").should.equal true
267
- s.delete_item("/cal/ev.ics").should.equal false
268
- end
260
+ it "put_item overwrites the body" do
261
+ s = Async::Caldav::Storage::Mock.new
262
+ s.put_item("/cal/ev.ics", "old", "text/calendar")
263
+ s.put_item("/cal/ev.ics", "new", "text/calendar")
264
+ s.get_item("/cal/ev.ics")[:body].should.equal "new"
265
+ end
269
266
 
270
- it "list_items returns only items, not collections" do
271
- s = Async::Caldav::Storage::Mock.new
272
- s.create_collection("/cal/")
273
- s.put_item("/cal/ev.ics", "data", "text/calendar")
274
- items = s.list_items("/cal/")
275
- items.length.should.equal 1
276
- items[0][0].should.equal "/cal/ev.ics"
277
- end
267
+ it "get_item returns nil for nonexistent" do
268
+ s = Async::Caldav::Storage::Mock.new
269
+ s.get_item("/nope").should.be.nil
270
+ end
278
271
 
279
- it "list_items on empty collection returns empty" do
280
- s = Async::Caldav::Storage::Mock.new
281
- s.list_items("/empty/").should.equal []
282
- end
272
+ it "delete_item returns true/false" do
273
+ s = Async::Caldav::Storage::Mock.new
274
+ s.put_item("/cal/ev.ics", "d", "text/calendar")
275
+ s.delete_item("/cal/ev.ics").should.equal true
276
+ s.delete_item("/cal/ev.ics").should.equal false
277
+ end
283
278
 
284
- it "move_item removes source, creates destination" do
285
- s = Async::Caldav::Storage::Mock.new
286
- s.put_item("/cal/a.ics", "data", "text/calendar")
287
- s.move_item("/cal/a.ics", "/cal/b.ics")
288
- s.get_item("/cal/a.ics").should.be.nil
289
- s.get_item("/cal/b.ics")[:body].should.equal "data"
290
- end
279
+ it "list_items returns only items, not collections" do
280
+ s = Async::Caldav::Storage::Mock.new
281
+ s.create_collection("/cal/")
282
+ s.put_item("/cal/ev.ics", "data", "text/calendar")
283
+ items = s.list_items("/cal/")
284
+ items.length.should.equal 1
285
+ items[0][0].should.equal "/cal/ev.ics"
286
+ end
291
287
 
292
- it "move_item returns nil when source missing" do
293
- s = Async::Caldav::Storage::Mock.new
294
- s.move_item("/nope", "/dest").should.be.nil
295
- end
288
+ it "list_items on empty collection returns empty" do
289
+ s = Async::Caldav::Storage::Mock.new
290
+ s.list_items("/empty/").should.equal []
291
+ end
296
292
 
297
- it "get_multi returns results in input order with nils for missing" do
298
- s = Async::Caldav::Storage::Mock.new
299
- s.put_item("/cal/a.ics", "A", "text/calendar")
300
- result = s.get_multi(["/cal/a.ics", "/cal/nope.ics"])
301
- result.length.should.equal 2
302
- result[0][1][:body].should.equal "A"
303
- result[1][1].should.be.nil
304
- end
293
+ it "move_item removes source, creates destination" do
294
+ s = Async::Caldav::Storage::Mock.new
295
+ s.put_item("/cal/a.ics", "data", "text/calendar")
296
+ s.move_item("/cal/a.ics", "/cal/b.ics")
297
+ s.get_item("/cal/a.ics").should.be.nil
298
+ s.get_item("/cal/b.ics")[:body].should.equal "data"
299
+ end
305
300
 
306
- it "get_multi with empty input returns empty" do
307
- s = Async::Caldav::Storage::Mock.new
308
- s.get_multi([]).should.equal []
309
- end
301
+ it "move_item returns nil when source missing" do
302
+ s = Async::Caldav::Storage::Mock.new
303
+ s.move_item("/nope", "/dest").should.be.nil
304
+ end
310
305
 
311
- it "exists? for items, collections, and nonexistent" do
312
- s = Async::Caldav::Storage::Mock.new
313
- s.exists?("/nope").should.equal false
314
- s.create_collection("/col/")
315
- s.exists?("/col/").should.equal true
316
- s.put_item("/col/x.ics", "d", "text/calendar")
317
- s.exists?("/col/x.ics").should.equal true
318
- end
306
+ it "get_multi returns results in input order with nils for missing" do
307
+ s = Async::Caldav::Storage::Mock.new
308
+ s.put_item("/cal/a.ics", "A", "text/calendar")
309
+ result = s.get_multi(["/cal/a.ics", "/cal/nope.ics"])
310
+ result.length.should.equal 2
311
+ result[0][1][:body].should.equal "A"
312
+ result[1][1].should.be.nil
313
+ end
319
314
 
320
- it "etag returns item etag, nil for nonexistent" do
321
- s = Async::Caldav::Storage::Mock.new
322
- s.put_item("/cal/ev.ics", "body", "text/calendar")
323
- s.etag("/cal/ev.ics").should.not.be.nil
324
- s.etag("/nope").should.be.nil
325
- end
315
+ it "get_multi with empty input returns empty" do
316
+ s = Async::Caldav::Storage::Mock.new
317
+ s.get_multi([]).should.equal []
318
+ end
326
319
 
327
- it "etag changes when body changes" do
328
- s = Async::Caldav::Storage::Mock.new
329
- s.put_item("/cal/ev.ics", "body1", "text/calendar")
330
- e1 = s.etag("/cal/ev.ics")
331
- s.put_item("/cal/ev.ics", "body2", "text/calendar")
332
- e2 = s.etag("/cal/ev.ics")
333
- e1.should.not.equal e2
334
- end
320
+ it "exists? for items, collections, and nonexistent" do
321
+ s = Async::Caldav::Storage::Mock.new
322
+ s.exists?("/nope").should.equal false
323
+ s.create_collection("/col/")
324
+ s.exists?("/col/").should.equal true
325
+ s.put_item("/col/x.ics", "d", "text/calendar")
326
+ s.exists?("/col/x.ics").should.equal true
327
+ end
335
328
 
336
- it "snapshot_sync returns a token" do
337
- s = Async::Caldav::Storage::Mock.new
338
- s.create_collection("/cal/", type: :calendar)
339
- s.put_item("/cal/ev.ics", "body", "text/calendar")
340
- token = s.snapshot_sync("/cal/")
341
- token.should.not.be.nil
342
- token.should.include "http://caldav.local/sync/"
343
- end
329
+ it "etag returns item etag, nil for nonexistent" do
330
+ s = Async::Caldav::Storage::Mock.new
331
+ s.put_item("/cal/ev.ics", "body", "text/calendar")
332
+ s.etag("/cal/ev.ics").should.not.be.nil
333
+ s.etag("/nope").should.be.nil
334
+ end
344
335
 
345
- it "sync_changes returns empty changes when nothing changed" do
346
- s = Async::Caldav::Storage::Mock.new
347
- s.create_collection("/cal/", type: :calendar)
348
- s.put_item("/cal/ev.ics", "body", "text/calendar")
349
- token = s.snapshot_sync("/cal/")
350
- new_token, changes = s.sync_changes("/cal/", token)
351
- changes.should.equal []
352
- new_token.should.equal token
353
- end
336
+ it "etag changes when body changes" do
337
+ s = Async::Caldav::Storage::Mock.new
338
+ s.put_item("/cal/ev.ics", "body1", "text/calendar")
339
+ e1 = s.etag("/cal/ev.ics")
340
+ s.put_item("/cal/ev.ics", "body2", "text/calendar")
341
+ e2 = s.etag("/cal/ev.ics")
342
+ e1.should.not.equal e2
343
+ end
354
344
 
355
- it "sync_changes detects added items" do
356
- s = Async::Caldav::Storage::Mock.new
357
- s.create_collection("/cal/", type: :calendar)
358
- token = s.snapshot_sync("/cal/")
359
- s.put_item("/cal/new.ics", "body", "text/calendar")
360
- _, changes = s.sync_changes("/cal/", token)
361
- changes.length.should.equal 1
362
- changes[0][0].should.equal "/cal/new.ics"
363
- changes[0][1].should.equal :modified
364
- end
345
+ it "snapshot_sync returns a token" do
346
+ s = Async::Caldav::Storage::Mock.new
347
+ s.create_collection("/cal/", type: :calendar)
348
+ s.put_item("/cal/ev.ics", "body", "text/calendar")
349
+ token = s.snapshot_sync("/cal/")
350
+ token.should.not.be.nil
351
+ token.should.include "http://caldav.local/sync/"
352
+ end
365
353
 
366
- it "sync_changes detects deleted items" do
367
- s = Async::Caldav::Storage::Mock.new
368
- s.create_collection("/cal/", type: :calendar)
369
- s.put_item("/cal/ev.ics", "body", "text/calendar")
370
- token = s.snapshot_sync("/cal/")
371
- s.delete_item("/cal/ev.ics")
372
- _, changes = s.sync_changes("/cal/", token)
373
- changes.length.should.equal 1
374
- changes[0][0].should.equal "/cal/ev.ics"
375
- changes[0][1].should.equal :deleted
376
- end
354
+ it "sync_changes returns empty changes when nothing changed" do
355
+ s = Async::Caldav::Storage::Mock.new
356
+ s.create_collection("/cal/", type: :calendar)
357
+ s.put_item("/cal/ev.ics", "body", "text/calendar")
358
+ token = s.snapshot_sync("/cal/")
359
+ new_token, changes = s.sync_changes("/cal/", token)
360
+ changes.should.equal []
361
+ new_token.should.equal token
362
+ end
377
363
 
378
- it "sync_changes detects modified items" do
379
- s = Async::Caldav::Storage::Mock.new
380
- s.create_collection("/cal/", type: :calendar)
381
- s.put_item("/cal/ev.ics", "body1", "text/calendar")
382
- token = s.snapshot_sync("/cal/")
383
- s.put_item("/cal/ev.ics", "body2", "text/calendar")
384
- _, changes = s.sync_changes("/cal/", token)
385
- changes.length.should.equal 1
386
- changes[0][1].should.equal :modified
387
- end
364
+ it "sync_changes detects added items" do
365
+ s = Async::Caldav::Storage::Mock.new
366
+ s.create_collection("/cal/", type: :calendar)
367
+ token = s.snapshot_sync("/cal/")
368
+ s.put_item("/cal/new.ics", "body", "text/calendar")
369
+ _, changes = s.sync_changes("/cal/", token)
370
+ changes.length.should.equal 1
371
+ changes[0][0].should.equal "/cal/new.ics"
372
+ changes[0][1].should.equal :modified
373
+ end
388
374
 
389
- it "sync_changes returns nil for invalid token" do
390
- s = Async::Caldav::Storage::Mock.new
391
- s.create_collection("/cal/", type: :calendar)
392
- s.sync_changes("/cal/", "bogus-token").should.be.nil
393
- end
375
+ it "sync_changes detects deleted items" do
376
+ s = Async::Caldav::Storage::Mock.new
377
+ s.create_collection("/cal/", type: :calendar)
378
+ s.put_item("/cal/ev.ics", "body", "text/calendar")
379
+ token = s.snapshot_sync("/cal/")
380
+ s.delete_item("/cal/ev.ics")
381
+ _, changes = s.sync_changes("/cal/", token)
382
+ changes.length.should.equal 1
383
+ changes[0][0].should.equal "/cal/ev.ics"
384
+ changes[0][1].should.equal :deleted
385
+ end
394
386
 
395
- it "same body produces same etag" do
396
- s = Async::Caldav::Storage::Mock.new
397
- s.put_item("/a.ics", "same", "text/calendar")
398
- s.put_item("/b.ics", "same", "text/calendar")
399
- s.etag("/a.ics").should.equal s.etag("/b.ics")
400
- end
387
+ it "sync_changes detects modified items" do
388
+ s = Async::Caldav::Storage::Mock.new
389
+ s.create_collection("/cal/", type: :calendar)
390
+ s.put_item("/cal/ev.ics", "body1", "text/calendar")
391
+ token = s.snapshot_sync("/cal/")
392
+ s.put_item("/cal/ev.ics", "body2", "text/calendar")
393
+ _, changes = s.sync_changes("/cal/", token)
394
+ changes.length.should.equal 1
395
+ changes[0][1].should.equal :modified
401
396
  end
402
- end
397
+
398
+ it "sync_changes returns nil for invalid token" do
399
+ s = Async::Caldav::Storage::Mock.new
400
+ s.create_collection("/cal/", type: :calendar)
401
+ s.sync_changes("/cal/", "bogus-token").should.be.nil
402
+ end
403
+
404
+ it "same body produces same etag" do
405
+ s = Async::Caldav::Storage::Mock.new
406
+ s.put_item("/a.ics", "same", "text/calendar")
407
+ s.put_item("/b.ics", "same", "text/calendar")
408
+ s.etag("/a.ics").should.equal s.etag("/b.ics")
409
+ end
410
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Async
4
4
  module Caldav
5
- VERSION = "1.2.4"
5
+ VERSION = "1.3.0"
6
6
  end
7
7
  end