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.
- checksums.yaml +4 -4
- data/lib/protocol/caldav/collection.rb +99 -75
- data/lib/protocol/caldav/constants.rb +23 -27
- data/lib/protocol/caldav/content_line.rb +94 -91
- data/lib/protocol/caldav/ctag.rb +46 -50
- data/lib/protocol/caldav/etag.rb +24 -28
- data/lib/protocol/caldav/filter/addressbook.rb +41 -28
- data/lib/protocol/caldav/filter/calendar.rb +65 -44
- data/lib/protocol/caldav/filter/match.rb +632 -556
- data/lib/protocol/caldav/filter/parser.rb +273 -252
- data/lib/protocol/caldav/ical/component.rb +49 -46
- data/lib/protocol/caldav/ical/expand.rb +148 -134
- data/lib/protocol/caldav/ical/freebusy.rb +106 -74
- data/lib/protocol/caldav/ical/parser.rb +139 -139
- data/lib/protocol/caldav/ical/property.rb +22 -21
- data/lib/protocol/caldav/ical/rrule.rb +346 -235
- data/lib/protocol/caldav/item.rb +41 -43
- data/lib/protocol/caldav/multistatus.rb +43 -46
- data/lib/protocol/caldav/path.rb +82 -79
- data/lib/protocol/caldav/storage.rb +24 -28
- data/lib/protocol/caldav/vcard/card.rb +25 -27
- data/lib/protocol/caldav/vcard/parser.rb +56 -59
- data/lib/protocol/caldav/version.rb +1 -1
- data/lib/protocol/caldav/xml.rb +84 -80
- data/lib/protocol/caldav/xml_builder.rb +4 -2
- metadata +18 -4
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "bundler/setup"
|
|
4
|
-
require "scampi"
|
|
5
3
|
require "protocol/caldav"
|
|
6
4
|
|
|
7
5
|
module Protocol
|
|
8
6
|
module Caldav
|
|
9
7
|
module Ical
|
|
10
|
-
Component = Struct.new(
|
|
8
|
+
Component = Struct.new(
|
|
9
|
+
:name,
|
|
10
|
+
:properties,
|
|
11
|
+
:components,
|
|
12
|
+
keyword_init: true,
|
|
13
|
+
) do
|
|
11
14
|
def initialize(name:, properties: [], components: [])
|
|
12
15
|
super(name: name, properties: properties, components: components)
|
|
13
16
|
end
|
|
@@ -28,57 +31,57 @@ module Protocol
|
|
|
28
31
|
end
|
|
29
32
|
end
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
describe "Protocol::Caldav::Ical::Component" do
|
|
33
|
-
def prop(name, value, params: {})
|
|
34
|
-
Protocol::Caldav::Ical::Property.new(name: name, params: params, value: value)
|
|
35
|
-
end
|
|
34
|
+
__END__
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
describe "Protocol::Caldav::Ical::Component" do
|
|
37
|
+
def prop(name, value, params: {})
|
|
38
|
+
Protocol::Caldav::Ical::Property.new(name: name, params: params, value: value)
|
|
39
|
+
end
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
end
|
|
41
|
+
def comp(name, properties: [], components: [])
|
|
42
|
+
Protocol::Caldav::Ical::Component.new(name: name, properties: properties, components: components)
|
|
43
|
+
end
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
it "find_property returns the first property with that name" do
|
|
46
|
+
c = comp("VEVENT", properties: [prop("SUMMARY", "First"), prop("SUMMARY", "Second")])
|
|
47
|
+
c.find_property("SUMMARY").value.should.equal "First"
|
|
48
|
+
end
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
it "find_property is case-insensitive" do
|
|
51
|
+
c = comp("VEVENT", properties: [prop("SUMMARY", "Hello")])
|
|
52
|
+
c.find_property("summary").value.should.equal "Hello"
|
|
53
|
+
end
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
it "find_property returns nil when absent" do
|
|
56
|
+
c = comp("VEVENT", properties: [])
|
|
57
|
+
c.find_property("SUMMARY").should.be.nil
|
|
58
|
+
end
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
it "find_components returns all matching children" do
|
|
61
|
+
c = comp("VCALENDAR", components: [comp("VEVENT"), comp("VTODO"), comp("VEVENT")])
|
|
62
|
+
c.find_components("VEVENT").length.should.equal 2
|
|
63
|
+
end
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
vcal.find_components("VALARM").should.equal []
|
|
71
|
-
end
|
|
65
|
+
it "find_components returns empty array when none match" do
|
|
66
|
+
c = comp("VCALENDAR", components: [comp("VEVENT")])
|
|
67
|
+
c.find_components("VTODO").should.equal []
|
|
68
|
+
end
|
|
72
69
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
it "find_components does not recurse into grandchildren" do
|
|
71
|
+
inner = comp("VALARM")
|
|
72
|
+
vevent = comp("VEVENT", components: [inner])
|
|
73
|
+
vcal = comp("VCALENDAR", components: [vevent])
|
|
74
|
+
vcal.find_components("VALARM").should.equal []
|
|
75
|
+
end
|
|
78
76
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
it "a component with no properties or sub-components is valid" do
|
|
78
|
+
c = comp("VEVENT")
|
|
79
|
+
c.properties.should.equal []
|
|
80
|
+
c.components.should.equal []
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "find_all_properties returns all matching" do
|
|
84
|
+
c = comp("VEVENT", properties: [prop("ATTENDEE", "a"), prop("SUMMARY", "x"), prop("ATTENDEE", "b")])
|
|
85
|
+
c.find_all_properties("ATTENDEE").length.should.equal 2
|
|
83
86
|
end
|
|
84
87
|
end
|
|
@@ -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
|
|
@@ -15,62 +12,69 @@ module Protocol
|
|
|
15
12
|
# Returns a serialized VCALENDAR string with RRULE removed
|
|
16
13
|
# and each occurrence as a separate VEVENT with RECURRENCE-ID.
|
|
17
14
|
def expand(component, range_start:, range_end:, max_occurrences: 10000)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
15
|
+
if component.name.casecmp?('VCALENDAR')
|
|
16
|
+
base = component.find_components('VEVENT').find { |v| v.find_property('RRULE') }
|
|
17
|
+
if base
|
|
18
|
+
uid = base.find_property('UID')&.value
|
|
19
|
+
overrides = {}
|
|
20
|
+
component.find_components('VEVENT').each do |v|
|
|
21
|
+
rid = v.find_property('RECURRENCE-ID')
|
|
22
|
+
if rid && v.find_property('UID')&.value == uid
|
|
23
|
+
rid_time = Filter::Match.send(:parse_datetime_string, rid.value.strip)
|
|
24
|
+
if rid_time
|
|
25
|
+
overrides[rid_time] = v
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
dtstart_prop = base.find_property('DTSTART')
|
|
30
|
+
dtstart = Filter::Match.send(:parse_datetime_string, dtstart_prop.value.strip)
|
|
31
|
+
if dtstart
|
|
32
|
+
dtend = Filter::Match.send(:parse_ical_datetime, base, 'DTEND')
|
|
33
|
+
if dtend
|
|
34
|
+
duration = (dtend - dtstart).to_i
|
|
35
|
+
else
|
|
36
|
+
duration = 3600
|
|
37
|
+
end
|
|
38
|
+
rrule = base.find_property('RRULE').value.strip
|
|
39
|
+
exdates = base.find_all_properties('EXDATE').filter_map do |ex|
|
|
40
|
+
Filter::Match.send(:parse_datetime_string, ex.value.strip)
|
|
41
|
+
end
|
|
42
|
+
occurrences = Rrule.expand(
|
|
43
|
+
dtstart: dtstart,
|
|
44
|
+
rrule_value: rrule,
|
|
45
|
+
range_start: range_start,
|
|
46
|
+
range_end: range_end,
|
|
47
|
+
exdates: exdates,
|
|
48
|
+
max_count: max_occurrences,
|
|
49
|
+
)
|
|
50
|
+
instances = occurrences.map do |occ_start|
|
|
51
|
+
override = overrides[occ_start]
|
|
52
|
+
if override
|
|
53
|
+
serialize_vevent_instance(override, occ_start)
|
|
54
|
+
else
|
|
55
|
+
serialize_expanded_instance(base, occ_start, duration)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
overrides.each do |rid_time, override_vevent|
|
|
59
|
+
if occurrences.any? { |o| Rrule.send(:times_equal?, o, rid_time) }
|
|
60
|
+
next
|
|
61
|
+
end
|
|
62
|
+
odt = Filter::Match.send(:parse_ical_datetime, override_vevent, 'DTSTART')
|
|
63
|
+
unless odt && odt >= range_start && odt < range_end
|
|
64
|
+
next
|
|
65
|
+
end
|
|
66
|
+
instances << serialize_vevent_instance(override_vevent, rid_time)
|
|
67
|
+
end
|
|
68
|
+
"BEGIN:VCALENDAR\r\n#{instances.join}END:VCALENDAR\r\n"
|
|
69
|
+
else
|
|
70
|
+
serialize_component(component)
|
|
71
|
+
end
|
|
60
72
|
else
|
|
61
|
-
|
|
73
|
+
serialize_component(component)
|
|
62
74
|
end
|
|
75
|
+
else
|
|
76
|
+
serialize_component(component)
|
|
63
77
|
end
|
|
64
|
-
|
|
65
|
-
# Also include overrides that aren't in the base RRULE expansion
|
|
66
|
-
overrides.each do |rid_time, override_vevent|
|
|
67
|
-
next if occurrences.any? { |o| Rrule.send(:times_equal?, o, rid_time) }
|
|
68
|
-
odt = Filter::Match.send(:parse_ical_datetime, override_vevent, 'DTSTART')
|
|
69
|
-
next unless odt && odt >= range_start && odt < range_end
|
|
70
|
-
instances << serialize_vevent_instance(override_vevent, rid_time)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
"BEGIN:VCALENDAR\r\n#{instances.join}END:VCALENDAR\r\n"
|
|
74
78
|
end
|
|
75
79
|
|
|
76
80
|
def serialize_expanded_instance(base, occ_start, duration)
|
|
@@ -82,7 +86,9 @@ module Protocol
|
|
|
82
86
|
lines << "DTEND:#{format_utc(occ_end)}"
|
|
83
87
|
|
|
84
88
|
base.properties.each do |prop|
|
|
85
|
-
|
|
89
|
+
if %w[DTSTART DTEND RRULE EXDATE RDATE EXRULE RECURRENCE-ID DURATION].include?(prop.name.upcase)
|
|
90
|
+
next
|
|
91
|
+
end
|
|
86
92
|
lines << "#{prop.name}:#{prop.value}"
|
|
87
93
|
end
|
|
88
94
|
|
|
@@ -95,11 +101,17 @@ module Protocol
|
|
|
95
101
|
lines << "BEGIN:VEVENT"
|
|
96
102
|
has_rid = false
|
|
97
103
|
vevent.properties.each do |prop|
|
|
98
|
-
|
|
99
|
-
|
|
104
|
+
if %w[RRULE EXDATE RDATE EXRULE].include?(prop.name.upcase)
|
|
105
|
+
next
|
|
106
|
+
end
|
|
107
|
+
if prop.name.casecmp?('RECURRENCE-ID')
|
|
108
|
+
has_rid = true
|
|
109
|
+
end
|
|
100
110
|
lines << "#{prop.name}:#{prop.value}"
|
|
101
111
|
end
|
|
102
|
-
|
|
112
|
+
unless has_rid
|
|
113
|
+
lines.insert(1, "RECURRENCE-ID:#{format_utc(rid_time)}")
|
|
114
|
+
end
|
|
103
115
|
lines << "END:VEVENT"
|
|
104
116
|
lines.map { |l| "#{l}\r\n" }.join
|
|
105
117
|
end
|
|
@@ -116,93 +128,95 @@ module Protocol
|
|
|
116
128
|
time.utc.strftime('%Y%m%dT%H%M%SZ')
|
|
117
129
|
end
|
|
118
130
|
|
|
119
|
-
private_class_method :serialize_expanded_instance,
|
|
120
|
-
|
|
131
|
+
private_class_method :serialize_expanded_instance,
|
|
132
|
+
:serialize_vevent_instance,
|
|
133
|
+
:serialize_component,
|
|
134
|
+
:format_utc
|
|
121
135
|
end
|
|
122
136
|
end
|
|
123
137
|
end
|
|
124
138
|
end
|
|
125
139
|
|
|
126
|
-
|
|
127
|
-
describe "Protocol::Caldav::Ical::Expand" do
|
|
128
|
-
def parse(text)
|
|
129
|
-
Protocol::Caldav::Ical::Parser.parse(text)
|
|
130
|
-
end
|
|
140
|
+
__END__
|
|
131
141
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
result.should.include "SUMMARY:Hello"
|
|
137
|
-
result.should.include "END:VEVENT"
|
|
138
|
-
end
|
|
142
|
+
describe "Protocol::Caldav::Ical::Expand" do
|
|
143
|
+
def parse(text)
|
|
144
|
+
Protocol::Caldav::Ical::Parser.parse(text)
|
|
145
|
+
end
|
|
139
146
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
end
|
|
147
|
+
it "returns serialized component when not VCALENDAR" do
|
|
148
|
+
vevent = parse("BEGIN:VEVENT\r\nSUMMARY:Hello\r\nEND:VEVENT")
|
|
149
|
+
result = Protocol::Caldav::Ical::Expand.expand(vevent, range_start: Time.utc(2026, 1, 1), range_end: Time.utc(2026, 12, 31))
|
|
150
|
+
result.should.include "BEGIN:VEVENT"
|
|
151
|
+
result.should.include "SUMMARY:Hello"
|
|
152
|
+
result.should.include "END:VEVENT"
|
|
153
|
+
end
|
|
148
154
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
it "returns serialized component when no RRULE found" do
|
|
156
|
+
ical = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260101T090000Z\r\nSUMMARY:Once\r\nEND:VEVENT\r\nEND:VCALENDAR"
|
|
157
|
+
component = parse(ical)
|
|
158
|
+
result = Protocol::Caldav::Ical::Expand.expand(component, range_start: Time.utc(2026, 1, 1), range_end: Time.utc(2026, 12, 31))
|
|
159
|
+
result.should.include "BEGIN:VCALENDAR"
|
|
160
|
+
result.should.include "SUMMARY:Once"
|
|
161
|
+
result.should.include "END:VCALENDAR"
|
|
162
|
+
end
|
|
157
163
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
+
it "expands daily RRULE into individual instances" do
|
|
165
|
+
ical = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260101T090000Z\r\nDTEND:20260101T100000Z\r\nRRULE:FREQ=DAILY;COUNT=3\r\nUID:expand-test\r\nSUMMARY:Daily\r\nEND:VEVENT\r\nEND:VCALENDAR"
|
|
166
|
+
component = parse(ical)
|
|
167
|
+
result = Protocol::Caldav::Ical::Expand.expand(component, range_start: Time.utc(2026, 1, 1), range_end: Time.utc(2026, 1, 10))
|
|
168
|
+
result.scan("BEGIN:VEVENT").length.should.equal 3
|
|
169
|
+
result.should.include "RECURRENCE-ID"
|
|
170
|
+
result.should.include "SUMMARY:Daily"
|
|
171
|
+
end
|
|
164
172
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
result.should.include "SUMMARY:Base"
|
|
172
|
-
end
|
|
173
|
+
it "removes RRULE from expanded output" do
|
|
174
|
+
ical = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260101T090000Z\r\nDTEND:20260101T100000Z\r\nRRULE:FREQ=DAILY;COUNT=2\r\nUID:no-rrule\r\nEND:VEVENT\r\nEND:VCALENDAR"
|
|
175
|
+
component = parse(ical)
|
|
176
|
+
result = Protocol::Caldav::Ical::Expand.expand(component, range_start: Time.utc(2026, 1, 1), range_end: Time.utc(2026, 1, 10))
|
|
177
|
+
result.should.not.include "RRULE"
|
|
178
|
+
end
|
|
173
179
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
it "applies override VEVENT replacing base occurrence" do
|
|
181
|
+
ical = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260101T090000Z\r\nDTEND:20260101T100000Z\r\nRRULE:FREQ=DAILY;COUNT=3\r\nUID:override-test\r\nSUMMARY:Base\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nDTSTART:20260102T140000Z\r\nDTEND:20260102T150000Z\r\nRECURRENCE-ID:20260102T090000Z\r\nUID:override-test\r\nSUMMARY:Override\r\nEND:VEVENT\r\nEND:VCALENDAR"
|
|
182
|
+
component = parse(ical)
|
|
183
|
+
result = Protocol::Caldav::Ical::Expand.expand(component, range_start: Time.utc(2026, 1, 1), range_end: Time.utc(2026, 1, 10))
|
|
184
|
+
result.scan("BEGIN:VEVENT").length.should.equal 3
|
|
185
|
+
result.should.include "SUMMARY:Override"
|
|
186
|
+
result.should.include "SUMMARY:Base"
|
|
187
|
+
end
|
|
182
188
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
+
it "non-recurring event passes through unchanged" do
|
|
190
|
+
ical = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260101T090000Z\r\nDTEND:20260101T100000Z\r\nUID:single\r\nSUMMARY:Once\r\nEND:VEVENT\r\nEND:VCALENDAR"
|
|
191
|
+
component = parse(ical)
|
|
192
|
+
result = Protocol::Caldav::Ical::Expand.expand(component, range_start: Time.utc(2026, 1, 1), range_end: Time.utc(2026, 12, 31))
|
|
193
|
+
result.scan("BEGIN:VEVENT").length.should.equal 1
|
|
194
|
+
result.should.include "SUMMARY:Once"
|
|
195
|
+
result.should.not.include "RECURRENCE-ID"
|
|
196
|
+
end
|
|
189
197
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
result.should.include "20260105T090000Z"
|
|
197
|
-
end
|
|
198
|
+
it "respects EXDATE exclusions" do
|
|
199
|
+
ical = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260101T090000Z\r\nDTEND:20260101T100000Z\r\nRRULE:FREQ=DAILY;COUNT=3\r\nEXDATE:20260102T090000Z\r\nUID:exdate-test\r\nEND:VEVENT\r\nEND:VCALENDAR"
|
|
200
|
+
component = parse(ical)
|
|
201
|
+
result = Protocol::Caldav::Ical::Expand.expand(component, range_start: Time.utc(2026, 1, 1), range_end: Time.utc(2026, 1, 10))
|
|
202
|
+
result.scan("BEGIN:VEVENT").length.should.equal 2
|
|
203
|
+
end
|
|
198
204
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
it "only returns instances within range" do
|
|
206
|
+
ical = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260101T090000Z\r\nDTEND:20260101T100000Z\r\nRRULE:FREQ=DAILY;COUNT=10\r\nUID:range-test\r\nEND:VEVENT\r\nEND:VCALENDAR"
|
|
207
|
+
component = parse(ical)
|
|
208
|
+
result = Protocol::Caldav::Ical::Expand.expand(component, range_start: Time.utc(2026, 1, 3), range_end: Time.utc(2026, 1, 6))
|
|
209
|
+
result.scan("BEGIN:VEVENT").length.should.equal 3
|
|
210
|
+
result.should.include "20260103T090000Z"
|
|
211
|
+
result.should.include "20260105T090000Z"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
it "preserves non-recurrence properties in expanded instances" do
|
|
215
|
+
ical = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260101T090000Z\r\nDTEND:20260101T100000Z\r\nRRULE:FREQ=DAILY;COUNT=2\r\nUID:props-test\r\nSUMMARY:Keep Me\r\nLOCATION:Room 1\r\nEND:VEVENT\r\nEND:VCALENDAR"
|
|
216
|
+
component = parse(ical)
|
|
217
|
+
result = Protocol::Caldav::Ical::Expand.expand(component, range_start: Time.utc(2026, 1, 1), range_end: Time.utc(2026, 1, 10))
|
|
218
|
+
# Both instances should have SUMMARY and LOCATION
|
|
219
|
+
result.scan("SUMMARY:Keep Me").length.should.equal 2
|
|
220
|
+
result.scan("LOCATION:Room 1").length.should.equal 2
|
|
207
221
|
end
|
|
208
222
|
end
|