@@ -129,6 +129,91 @@ func TestGenerateRSVP(t *testing.T) {
129129 }
130130}
131131
132+ // buildICS wraps DTSTART/DTEND lines into a minimal VCALENDAR for ParseICS.
133+ func buildICS (dtstart , dtend string ) []byte {
134+ return []byte ("BEGIN:VCALENDAR\r \n " +
135+ "VERSION:2.0\r \n " +
136+ "PRODID:-//Test//Test//EN\r \n " +
137+ "BEGIN:VEVENT\r \n " +
138+ "UID:date-only@example.com\r \n " +
139+ "DTSTAMP:20260415T120000Z\r \n " +
140+ dtstart + "\r \n " +
141+ dtend + "\r \n " +
142+ "SUMMARY:Test\r \n " +
143+ "END:VEVENT\r \n " +
144+ "END:VCALENDAR\r \n " )
145+ }
146+
147+ func TestParseICS_DateOnly (t * testing.T ) {
148+ wantStart := time .Date (2026 , 4 , 21 , 0 , 0 , 0 , 0 , time .UTC )
149+ wantEnd := time .Date (2026 , 4 , 22 , 0 , 0 , 0 , 0 , time .UTC )
150+
151+ tests := []struct {
152+ name string
153+ dtstart string
154+ dtend string
155+ }{
156+ {
157+ name : "VALUE=DATE without TZID" ,
158+ dtstart : "DTSTART;VALUE=DATE:20260421" ,
159+ dtend : "DTEND;VALUE=DATE:20260422" ,
160+ },
161+ {
162+ // Regression: TZID present on a date-only value must be ignored
163+ // (RFC 5545 forbids TZID with VALUE=DATE; some producers emit it anyway).
164+ name : "VALUE=DATE with TZID is ignored" ,
165+ dtstart : "DTSTART;TZID=America/New_York;VALUE=DATE:20260421" ,
166+ dtend : "DTEND;TZID=America/New_York;VALUE=DATE:20260422" ,
167+ },
168+ {
169+ // Shape-only detection: no VALUE param, but YYYYMMDD value with TZID.
170+ name : "YYYYMMDD shape with TZID is treated as date-only" ,
171+ dtstart : "DTSTART;TZID=America/Los_Angeles:20260421" ,
172+ dtend : "DTEND;TZID=America/Los_Angeles:20260422" ,
173+ },
174+ }
175+
176+ for _ , tt := range tests {
177+ t .Run (tt .name , func (t * testing.T ) {
178+ event , err := ParseICS (buildICS (tt .dtstart , tt .dtend ))
179+ if err != nil {
180+ t .Fatalf ("ParseICS failed: %v" , err )
181+ }
182+ if ! event .Start .Equal (wantStart ) {
183+ t .Errorf ("Start = %v, want %v" , event .Start .UTC (), wantStart )
184+ }
185+ if ! event .End .Equal (wantEnd ) {
186+ t .Errorf ("End = %v, want %v" , event .End .UTC (), wantEnd )
187+ }
188+ })
189+ }
190+ }
191+
192+ func TestParseICS_TimedWithTZID (t * testing.T ) {
193+ // Existing behavior: timed values with TZID keep their zone semantics.
194+ event , err := ParseICS (buildICS (
195+ "DTSTART;TZID=America/New_York:20260421T140000" ,
196+ "DTEND;TZID=America/New_York:20260421T153000" ,
197+ ))
198+ if err != nil {
199+ t .Fatalf ("ParseICS failed: %v" , err )
200+ }
201+
202+ loc , err := time .LoadLocation ("America/New_York" )
203+ if err != nil {
204+ t .Skipf ("America/New_York unavailable on this system: %v" , err )
205+ }
206+ wantStart := time .Date (2026 , 4 , 21 , 14 , 0 , 0 , 0 , loc )
207+ wantEnd := time .Date (2026 , 4 , 21 , 15 , 30 , 0 , 0 , loc )
208+
209+ if ! event .Start .Equal (wantStart ) {
210+ t .Errorf ("Start = %v, want %v" , event .Start , wantStart )
211+ }
212+ if ! event .End .Equal (wantEnd ) {
213+ t .Errorf ("End = %v, want %v" , event .End , wantEnd )
214+ }
215+ }
216+
132217func TestExtractEmail (t * testing.T ) {
133218 tests := []struct {
134219 input string
0 commit comments