protocol-caldav 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/setup"
4
- require "scampi"
5
-
6
3
  require "protocol/caldav"
7
4
 
8
5
  module Protocol
@@ -14,43 +11,57 @@ module Protocol
14
11
  # --- Calendar (RFC 4791 §9.7) ---
15
12
 
16
13
  def calendar?(filter, component)
17
- return false unless component
18
- comp_filter_matches?(filter, component)
14
+ if component
15
+ comp_filter_matches?(filter, component)
16
+ else
17
+ false
18
+ end
19
19
  end
20
20
 
21
21
  # --- Addressbook (RFC 6352 §10.5) ---
22
22
 
23
23
  def addressbook?(filter, card)
24
- return false unless card
25
- return true if filter.prop_filters.empty?
26
-
27
- if filter.test == 'allof'
28
- filter.prop_filters.all? { |pf| card_prop_filter_matches?(pf, card) }
24
+ if card
25
+ if filter.prop_filters.empty?
26
+ true
27
+ else
28
+ if filter.test == 'allof'
29
+ filter.prop_filters.all? { |pf| card_prop_filter_matches?(pf, card) }
30
+ else
31
+ filter.prop_filters.any? { |pf| card_prop_filter_matches?(pf, card) }
32
+ end
33
+ end
29
34
  else
30
- filter.prop_filters.any? { |pf| card_prop_filter_matches?(pf, card) }
35
+ false
31
36
  end
32
37
  end
33
38
 
34
39
  # --- Private: Calendar matching ---
35
40
 
36
41
  def comp_filter_matches?(filter, component)
37
- return false unless component.name.casecmp?(filter.name)
38
- return false if filter.is_not_defined
39
-
40
- # Time-range check on this component
41
- return false if filter.time_range && !time_range_matches?(filter.time_range, component)
42
-
43
- # All nested prop-filters must match (AND)
44
- return false unless filter.prop_filters.all? { |pf| prop_filter_matches?(pf, component) }
45
-
46
- # All nested comp-filters must match
47
- filter.comp_filters.all? do |cf|
48
- children = component.find_components(cf.name)
49
- if cf.is_not_defined
50
- children.empty?
42
+ if component.name.casecmp?(filter.name)
43
+ if filter.is_not_defined
44
+ false
51
45
  else
52
- children.any? { |child| comp_filter_matches?(cf, child) }
46
+ if filter.time_range && !time_range_matches?(filter.time_range, component)
47
+ false
48
+ else
49
+ if filter.prop_filters.all? { |pf| prop_filter_matches?(pf, component) }
50
+ filter.comp_filters.all? do |cf|
51
+ children = component.find_components(cf.name)
52
+ if cf.is_not_defined
53
+ children.empty?
54
+ else
55
+ children.any? { |child| comp_filter_matches?(cf, child) }
56
+ end
57
+ end
58
+ else
59
+ false
60
+ end
61
+ end
53
62
  end
63
+ else
64
+ false
54
65
  end
55
66
  end
56
67
 
@@ -58,83 +69,115 @@ module Protocol
58
69
  # Handle recurring events via RRULE expansion
59
70
  rrule_prop = component.find_property('RRULE')
60
71
  if rrule_prop
61
- return rrule_time_range_matches?(tr, component, rrule_prop)
62
- end
63
-
64
- filter_start = tr.start_time ? parse_datetime_string(tr.start_time) : Time.at(0).utc
65
- filter_end = tr.end_time ? parse_datetime_string(tr.end_time) : Time.utc(9999)
66
-
67
- comp_start = parse_ical_datetime(component, 'DTSTART')
68
-
69
- # VTODO special cases per RFC 4791 §9.9
70
- if component.name.casecmp?('VTODO')
71
- due = parse_ical_datetime(component, 'DUE')
72
- completed = parse_ical_datetime(component, 'COMPLETED')
73
- created = parse_ical_datetime(component, 'CREATED')
74
-
75
- if comp_start && due
76
- return comp_start < filter_end && due > filter_start
77
- elsif comp_start
78
- return comp_start < filter_end && (comp_start + 1) > filter_start
79
- elsif due
80
- return (due - 1) < filter_end && due > filter_start
81
- elsif completed
82
- return completed >= filter_start && completed < filter_end
83
- elsif created
84
- return created >= filter_start && created < filter_end
72
+ rrule_time_range_matches?(tr, component, rrule_prop)
73
+ else
74
+ if tr.start_time
75
+ filter_start = parse_datetime_string(tr.start_time)
85
76
  else
86
- return true
77
+ filter_start = Time.at(0).utc
78
+ end
79
+ if tr.end_time
80
+ filter_end = parse_datetime_string(tr.end_time)
81
+ else
82
+ filter_end = Time.utc(9999)
83
+ end
84
+ comp_start = parse_ical_datetime(component, 'DTSTART')
85
+
86
+ if component.name.casecmp?('VTODO')
87
+ due = parse_ical_datetime(component, 'DUE')
88
+ completed = parse_ical_datetime(component, 'COMPLETED')
89
+ created = parse_ical_datetime(component, 'CREATED')
90
+
91
+ if comp_start && due
92
+ comp_start < filter_end && due > filter_start
93
+ elsif comp_start
94
+ comp_start < filter_end && (comp_start + 1) > filter_start
95
+ elsif due
96
+ (due - 1) < filter_end && due > filter_start
97
+ elsif completed
98
+ completed >= filter_start && completed < filter_end
99
+ elsif created
100
+ created >= filter_start && created < filter_end
101
+ else
102
+ true
103
+ end
104
+ elsif component.name.casecmp?('VJOURNAL')
105
+ if comp_start
106
+ comp_start >= filter_start && comp_start < filter_end
107
+ else
108
+ false
109
+ end
110
+ else
111
+ comp_end = parse_ical_datetime(component, 'DTEND') ||
112
+ parse_duration_end(component, comp_start) ||
113
+ (comp_start ? comp_start + 1 : nil)
114
+
115
+ if comp_start
116
+ comp_start < filter_end && (comp_end || comp_start + 1) > filter_start
117
+ else
118
+ false
119
+ end
87
120
  end
88
121
  end
89
-
90
- # VJOURNAL: uses DTSTART only (instantaneous)
91
- if component.name.casecmp?('VJOURNAL')
92
- return false unless comp_start
93
- return comp_start >= filter_start && comp_start < filter_end
94
- end
95
-
96
- # VEVENT
97
- comp_end = parse_ical_datetime(component, 'DTEND') ||
98
- parse_duration_end(component, comp_start) ||
99
- (comp_start ? comp_start + 1 : nil)
100
-
101
- return false unless comp_start
102
-
103
- # Half-open overlap: [comp_start, comp_end) overlaps [filter_start, filter_end)
104
- comp_start < filter_end && (comp_end || comp_start + 1) > filter_start
105
122
  end
106
123
 
107
124
  def parse_ical_datetime(component, prop_name)
108
125
  prop = component.find_property(prop_name)
109
- return nil unless prop
110
- parse_datetime_string(prop.value.strip)
126
+ if prop
127
+ parse_datetime_string(prop.value.strip)
128
+ else
129
+ nil
130
+ end
111
131
  end
112
132
 
113
133
  def parse_duration_end(component, start_time)
114
- return nil unless start_time
115
- dur = component.find_property('DURATION')
116
- return nil unless dur
117
- val = dur.value.strip
118
- seconds = 0
119
- if m = val.match(/P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?/)
120
- seconds += (m[1]&.to_i || 0) * 86400
121
- seconds += (m[2]&.to_i || 0) * 3600
122
- seconds += (m[3]&.to_i || 0) * 60
123
- seconds += (m[4]&.to_i || 0)
134
+ if start_time
135
+ dur = component.find_property('DURATION')
136
+ if dur
137
+ val = dur.value.strip
138
+ seconds = 0
139
+ if m = val.match(/P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?/)
140
+ seconds += (m[1]&.to_i || 0) * 86400
141
+ seconds += (m[2]&.to_i || 0) * 3600
142
+ seconds += (m[3]&.to_i || 0) * 60
143
+ seconds += (m[4]&.to_i || 0)
144
+ end
145
+ seconds > 0 ? start_time + seconds : nil
146
+ else
147
+ nil
148
+ end
149
+ else
150
+ nil
124
151
  end
125
- seconds > 0 ? start_time + seconds : nil
126
152
  end
127
153
 
128
154
  def parse_datetime_string(str)
129
- return nil unless str && !str.empty?
130
- str = str.strip
131
- if str.length == 8 # DATE format: YYYYMMDD
132
- Time.utc(str[0..3].to_i, str[4..5].to_i, str[6..7].to_i)
133
- elsif str.end_with?('Z') # UTC datetime
134
- s = str.chomp('Z')
135
- Time.utc(s[0..3].to_i, s[4..5].to_i, s[6..7].to_i, s[9..10].to_i, s[11..12].to_i, s[13..14].to_i)
136
- else # Floating datetime -- treat as UTC per RFC 4791 §9.9
137
- Time.utc(str[0..3].to_i, str[4..5].to_i, str[6..7].to_i, str[9..10].to_i, str[11..12].to_i, str[13..14].to_i)
155
+ if str && !str.empty?
156
+ str = str.strip
157
+ if str.length == 8 # DATE format: YYYYMMDD
158
+ Time.utc(str[0..3].to_i, str[4..5].to_i, str[6..7].to_i)
159
+ elsif str.end_with?('Z') # UTC datetime
160
+ s = str.chomp('Z')
161
+ Time.utc(
162
+ s[0..3].to_i,
163
+ s[4..5].to_i,
164
+ s[6..7].to_i,
165
+ s[9..10].to_i,
166
+ s[11..12].to_i,
167
+ s[13..14].to_i,
168
+ )
169
+ else # Floating datetime -- treat as UTC per RFC 4791 §9.9
170
+ Time.utc(
171
+ str[0..3].to_i,
172
+ str[4..5].to_i,
173
+ str[6..7].to_i,
174
+ str[9..10].to_i,
175
+ str[11..12].to_i,
176
+ str[13..14].to_i,
177
+ )
178
+ end
179
+ else
180
+ nil
138
181
  end
139
182
  rescue ArgumentError
140
183
  nil
@@ -144,14 +187,18 @@ module Protocol
144
187
  properties = component.find_all_properties(filter.name)
145
188
 
146
189
  if filter.is_not_defined
147
- return properties.empty?
148
- end
149
-
150
- return false if properties.empty?
151
-
152
- properties.any? do |prop|
153
- next false if filter.text_match && !text_match_matches?(filter.text_match, prop.value)
154
- filter.param_filters.all? { |pf| param_filter_matches?(pf, prop) }
190
+ properties.empty?
191
+ else
192
+ if properties.empty?
193
+ false
194
+ else
195
+ properties.any? do |prop|
196
+ if filter.text_match && !text_match_matches?(filter.text_match, prop.value)
197
+ next false
198
+ end
199
+ filter.param_filters.all? { |pf| param_filter_matches?(pf, prop) }
200
+ end
201
+ end
155
202
  end
156
203
  end
157
204
 
@@ -159,24 +206,30 @@ module Protocol
159
206
  param_value = property.param(filter.name)
160
207
 
161
208
  if filter.is_not_defined
162
- return param_value.nil?
163
- end
164
-
165
- return false if param_value.nil?
166
-
167
- if filter.text_match
168
- text_match_matches?(filter.text_match, param_value)
209
+ param_value.nil?
169
210
  else
170
- true
211
+ if param_value.nil?
212
+ false
213
+ else
214
+ if filter.text_match
215
+ text_match_matches?(filter.text_match, param_value)
216
+ else
217
+ true
218
+ end
219
+ end
171
220
  end
172
221
  end
173
222
 
174
223
  def text_match_matches?(matcher, value)
175
- result = case matcher.match_type
176
- when 'equals' then collate_equal?(matcher.collation, value, matcher.value)
177
- when 'starts-with' then collate_starts?(matcher.collation, value, matcher.value)
178
- when 'ends-with' then collate_ends?(matcher.collation, value, matcher.value)
179
- else collate_contains?(matcher.collation, value, matcher.value)
224
+ case matcher.match_type
225
+ when 'equals'
226
+ result = collate_equal?(matcher.collation, value, matcher.value)
227
+ when 'starts-with'
228
+ result = collate_starts?(matcher.collation, value, matcher.value)
229
+ when 'ends-with'
230
+ result = collate_ends?(matcher.collation, value, matcher.value)
231
+ else
232
+ result = collate_contains?(matcher.collation, value, matcher.value)
180
233
  end
181
234
  matcher.negate_condition ? !result : result
182
235
  end
@@ -219,49 +272,65 @@ module Protocol
219
272
  properties = card.find_all_properties(filter.name)
220
273
 
221
274
  if filter.is_not_defined
222
- return properties.empty?
223
- end
224
-
225
- return false if properties.empty?
226
-
227
- properties.any? do |prop|
228
- next false if filter.text_match && !text_match_matches?(filter.text_match, prop.value)
229
- filter.param_filters.all? { |pf| param_filter_matches?(pf, prop) }
275
+ properties.empty?
276
+ else
277
+ if properties.empty?
278
+ false
279
+ else
280
+ properties.any? do |prop|
281
+ if filter.text_match && !text_match_matches?(filter.text_match, prop.value)
282
+ next false
283
+ end
284
+ filter.param_filters.all? { |pf| param_filter_matches?(pf, prop) }
285
+ end
286
+ end
230
287
  end
231
288
  end
232
289
 
233
290
  def rrule_time_range_matches?(tr, component, rrule_prop)
234
291
  dtstart = parse_ical_datetime(component, 'DTSTART')
235
- return false unless dtstart
236
-
237
- filter_start = tr.start_time ? parse_datetime_string(tr.start_time) : Time.at(0).utc
238
- filter_end = tr.end_time ? parse_datetime_string(tr.end_time) : Time.utc(9999)
292
+ if dtstart
293
+ if tr.start_time
294
+ filter_start = parse_datetime_string(tr.start_time)
295
+ else
296
+ filter_start = Time.at(0).utc
297
+ end
298
+ if tr.end_time
299
+ filter_end = parse_datetime_string(tr.end_time)
300
+ else
301
+ filter_end = Time.utc(9999)
302
+ end
303
+ exdates = component.find_all_properties('EXDATE').filter_map do |ex|
304
+ parse_datetime_string(ex.value.strip)
305
+ end
306
+ dtend = parse_ical_datetime(component, 'DTEND')
307
+ duration = nil
239
308
 
240
- exdates = component.find_all_properties('EXDATE').filter_map do |ex|
241
- parse_datetime_string(ex.value.strip)
242
- end
309
+ if dtend
310
+ duration = dtend - dtstart
311
+ else
312
+ dur_prop = component.find_property('DURATION')
313
+ duration = 1
243
314
 
244
- dtend = parse_ical_datetime(component, 'DTEND')
245
- duration = if dtend
246
- dtend - dtstart
315
+ if dur_prop
316
+ duration = parse_duration_seconds(dur_prop.value.strip)
317
+ end
318
+ end
319
+ adjusted_start = filter_start - [duration, 0].max
320
+ occurrences = Ical::Rrule.expand(
321
+ dtstart: dtstart,
322
+ rrule_value: rrule_prop.value.strip,
323
+ range_start: adjusted_start,
324
+ range_end: filter_end,
325
+ exdates: exdates,
326
+ max_count: 10000,
327
+ )
328
+ occurrences.any? do |occ_start|
329
+ occ_end = occ_start + duration
330
+ occ_start < filter_end && occ_end > filter_start
331
+ end
247
332
  else
248
- dur_prop = component.find_property('DURATION')
249
- dur_prop ? parse_duration_seconds(dur_prop.value.strip) : 1
250
- end
251
-
252
- adjusted_start = filter_start - [duration, 0].max
253
- occurrences = Ical::Rrule.expand(
254
- dtstart: dtstart,
255
- rrule_value: rrule_prop.value.strip,
256
- range_start: adjusted_start,
257
- range_end: filter_end,
258
- exdates: exdates,
259
- max_count: 10000
260
- )
261
-
262
- occurrences.any? do |occ_start|
263
- occ_end = occ_start + duration
264
- occ_start < filter_end && occ_end > filter_start
333
+ false
265
334
  end
266
335
  end
267
336
 
@@ -277,500 +346,507 @@ module Protocol
277
346
  seconds > 0 ? seconds : 1
278
347
  end
279
348
 
280
- private_class_method :comp_filter_matches?, :prop_filter_matches?,
281
- :param_filter_matches?, :text_match_matches?,
282
- :collate_contains?, :collate_equal?,
283
- :collate_starts?, :collate_ends?,
284
- :card_prop_filter_matches?,
285
- :time_range_matches?, :parse_ical_datetime,
286
- :parse_duration_end, :parse_datetime_string,
287
- :rrule_time_range_matches?, :parse_duration_seconds
349
+ private_class_method :comp_filter_matches?,
350
+ :prop_filter_matches?,
351
+ :param_filter_matches?,
352
+ :text_match_matches?,
353
+ :collate_contains?,
354
+ :collate_equal?,
355
+ :collate_starts?,
356
+ :collate_ends?,
357
+ :card_prop_filter_matches?,
358
+ :time_range_matches?,
359
+ :parse_ical_datetime,
360
+ :parse_duration_end,
361
+ :parse_datetime_string,
362
+ :rrule_time_range_matches?,
363
+ :parse_duration_seconds
288
364
  end
289
365
  end
290
366
  end
291
367
  end
292
368
 
293
- test do
294
- def parse_ical(text)
295
- Protocol::Caldav::Ical::Parser.parse(text)
296
- end
369
+ __END__
297
370
 
298
- def parse_vcard(text)
299
- Protocol::Caldav::Vcard::Parser.parse(text)
300
- end
371
+ def parse_ical(text)
372
+ Protocol::Caldav::Ical::Parser.parse(text)
373
+ end
301
374
 
302
- def comp_filter(name, **opts)
303
- Protocol::Caldav::Filter::Calendar::CompFilter.new(name: name, **opts)
304
- end
375
+ def parse_vcard(text)
376
+ Protocol::Caldav::Vcard::Parser.parse(text)
377
+ end
305
378
 
306
- def prop_filter(name, **opts)
307
- Protocol::Caldav::Filter::Calendar::PropFilter.new(name: name, **opts)
308
- end
379
+ def comp_filter(name, **opts)
380
+ Protocol::Caldav::Filter::Calendar::CompFilter.new(name: name, **opts)
381
+ end
309
382
 
310
- def text_match(value, **opts)
311
- Protocol::Caldav::Filter::Calendar::TextMatch.new(value: value, **opts)
312
- end
383
+ def prop_filter(name, **opts)
384
+ Protocol::Caldav::Filter::Calendar::PropFilter.new(name: name, **opts)
385
+ end
313
386
 
314
- def ab_filter(**opts)
315
- Protocol::Caldav::Filter::Addressbook::Filter.new(**opts)
316
- end
387
+ def text_match(value, **opts)
388
+ Protocol::Caldav::Filter::Calendar::TextMatch.new(value: value, **opts)
389
+ end
317
390
 
318
- def ab_prop_filter(name, **opts)
319
- Protocol::Caldav::Filter::Addressbook::PropFilter.new(name: name, **opts)
320
- end
391
+ def ab_filter(**opts)
392
+ Protocol::Caldav::Filter::Addressbook::Filter.new(**opts)
393
+ end
321
394
 
322
- def ab_text_match(value, **opts)
323
- Protocol::Caldav::Filter::Addressbook::TextMatch.new(value: value, **opts)
324
- end
395
+ def ab_prop_filter(name, **opts)
396
+ Protocol::Caldav::Filter::Addressbook::PropFilter.new(name: name, **opts)
397
+ end
325
398
 
326
- describe "Protocol::Caldav::Filter::Match" do
327
- describe "CompFilter against component" do
328
- it "matches when component name equals filter name" do
329
- vcal = parse_ical("BEGIN:VCALENDAR\r\nEND:VCALENDAR")
330
- Protocol::Caldav::Filter::Match.calendar?(comp_filter("VCALENDAR"), vcal).should.equal true
331
- end
399
+ def ab_text_match(value, **opts)
400
+ Protocol::Caldav::Filter::Addressbook::TextMatch.new(value: value, **opts)
401
+ end
332
402
 
333
- it "does not match when names differ" do
334
- vcal = parse_ical("BEGIN:VCALENDAR\r\nEND:VCALENDAR")
335
- Protocol::Caldav::Filter::Match.calendar?(comp_filter("VEVENT"), vcal).should.equal false
336
- end
403
+ describe "Protocol::Caldav::Filter::Match" do
404
+ describe "CompFilter against component" do
405
+ it "matches when component name equals filter name" do
406
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nEND:VCALENDAR")
407
+ Protocol::Caldav::Filter::Match.calendar?(comp_filter("VCALENDAR"), vcal).should.equal true
408
+ end
337
409
 
338
- it "with is_not_defined: does not match when component present" do
339
- vcal = parse_ical("BEGIN:VCALENDAR\r\nEND:VCALENDAR")
340
- Protocol::Caldav::Filter::Match.calendar?(comp_filter("VCALENDAR", is_not_defined: true), vcal).should.equal false
341
- end
410
+ it "does not match when names differ" do
411
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nEND:VCALENDAR")
412
+ Protocol::Caldav::Filter::Match.calendar?(comp_filter("VEVENT"), vcal).should.equal false
413
+ end
342
414
 
343
- it "all nested prop-filters must match (AND semantics)" do
344
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR")
345
- f = comp_filter("VCALENDAR", comp_filters: [
346
- comp_filter("VEVENT", prop_filters: [
347
- prop_filter("SUMMARY"),
348
- prop_filter("DESCRIPTION") # absent -> fails
349
- ])
415
+ it "with is_not_defined: does not match when component present" do
416
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nEND:VCALENDAR")
417
+ Protocol::Caldav::Filter::Match.calendar?(comp_filter("VCALENDAR", is_not_defined: true), vcal).should.equal false
418
+ end
419
+
420
+ it "all nested prop-filters must match (AND semantics)" do
421
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR")
422
+ f = comp_filter("VCALENDAR", comp_filters: [
423
+ comp_filter("VEVENT", prop_filters: [
424
+ prop_filter("SUMMARY"),
425
+ prop_filter("DESCRIPTION") # absent -> fails
350
426
  ])
351
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
352
- end
427
+ ])
428
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
429
+ end
353
430
 
354
- it "empty filter matches any component of that name" do
355
- vcal = parse_ical("BEGIN:VCALENDAR\r\nEND:VCALENDAR")
356
- Protocol::Caldav::Filter::Match.calendar?(comp_filter("VCALENDAR"), vcal).should.equal true
357
- end
431
+ it "empty filter matches any component of that name" do
432
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nEND:VCALENDAR")
433
+ Protocol::Caldav::Filter::Match.calendar?(comp_filter("VCALENDAR"), vcal).should.equal true
434
+ end
358
435
 
359
- it "is_not_defined on nested comp-filter: matches when absent" do
360
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nEND:VEVENT\r\nEND:VCALENDAR")
361
- f = comp_filter("VCALENDAR", comp_filters: [
362
- comp_filter("VTODO", is_not_defined: true)
363
- ])
364
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
365
- end
436
+ it "is_not_defined on nested comp-filter: matches when absent" do
437
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nEND:VEVENT\r\nEND:VCALENDAR")
438
+ f = comp_filter("VCALENDAR", comp_filters: [
439
+ comp_filter("VTODO", is_not_defined: true)
440
+ ])
441
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
442
+ end
366
443
 
367
- it "is_not_defined on nested comp-filter: does not match when present" do
368
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nEND:VEVENT\r\nEND:VCALENDAR")
369
- f = comp_filter("VCALENDAR", comp_filters: [
370
- comp_filter("VEVENT", is_not_defined: true)
371
- ])
372
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
373
- end
444
+ it "is_not_defined on nested comp-filter: does not match when present" do
445
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nEND:VEVENT\r\nEND:VCALENDAR")
446
+ f = comp_filter("VCALENDAR", comp_filters: [
447
+ comp_filter("VEVENT", is_not_defined: true)
448
+ ])
449
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
374
450
  end
451
+ end
375
452
 
376
- describe "PropFilter" do
377
- it "matches when property exists (defined-only check)" do
378
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Test\r\nEND:VEVENT\r\nEND:VCALENDAR")
379
- f = comp_filter("VCALENDAR", comp_filters: [
380
- comp_filter("VEVENT", prop_filters: [prop_filter("SUMMARY")])
381
- ])
382
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
383
- end
453
+ describe "PropFilter" do
454
+ it "matches when property exists (defined-only check)" do
455
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Test\r\nEND:VEVENT\r\nEND:VCALENDAR")
456
+ f = comp_filter("VCALENDAR", comp_filters: [
457
+ comp_filter("VEVENT", prop_filters: [prop_filter("SUMMARY")])
458
+ ])
459
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
460
+ end
384
461
 
385
- it "does not match when property absent" do
386
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nEND:VEVENT\r\nEND:VCALENDAR")
387
- f = comp_filter("VCALENDAR", comp_filters: [
388
- comp_filter("VEVENT", prop_filters: [prop_filter("SUMMARY")])
389
- ])
390
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
391
- end
462
+ it "does not match when property absent" do
463
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nEND:VEVENT\r\nEND:VCALENDAR")
464
+ f = comp_filter("VCALENDAR", comp_filters: [
465
+ comp_filter("VEVENT", prop_filters: [prop_filter("SUMMARY")])
466
+ ])
467
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
468
+ end
392
469
 
393
- it "with is_not_defined: matches when property absent" do
394
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nEND:VEVENT\r\nEND:VCALENDAR")
395
- f = comp_filter("VCALENDAR", comp_filters: [
396
- comp_filter("VEVENT", prop_filters: [prop_filter("STATUS", is_not_defined: true)])
397
- ])
398
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
399
- end
470
+ it "with is_not_defined: matches when property absent" do
471
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nEND:VEVENT\r\nEND:VCALENDAR")
472
+ f = comp_filter("VCALENDAR", comp_filters: [
473
+ comp_filter("VEVENT", prop_filters: [prop_filter("STATUS", is_not_defined: true)])
474
+ ])
475
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
476
+ end
400
477
 
401
- it "with is_not_defined: does not match when property present" do
402
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSTATUS:CONFIRMED\r\nEND:VEVENT\r\nEND:VCALENDAR")
403
- f = comp_filter("VCALENDAR", comp_filters: [
404
- comp_filter("VEVENT", prop_filters: [prop_filter("STATUS", is_not_defined: true)])
405
- ])
406
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
407
- end
478
+ it "with is_not_defined: does not match when property present" do
479
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSTATUS:CONFIRMED\r\nEND:VEVENT\r\nEND:VCALENDAR")
480
+ f = comp_filter("VCALENDAR", comp_filters: [
481
+ comp_filter("VEVENT", prop_filters: [prop_filter("STATUS", is_not_defined: true)])
482
+ ])
483
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
484
+ end
408
485
 
409
- it "multi-value: matches if any instance satisfies" do
410
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nATTENDEE:alice\r\nATTENDEE:bob\r\nEND:VEVENT\r\nEND:VCALENDAR")
411
- f = comp_filter("VCALENDAR", comp_filters: [
412
- comp_filter("VEVENT", prop_filters: [
413
- prop_filter("ATTENDEE", text_match: text_match("bob"))
414
- ])
486
+ it "multi-value: matches if any instance satisfies" do
487
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nATTENDEE:alice\r\nATTENDEE:bob\r\nEND:VEVENT\r\nEND:VCALENDAR")
488
+ f = comp_filter("VCALENDAR", comp_filters: [
489
+ comp_filter("VEVENT", prop_filters: [
490
+ prop_filter("ATTENDEE", text_match: text_match("bob"))
415
491
  ])
416
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
417
- end
492
+ ])
493
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
418
494
  end
495
+ end
419
496
 
420
- describe "TextMatch" do
421
- it "contains (default) matches substring" do
422
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Team Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR")
423
- f = comp_filter("VCALENDAR", comp_filters: [
424
- comp_filter("VEVENT", prop_filters: [
425
- prop_filter("SUMMARY", text_match: text_match("Meeting"))
426
- ])
497
+ describe "TextMatch" do
498
+ it "contains (default) matches substring" do
499
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Team Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR")
500
+ f = comp_filter("VCALENDAR", comp_filters: [
501
+ comp_filter("VEVENT", prop_filters: [
502
+ prop_filter("SUMMARY", text_match: text_match("Meeting"))
427
503
  ])
428
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
429
- end
504
+ ])
505
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
506
+ end
430
507
 
431
- it "equals matches whole-string equality" do
432
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR")
433
- f = comp_filter("VCALENDAR", comp_filters: [
434
- comp_filter("VEVENT", prop_filters: [
435
- prop_filter("SUMMARY", text_match: text_match("Meeting", match_type: "equals"))
436
- ])
508
+ it "equals matches whole-string equality" do
509
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR")
510
+ f = comp_filter("VCALENDAR", comp_filters: [
511
+ comp_filter("VEVENT", prop_filters: [
512
+ prop_filter("SUMMARY", text_match: text_match("Meeting", match_type: "equals"))
437
513
  ])
438
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
439
- end
514
+ ])
515
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
516
+ end
440
517
 
441
- it "equals rejects partial match" do
442
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Team Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR")
443
- f = comp_filter("VCALENDAR", comp_filters: [
444
- comp_filter("VEVENT", prop_filters: [
445
- prop_filter("SUMMARY", text_match: text_match("Meeting", match_type: "equals"))
446
- ])
518
+ it "equals rejects partial match" do
519
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Team Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR")
520
+ f = comp_filter("VCALENDAR", comp_filters: [
521
+ comp_filter("VEVENT", prop_filters: [
522
+ prop_filter("SUMMARY", text_match: text_match("Meeting", match_type: "equals"))
447
523
  ])
448
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
449
- end
524
+ ])
525
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
526
+ end
450
527
 
451
- it "starts-with matches prefix" do
452
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Team Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR")
453
- f = comp_filter("VCALENDAR", comp_filters: [
454
- comp_filter("VEVENT", prop_filters: [
455
- prop_filter("SUMMARY", text_match: text_match("Team", match_type: "starts-with"))
456
- ])
528
+ it "starts-with matches prefix" do
529
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Team Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR")
530
+ f = comp_filter("VCALENDAR", comp_filters: [
531
+ comp_filter("VEVENT", prop_filters: [
532
+ prop_filter("SUMMARY", text_match: text_match("Team", match_type: "starts-with"))
457
533
  ])
458
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
459
- end
534
+ ])
535
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
536
+ end
460
537
 
461
- it "ends-with matches suffix" do
462
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Team Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR")
463
- f = comp_filter("VCALENDAR", comp_filters: [
464
- comp_filter("VEVENT", prop_filters: [
465
- prop_filter("SUMMARY", text_match: text_match("Meeting", match_type: "ends-with"))
466
- ])
538
+ it "ends-with matches suffix" do
539
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Team Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR")
540
+ f = comp_filter("VCALENDAR", comp_filters: [
541
+ comp_filter("VEVENT", prop_filters: [
542
+ prop_filter("SUMMARY", text_match: text_match("Meeting", match_type: "ends-with"))
467
543
  ])
468
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
469
- end
544
+ ])
545
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
546
+ end
470
547
 
471
- it "i;ascii-casemap (default) is case-insensitive" do
472
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:MEETING\r\nEND:VEVENT\r\nEND:VCALENDAR")
473
- f = comp_filter("VCALENDAR", comp_filters: [
474
- comp_filter("VEVENT", prop_filters: [
475
- prop_filter("SUMMARY", text_match: text_match("meeting"))
476
- ])
548
+ it "i;ascii-casemap (default) is case-insensitive" do
549
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:MEETING\r\nEND:VEVENT\r\nEND:VCALENDAR")
550
+ f = comp_filter("VCALENDAR", comp_filters: [
551
+ comp_filter("VEVENT", prop_filters: [
552
+ prop_filter("SUMMARY", text_match: text_match("meeting"))
477
553
  ])
478
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
479
- end
554
+ ])
555
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
556
+ end
480
557
 
481
- it "i;octet is case-sensitive" do
482
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:MEETING\r\nEND:VEVENT\r\nEND:VCALENDAR")
483
- f = comp_filter("VCALENDAR", comp_filters: [
484
- comp_filter("VEVENT", prop_filters: [
485
- prop_filter("SUMMARY", text_match: text_match("meeting", collation: "i;octet"))
486
- ])
558
+ it "i;octet is case-sensitive" do
559
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:MEETING\r\nEND:VEVENT\r\nEND:VCALENDAR")
560
+ f = comp_filter("VCALENDAR", comp_filters: [
561
+ comp_filter("VEVENT", prop_filters: [
562
+ prop_filter("SUMMARY", text_match: text_match("meeting", collation: "i;octet"))
487
563
  ])
488
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
489
- end
564
+ ])
565
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
566
+ end
490
567
 
491
- it "negate-condition inverts the result" do
492
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR")
493
- f = comp_filter("VCALENDAR", comp_filters: [
494
- comp_filter("VEVENT", prop_filters: [
495
- prop_filter("SUMMARY", text_match: text_match("Meeting", negate_condition: true))
496
- ])
568
+ it "negate-condition inverts the result" do
569
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Meeting\r\nEND:VEVENT\r\nEND:VCALENDAR")
570
+ f = comp_filter("VCALENDAR", comp_filters: [
571
+ comp_filter("VEVENT", prop_filters: [
572
+ prop_filter("SUMMARY", text_match: text_match("Meeting", negate_condition: true))
497
573
  ])
498
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
499
- end
574
+ ])
575
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
576
+ end
500
577
 
501
- it "negate-condition on non-match produces true" do
502
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Lunch\r\nEND:VEVENT\r\nEND:VCALENDAR")
503
- f = comp_filter("VCALENDAR", comp_filters: [
504
- comp_filter("VEVENT", prop_filters: [
505
- prop_filter("SUMMARY", text_match: text_match("Meeting", negate_condition: true))
506
- ])
578
+ it "negate-condition on non-match produces true" do
579
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Lunch\r\nEND:VEVENT\r\nEND:VCALENDAR")
580
+ f = comp_filter("VCALENDAR", comp_filters: [
581
+ comp_filter("VEVENT", prop_filters: [
582
+ prop_filter("SUMMARY", text_match: text_match("Meeting", negate_condition: true))
507
583
  ])
508
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
509
- end
584
+ ])
585
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
510
586
  end
587
+ end
511
588
 
512
- describe "text-match on SUMMARY only matches SUMMARY property" do
513
- it "does not match when text appears in DESCRIPTION but not SUMMARY" do
514
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Lunch\r\nDESCRIPTION:Alice is coming\r\nEND:VEVENT\r\nEND:VCALENDAR")
515
- f = comp_filter("VCALENDAR", comp_filters: [
516
- comp_filter("VEVENT", prop_filters: [
517
- prop_filter("SUMMARY", text_match: text_match("Alice"))
518
- ])
589
+ describe "text-match on SUMMARY only matches SUMMARY property" do
590
+ it "does not match when text appears in DESCRIPTION but not SUMMARY" do
591
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Lunch\r\nDESCRIPTION:Alice is coming\r\nEND:VEVENT\r\nEND:VCALENDAR")
592
+ f = comp_filter("VCALENDAR", comp_filters: [
593
+ comp_filter("VEVENT", prop_filters: [
594
+ prop_filter("SUMMARY", text_match: text_match("Alice"))
519
595
  ])
520
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
521
- end
596
+ ])
597
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
522
598
  end
599
+ end
523
600
 
524
- describe "Addressbook filter" do
525
- it "anyof returns true if any prop-filter matches" do
526
- card = parse_vcard("BEGIN:VCARD\r\nFN:John\r\nEMAIL:john@x.com\r\nEND:VCARD")
527
- f = ab_filter(test: "anyof", prop_filters: [
528
- ab_prop_filter("FN", text_match: ab_text_match("Jane")),
529
- ab_prop_filter("EMAIL", text_match: ab_text_match("john"))
530
- ])
531
- Protocol::Caldav::Filter::Match.addressbook?(f, card).should.equal true
532
- end
601
+ describe "Addressbook filter" do
602
+ it "anyof returns true if any prop-filter matches" do
603
+ card = parse_vcard("BEGIN:VCARD\r\nFN:John\r\nEMAIL:john@x.com\r\nEND:VCARD")
604
+ f = ab_filter(test: "anyof", prop_filters: [
605
+ ab_prop_filter("FN", text_match: ab_text_match("Jane")),
606
+ ab_prop_filter("EMAIL", text_match: ab_text_match("john"))
607
+ ])
608
+ Protocol::Caldav::Filter::Match.addressbook?(f, card).should.equal true
609
+ end
533
610
 
534
- it "allof returns true only if all prop-filters match" do
535
- card = parse_vcard("BEGIN:VCARD\r\nFN:John\r\nEMAIL:john@x.com\r\nEND:VCARD")
536
- f = ab_filter(test: "allof", prop_filters: [
537
- ab_prop_filter("FN", text_match: ab_text_match("John")),
538
- ab_prop_filter("EMAIL", text_match: ab_text_match("jane"))
539
- ])
540
- Protocol::Caldav::Filter::Match.addressbook?(f, card).should.equal false
541
- end
611
+ it "allof returns true only if all prop-filters match" do
612
+ card = parse_vcard("BEGIN:VCARD\r\nFN:John\r\nEMAIL:john@x.com\r\nEND:VCARD")
613
+ f = ab_filter(test: "allof", prop_filters: [
614
+ ab_prop_filter("FN", text_match: ab_text_match("John")),
615
+ ab_prop_filter("EMAIL", text_match: ab_text_match("jane"))
616
+ ])
617
+ Protocol::Caldav::Filter::Match.addressbook?(f, card).should.equal false
618
+ end
542
619
 
543
- it "allof returns true when all match" do
544
- card = parse_vcard("BEGIN:VCARD\r\nFN:John\r\nEMAIL:john@x.com\r\nEND:VCARD")
545
- f = ab_filter(test: "allof", prop_filters: [
546
- ab_prop_filter("FN", text_match: ab_text_match("John")),
547
- ab_prop_filter("EMAIL", text_match: ab_text_match("john"))
548
- ])
549
- Protocol::Caldav::Filter::Match.addressbook?(f, card).should.equal true
550
- end
620
+ it "allof returns true when all match" do
621
+ card = parse_vcard("BEGIN:VCARD\r\nFN:John\r\nEMAIL:john@x.com\r\nEND:VCARD")
622
+ f = ab_filter(test: "allof", prop_filters: [
623
+ ab_prop_filter("FN", text_match: ab_text_match("John")),
624
+ ab_prop_filter("EMAIL", text_match: ab_text_match("john"))
625
+ ])
626
+ Protocol::Caldav::Filter::Match.addressbook?(f, card).should.equal true
627
+ end
551
628
 
552
- it "empty filter list returns true" do
553
- card = parse_vcard("BEGIN:VCARD\r\nFN:John\r\nEND:VCARD")
554
- f = ab_filter(prop_filters: [])
555
- Protocol::Caldav::Filter::Match.addressbook?(f, card).should.equal true
556
- end
629
+ it "empty filter list returns true" do
630
+ card = parse_vcard("BEGIN:VCARD\r\nFN:John\r\nEND:VCARD")
631
+ f = ab_filter(prop_filters: [])
632
+ Protocol::Caldav::Filter::Match.addressbook?(f, card).should.equal true
557
633
  end
634
+ end
558
635
 
559
- describe "ParamFilter" do
560
- it "matches when parameter exists (defined-only check)" do
561
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nATTENDEE;PARTSTAT=ACCEPTED:mailto:alice@x.com\r\nEND:VEVENT\r\nEND:VCALENDAR")
562
- pf = Protocol::Caldav::Filter::Calendar::ParamFilter.new(name: "PARTSTAT")
563
- f = comp_filter("VCALENDAR", comp_filters: [
564
- comp_filter("VEVENT", prop_filters: [
565
- prop_filter("ATTENDEE", param_filters: [pf])
566
- ])
636
+ describe "ParamFilter" do
637
+ it "matches when parameter exists (defined-only check)" do
638
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nATTENDEE;PARTSTAT=ACCEPTED:mailto:alice@x.com\r\nEND:VEVENT\r\nEND:VCALENDAR")
639
+ pf = Protocol::Caldav::Filter::Calendar::ParamFilter.new(name: "PARTSTAT")
640
+ f = comp_filter("VCALENDAR", comp_filters: [
641
+ comp_filter("VEVENT", prop_filters: [
642
+ prop_filter("ATTENDEE", param_filters: [pf])
567
643
  ])
568
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
569
- end
644
+ ])
645
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
646
+ end
570
647
 
571
- it "does not match when parameter absent" do
572
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nATTENDEE:mailto:alice@x.com\r\nEND:VEVENT\r\nEND:VCALENDAR")
573
- pf = Protocol::Caldav::Filter::Calendar::ParamFilter.new(name: "PARTSTAT")
574
- f = comp_filter("VCALENDAR", comp_filters: [
575
- comp_filter("VEVENT", prop_filters: [
576
- prop_filter("ATTENDEE", param_filters: [pf])
577
- ])
648
+ it "does not match when parameter absent" do
649
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nATTENDEE:mailto:alice@x.com\r\nEND:VEVENT\r\nEND:VCALENDAR")
650
+ pf = Protocol::Caldav::Filter::Calendar::ParamFilter.new(name: "PARTSTAT")
651
+ f = comp_filter("VCALENDAR", comp_filters: [
652
+ comp_filter("VEVENT", prop_filters: [
653
+ prop_filter("ATTENDEE", param_filters: [pf])
578
654
  ])
579
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
580
- end
655
+ ])
656
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
657
+ end
581
658
 
582
- it "with is_not_defined: matches when parameter absent" do
583
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nATTENDEE:mailto:alice@x.com\r\nEND:VEVENT\r\nEND:VCALENDAR")
584
- pf = Protocol::Caldav::Filter::Calendar::ParamFilter.new(name: "PARTSTAT", is_not_defined: true)
585
- f = comp_filter("VCALENDAR", comp_filters: [
586
- comp_filter("VEVENT", prop_filters: [
587
- prop_filter("ATTENDEE", param_filters: [pf])
588
- ])
659
+ it "with is_not_defined: matches when parameter absent" do
660
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nATTENDEE:mailto:alice@x.com\r\nEND:VEVENT\r\nEND:VCALENDAR")
661
+ pf = Protocol::Caldav::Filter::Calendar::ParamFilter.new(name: "PARTSTAT", is_not_defined: true)
662
+ f = comp_filter("VCALENDAR", comp_filters: [
663
+ comp_filter("VEVENT", prop_filters: [
664
+ prop_filter("ATTENDEE", param_filters: [pf])
589
665
  ])
590
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
591
- end
666
+ ])
667
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
668
+ end
592
669
 
593
- it "with text-match: matches when parameter value matches" do
594
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nATTENDEE;PARTSTAT=ACCEPTED:mailto:alice@x.com\r\nEND:VEVENT\r\nEND:VCALENDAR")
595
- pf = Protocol::Caldav::Filter::Calendar::ParamFilter.new(
596
- name: "PARTSTAT",
597
- text_match: text_match("ACCEPTED", match_type: "equals")
598
- )
599
- f = comp_filter("VCALENDAR", comp_filters: [
600
- comp_filter("VEVENT", prop_filters: [
601
- prop_filter("ATTENDEE", param_filters: [pf])
602
- ])
603
- ])
604
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
605
- end
670
+ it "with text-match: matches when parameter value matches" do
671
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nATTENDEE;PARTSTAT=ACCEPTED:mailto:alice@x.com\r\nEND:VEVENT\r\nEND:VCALENDAR")
672
+ pf = Protocol::Caldav::Filter::Calendar::ParamFilter.new(
673
+ name: "PARTSTAT",
674
+ text_match: text_match("ACCEPTED", match_type: "equals")
675
+ )
676
+ f = comp_filter("VCALENDAR", comp_filters: [
677
+ comp_filter("VEVENT", prop_filters: [
678
+ prop_filter("ATTENDEE", param_filters: [pf])
679
+ ])
680
+ ])
681
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
682
+ end
606
683
 
607
- it "with text-match: does not match when parameter value differs" do
608
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nATTENDEE;PARTSTAT=DECLINED:mailto:alice@x.com\r\nEND:VEVENT\r\nEND:VCALENDAR")
609
- pf = Protocol::Caldav::Filter::Calendar::ParamFilter.new(
610
- name: "PARTSTAT",
611
- text_match: text_match("ACCEPTED", match_type: "equals")
612
- )
613
- f = comp_filter("VCALENDAR", comp_filters: [
614
- comp_filter("VEVENT", prop_filters: [
615
- prop_filter("ATTENDEE", param_filters: [pf])
616
- ])
617
- ])
618
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
619
- end
684
+ it "with text-match: does not match when parameter value differs" do
685
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nATTENDEE;PARTSTAT=DECLINED:mailto:alice@x.com\r\nEND:VEVENT\r\nEND:VCALENDAR")
686
+ pf = Protocol::Caldav::Filter::Calendar::ParamFilter.new(
687
+ name: "PARTSTAT",
688
+ text_match: text_match("ACCEPTED", match_type: "equals")
689
+ )
690
+ f = comp_filter("VCALENDAR", comp_filters: [
691
+ comp_filter("VEVENT", prop_filters: [
692
+ prop_filter("ATTENDEE", param_filters: [pf])
693
+ ])
694
+ ])
695
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
620
696
  end
697
+ end
621
698
 
622
- describe "time-range on VEVENT" do
623
- it "matches overlapping event" do
624
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260115T090000Z\r\nDTEND:20260115T100000Z\r\nEND:VEVENT\r\nEND:VCALENDAR")
625
- tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
626
- f = comp_filter("VCALENDAR", comp_filters: [
627
- comp_filter("VEVENT", time_range: tr)
628
- ])
629
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
630
- end
699
+ describe "time-range on VEVENT" do
700
+ it "matches overlapping event" do
701
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260115T090000Z\r\nDTEND:20260115T100000Z\r\nEND:VEVENT\r\nEND:VCALENDAR")
702
+ tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
703
+ f = comp_filter("VCALENDAR", comp_filters: [
704
+ comp_filter("VEVENT", time_range: tr)
705
+ ])
706
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
707
+ end
631
708
 
632
- it "does not match event fully before range" do
633
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20251215T090000Z\r\nDTEND:20251215T100000Z\r\nEND:VEVENT\r\nEND:VCALENDAR")
634
- tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
635
- f = comp_filter("VCALENDAR", comp_filters: [
636
- comp_filter("VEVENT", time_range: tr)
637
- ])
638
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
639
- end
709
+ it "does not match event fully before range" do
710
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20251215T090000Z\r\nDTEND:20251215T100000Z\r\nEND:VEVENT\r\nEND:VCALENDAR")
711
+ tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
712
+ f = comp_filter("VCALENDAR", comp_filters: [
713
+ comp_filter("VEVENT", time_range: tr)
714
+ ])
715
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
716
+ end
640
717
 
641
- it "does not match event fully after range" do
642
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260215T090000Z\r\nDTEND:20260215T100000Z\r\nEND:VEVENT\r\nEND:VCALENDAR")
643
- tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
644
- f = comp_filter("VCALENDAR", comp_filters: [
645
- comp_filter("VEVENT", time_range: tr)
646
- ])
647
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
648
- end
718
+ it "does not match event fully after range" do
719
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260215T090000Z\r\nDTEND:20260215T100000Z\r\nEND:VEVENT\r\nEND:VCALENDAR")
720
+ tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
721
+ f = comp_filter("VCALENDAR", comp_filters: [
722
+ comp_filter("VEVENT", time_range: tr)
723
+ ])
724
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
725
+ end
649
726
 
650
- it "half-open: event ending exactly at range start does not match" do
651
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20251231T230000Z\r\nDTEND:20260101T000000Z\r\nEND:VEVENT\r\nEND:VCALENDAR")
652
- tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
653
- f = comp_filter("VCALENDAR", comp_filters: [
654
- comp_filter("VEVENT", time_range: tr)
655
- ])
656
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
657
- end
727
+ it "half-open: event ending exactly at range start does not match" do
728
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20251231T230000Z\r\nDTEND:20260101T000000Z\r\nEND:VEVENT\r\nEND:VCALENDAR")
729
+ tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
730
+ f = comp_filter("VCALENDAR", comp_filters: [
731
+ comp_filter("VEVENT", time_range: tr)
732
+ ])
733
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
734
+ end
658
735
 
659
- it "half-open: event starting exactly at range start matches" do
660
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260101T000000Z\r\nDTEND:20260101T010000Z\r\nEND:VEVENT\r\nEND:VCALENDAR")
661
- tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
662
- f = comp_filter("VCALENDAR", comp_filters: [
663
- comp_filter("VEVENT", time_range: tr)
664
- ])
665
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
666
- end
736
+ it "half-open: event starting exactly at range start matches" do
737
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260101T000000Z\r\nDTEND:20260101T010000Z\r\nEND:VEVENT\r\nEND:VCALENDAR")
738
+ tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
739
+ f = comp_filter("VCALENDAR", comp_filters: [
740
+ comp_filter("VEVENT", time_range: tr)
741
+ ])
742
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
667
743
  end
744
+ end
668
745
 
669
- describe "time-range on VJOURNAL" do
670
- it "matches VJOURNAL with DTSTART in range" do
671
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VJOURNAL\r\nDTSTART:20260115T090000Z\r\nSUMMARY:Note\r\nEND:VJOURNAL\r\nEND:VCALENDAR")
672
- tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
673
- f = comp_filter("VCALENDAR", comp_filters: [
674
- comp_filter("VJOURNAL", time_range: tr)
675
- ])
676
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
677
- end
746
+ describe "time-range on VJOURNAL" do
747
+ it "matches VJOURNAL with DTSTART in range" do
748
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VJOURNAL\r\nDTSTART:20260115T090000Z\r\nSUMMARY:Note\r\nEND:VJOURNAL\r\nEND:VCALENDAR")
749
+ tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
750
+ f = comp_filter("VCALENDAR", comp_filters: [
751
+ comp_filter("VJOURNAL", time_range: tr)
752
+ ])
753
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
754
+ end
678
755
 
679
- it "does not match VJOURNAL with DTSTART outside range" do
680
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VJOURNAL\r\nDTSTART:20260315T090000Z\r\nSUMMARY:Note\r\nEND:VJOURNAL\r\nEND:VCALENDAR")
681
- tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
682
- f = comp_filter("VCALENDAR", comp_filters: [
683
- comp_filter("VJOURNAL", time_range: tr)
684
- ])
685
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
686
- end
756
+ it "does not match VJOURNAL with DTSTART outside range" do
757
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VJOURNAL\r\nDTSTART:20260315T090000Z\r\nSUMMARY:Note\r\nEND:VJOURNAL\r\nEND:VCALENDAR")
758
+ tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
759
+ f = comp_filter("VCALENDAR", comp_filters: [
760
+ comp_filter("VJOURNAL", time_range: tr)
761
+ ])
762
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
763
+ end
687
764
 
688
- it "does not match VJOURNAL without DTSTART" do
689
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VJOURNAL\r\nSUMMARY:Undated\r\nEND:VJOURNAL\r\nEND:VCALENDAR")
690
- tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
691
- f = comp_filter("VCALENDAR", comp_filters: [
692
- comp_filter("VJOURNAL", time_range: tr)
693
- ])
694
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
695
- end
765
+ it "does not match VJOURNAL without DTSTART" do
766
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VJOURNAL\r\nSUMMARY:Undated\r\nEND:VJOURNAL\r\nEND:VCALENDAR")
767
+ tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
768
+ f = comp_filter("VCALENDAR", comp_filters: [
769
+ comp_filter("VJOURNAL", time_range: tr)
770
+ ])
771
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
696
772
  end
773
+ end
697
774
 
698
- describe "time-range on VEVENT with RRULE" do
699
- it "matches recurring event with occurrence in range" do
700
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260101T090000Z\r\nDTEND:20260101T100000Z\r\nRRULE:FREQ=DAILY;COUNT=30\r\nEND:VEVENT\r\nEND:VCALENDAR")
701
- tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260115T000000Z", end_time: "20260116T000000Z")
702
- f = comp_filter("VCALENDAR", comp_filters: [
703
- comp_filter("VEVENT", time_range: tr)
704
- ])
705
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
706
- end
775
+ describe "time-range on VEVENT with RRULE" do
776
+ it "matches recurring event with occurrence in range" do
777
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260101T090000Z\r\nDTEND:20260101T100000Z\r\nRRULE:FREQ=DAILY;COUNT=30\r\nEND:VEVENT\r\nEND:VCALENDAR")
778
+ tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260115T000000Z", end_time: "20260116T000000Z")
779
+ f = comp_filter("VCALENDAR", comp_filters: [
780
+ comp_filter("VEVENT", time_range: tr)
781
+ ])
782
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
783
+ end
707
784
 
708
- it "does not match recurring event when all occurrences are outside range" do
709
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260101T090000Z\r\nDTEND:20260101T100000Z\r\nRRULE:FREQ=DAILY;COUNT=3\r\nEND:VEVENT\r\nEND:VCALENDAR")
710
- tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260201T000000Z", end_time: "20260301T000000Z")
711
- f = comp_filter("VCALENDAR", comp_filters: [
712
- comp_filter("VEVENT", time_range: tr)
713
- ])
714
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
715
- end
785
+ it "does not match recurring event when all occurrences are outside range" do
786
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260101T090000Z\r\nDTEND:20260101T100000Z\r\nRRULE:FREQ=DAILY;COUNT=3\r\nEND:VEVENT\r\nEND:VCALENDAR")
787
+ tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260201T000000Z", end_time: "20260301T000000Z")
788
+ f = comp_filter("VCALENDAR", comp_filters: [
789
+ comp_filter("VEVENT", time_range: tr)
790
+ ])
791
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
792
+ end
716
793
 
717
- it "matches weekly recurring event" do
718
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260105T090000Z\r\nDTEND:20260105T100000Z\r\nRRULE:FREQ=WEEKLY;COUNT=10\r\nEND:VEVENT\r\nEND:VCALENDAR")
719
- # The 3rd occurrence (Jan 19) should be in this range
720
- tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260119T000000Z", end_time: "20260120T000000Z")
721
- f = comp_filter("VCALENDAR", comp_filters: [
722
- comp_filter("VEVENT", time_range: tr)
723
- ])
724
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
725
- end
794
+ it "matches weekly recurring event" do
795
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260105T090000Z\r\nDTEND:20260105T100000Z\r\nRRULE:FREQ=WEEKLY;COUNT=10\r\nEND:VEVENT\r\nEND:VCALENDAR")
796
+ # The 3rd occurrence (Jan 19) should be in this range
797
+ tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260119T000000Z", end_time: "20260120T000000Z")
798
+ f = comp_filter("VCALENDAR", comp_filters: [
799
+ comp_filter("VEVENT", time_range: tr)
800
+ ])
801
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
802
+ end
726
803
 
727
- it "respects EXDATE when filtering recurring events" do
728
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260101T090000Z\r\nDTEND:20260101T100000Z\r\nRRULE:FREQ=DAILY;COUNT=5\r\nEXDATE:20260102T090000Z\r\nEND:VEVENT\r\nEND:VCALENDAR")
729
- # Range covers only Jan 2 — but that's excluded
730
- tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260102T000000Z", end_time: "20260102T120000Z")
731
- f = comp_filter("VCALENDAR", comp_filters: [
732
- comp_filter("VEVENT", time_range: tr)
733
- ])
734
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
735
- end
804
+ it "respects EXDATE when filtering recurring events" do
805
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260101T090000Z\r\nDTEND:20260101T100000Z\r\nRRULE:FREQ=DAILY;COUNT=5\r\nEXDATE:20260102T090000Z\r\nEND:VEVENT\r\nEND:VCALENDAR")
806
+ # Range covers only Jan 2 — but that's excluded
807
+ tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260102T000000Z", end_time: "20260102T120000Z")
808
+ f = comp_filter("VCALENDAR", comp_filters: [
809
+ comp_filter("VEVENT", time_range: tr)
810
+ ])
811
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
736
812
  end
813
+ end
737
814
 
738
- describe "time-range on VTODO" do
739
- it "matches VTODO with DTSTART and DUE in range" do
740
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nDTSTART:20260115T090000Z\r\nDUE:20260116T090000Z\r\nEND:VTODO\r\nEND:VCALENDAR")
741
- tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
742
- f = comp_filter("VCALENDAR", comp_filters: [
743
- comp_filter("VTODO", time_range: tr)
744
- ])
745
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
746
- end
815
+ describe "time-range on VTODO" do
816
+ it "matches VTODO with DTSTART and DUE in range" do
817
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nDTSTART:20260115T090000Z\r\nDUE:20260116T090000Z\r\nEND:VTODO\r\nEND:VCALENDAR")
818
+ tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
819
+ f = comp_filter("VCALENDAR", comp_filters: [
820
+ comp_filter("VTODO", time_range: tr)
821
+ ])
822
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
823
+ end
747
824
 
748
- it "does not match VTODO outside range" do
749
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nDTSTART:20260301T090000Z\r\nDUE:20260302T090000Z\r\nEND:VTODO\r\nEND:VCALENDAR")
750
- tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
751
- f = comp_filter("VCALENDAR", comp_filters: [
752
- comp_filter("VTODO", time_range: tr)
753
- ])
754
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
755
- end
825
+ it "does not match VTODO outside range" do
826
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nDTSTART:20260301T090000Z\r\nDUE:20260302T090000Z\r\nEND:VTODO\r\nEND:VCALENDAR")
827
+ tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
828
+ f = comp_filter("VCALENDAR", comp_filters: [
829
+ comp_filter("VTODO", time_range: tr)
830
+ ])
831
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal false
832
+ end
756
833
 
757
- it "matches VTODO with only COMPLETED in range" do
758
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nCOMPLETED:20260115T090000Z\r\nEND:VTODO\r\nEND:VCALENDAR")
759
- tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
760
- f = comp_filter("VCALENDAR", comp_filters: [
761
- comp_filter("VTODO", time_range: tr)
762
- ])
763
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
764
- end
834
+ it "matches VTODO with only COMPLETED in range" do
835
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nCOMPLETED:20260115T090000Z\r\nEND:VTODO\r\nEND:VCALENDAR")
836
+ tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
837
+ f = comp_filter("VCALENDAR", comp_filters: [
838
+ comp_filter("VTODO", time_range: tr)
839
+ ])
840
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
841
+ end
765
842
 
766
- it "matches VTODO with no dates (always matches per RFC 4791)" do
767
- vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nSUMMARY:Undated task\r\nEND:VTODO\r\nEND:VCALENDAR")
768
- tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
769
- f = comp_filter("VCALENDAR", comp_filters: [
770
- comp_filter("VTODO", time_range: tr)
771
- ])
772
- Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
773
- end
843
+ it "matches VTODO with no dates (always matches per RFC 4791)" do
844
+ vcal = parse_ical("BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nSUMMARY:Undated task\r\nEND:VTODO\r\nEND:VCALENDAR")
845
+ tr = Protocol::Caldav::Filter::Calendar::TimeRange.new(start_time: "20260101T000000Z", end_time: "20260201T000000Z")
846
+ f = comp_filter("VCALENDAR", comp_filters: [
847
+ comp_filter("VTODO", time_range: tr)
848
+ ])
849
+ Protocol::Caldav::Filter::Match.calendar?(f, vcal).should.equal true
774
850
  end
775
851
  end
776
852
  end