@@ -497,4 +497,147 @@ END:VCALENDAR`);
497497 expect ( filteredEvents [ 0 ] . location ) . toBe ( "Berlin" ) ;
498498 } ) ;
499499 } ) ;
500+
501+ describe ( "preFilterICSText" , ( ) => {
502+ const makeICS = ( events ) => {
503+ const header = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\n" ;
504+ const footer = "END:VCALENDAR\r\n" ;
505+ return header + events . join ( "" ) + footer ;
506+ } ;
507+
508+ const makeEvent = ( dtstart , dtend , summary , extra = "" ) => {
509+ return `BEGIN:VEVENT\r\nDTSTART:${ dtstart } \r\nDTEND:${ dtend } \r\nSUMMARY:${ summary } \r\n${ extra ? `${ extra } \r\n` : "" } END:VEVENT\r\n` ;
510+ } ;
511+
512+ // Format: YYYYMMDDTHHMMSSZ
513+ const dateStr = ( m ) => `${ m . clone ( ) . startOf ( "day" ) . format ( "YYYYMMDD" ) } T120000Z` ;
514+
515+ const now = moment ( ) ;
516+ const past = now . clone ( ) . subtract ( 400 , "days" ) ;
517+ const recent = now . clone ( ) . subtract ( 1 , "days" ) ;
518+ const tomorrow = now . clone ( ) . add ( 1 , "days" ) ;
519+ const future = now . clone ( ) . add ( 400 , "days" ) ;
520+
521+ const preFilterConfig = { includePastEvents : false , maximumNumberOfDays : 365 } ;
522+
523+ it ( "should return unchanged ICS if there are no VEVENT blocks" , ( ) => {
524+ const input = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nEND:VCALENDAR\r\n" ;
525+ const result = CalendarFetcherUtils . preFilterICSText ( input , preFilterConfig ) ;
526+ expect ( result ) . toBe ( input ) ;
527+ } ) ;
528+
529+ it ( "should keep events within the date range" , ( ) => {
530+ const icsText = makeICS ( [ makeEvent ( dateStr ( recent ) , dateStr ( tomorrow ) , "Recent event" ) ] ) ;
531+ const result = CalendarFetcherUtils . preFilterICSText ( icsText , preFilterConfig ) ;
532+ expect ( result ) . toContain ( "Recent event" ) ;
533+ } ) ;
534+
535+ it ( "should drop non-recurring events that ended in the distant past" , ( ) => {
536+ const icsText = makeICS ( [ makeEvent ( dateStr ( past ) , dateStr ( past . clone ( ) . add ( 1 , "days" ) ) , "Old event" ) ] ) ;
537+ const result = CalendarFetcherUtils . preFilterICSText ( icsText , preFilterConfig ) ;
538+ expect ( result ) . not . toContain ( "Old event" ) ;
539+ } ) ;
540+
541+ it ( "should drop non-recurring events that start in the distant future" , ( ) => {
542+ const icsText = makeICS ( [ makeEvent ( dateStr ( future ) , dateStr ( future . clone ( ) . add ( 1 , "days" ) ) , "Far future event" ) ] ) ;
543+ const result = CalendarFetcherUtils . preFilterICSText ( icsText , preFilterConfig ) ;
544+ expect ( result ) . not . toContain ( "Far future event" ) ;
545+ } ) ;
546+
547+ it ( "should always keep events with RRULE regardless of DTSTART" , ( ) => {
548+ const icsText = makeICS ( [ makeEvent ( dateStr ( past ) , dateStr ( past . clone ( ) . add ( 1 , "days" ) ) , "Birthday" , "RRULE:FREQ=YEARLY" ) ] ) ;
549+ const result = CalendarFetcherUtils . preFilterICSText ( icsText , preFilterConfig ) ;
550+ expect ( result ) . toContain ( "Birthday" ) ;
551+ } ) ;
552+
553+ it ( "should keep recurring events even when property names are lowercase" , ( ) => {
554+ const icsText = makeICS ( [ makeEvent ( dateStr ( past ) , dateStr ( past . clone ( ) . add ( 1 , "days" ) ) , "Lowercase recurrence" , "rrule:FREQ=YEARLY" ) ] ) ;
555+ const result = CalendarFetcherUtils . preFilterICSText ( icsText , preFilterConfig ) ;
556+ expect ( result ) . toContain ( "Lowercase recurrence" ) ;
557+ } ) ;
558+
559+ it ( "should always keep events with RDATE regardless of DTSTART" , ( ) => {
560+ const icsText = makeICS ( [ makeEvent ( dateStr ( past ) , dateStr ( past . clone ( ) . add ( 1 , "days" ) ) , "RDATE event" , `RDATE:${ dateStr ( tomorrow ) } ` ) ] ) ;
561+ const result = CalendarFetcherUtils . preFilterICSText ( icsText , preFilterConfig ) ;
562+ expect ( result ) . toContain ( "RDATE event" ) ;
563+ } ) ;
564+
565+ it ( "should always keep events with RECURRENCE-ID" , ( ) => {
566+ const icsText = makeICS ( [ makeEvent ( dateStr ( past ) , dateStr ( past . clone ( ) . add ( 1 , "days" ) ) , "Override" , `RECURRENCE-ID:${ dateStr ( past ) } ` ) ] ) ;
567+ const result = CalendarFetcherUtils . preFilterICSText ( icsText , preFilterConfig ) ;
568+ expect ( result ) . toContain ( "Override" ) ;
569+ } ) ;
570+
571+ it ( "should handle DTSTART with TZID parameter (out of range)" , ( ) => {
572+ const dateOnlyStr = `${ past . clone ( ) . startOf ( "day" ) . format ( "YYYYMMDD" ) } T120000` ;
573+ const event = `BEGIN:VEVENT\r\nDTSTART;TZID=America/New_York:${ dateOnlyStr } \r\nDTEND;TZID=America/New_York:${ dateOnlyStr } \r\nSUMMARY:Old TZ event\r\nEND:VEVENT\r\n` ;
574+ const icsText = makeICS ( [ event ] ) ;
575+ const result = CalendarFetcherUtils . preFilterICSText ( icsText , preFilterConfig ) ;
576+ expect ( result ) . not . toContain ( "Old TZ event" ) ;
577+ } ) ;
578+
579+ it ( "should handle DTSTART with TZID parameter (in range)" , ( ) => {
580+ const startStr = `${ tomorrow . clone ( ) . startOf ( "day" ) . format ( "YYYYMMDD" ) } T120000` ;
581+ const event = `BEGIN:VEVENT\r\nDTSTART;TZID=America/New_York:${ startStr } \r\nDTEND;TZID=America/New_York:${ startStr } \r\nSUMMARY:Upcoming TZ event\r\nEND:VEVENT\r\n` ;
582+ const icsText = makeICS ( [ event ] ) ;
583+ const result = CalendarFetcherUtils . preFilterICSText ( icsText , preFilterConfig ) ;
584+ expect ( result ) . toContain ( "Upcoming TZ event" ) ;
585+ } ) ;
586+
587+ it ( "should drop events whose DTSTART + DURATION still lies in the past" , ( ) => {
588+ const event = `BEGIN:VEVENT\r\nDTSTART:${ dateStr ( past ) } \r\nDURATION:P1D\r\nSUMMARY:Short past event\r\nEND:VEVENT\r\n` ;
589+ const result = CalendarFetcherUtils . preFilterICSText ( makeICS ( [ event ] ) , preFilterConfig ) ;
590+ expect ( result ) . not . toContain ( "Short past event" ) ;
591+ } ) ;
592+
593+ it ( "should keep events whose DURATION extends into the window" , ( ) => {
594+ const event = `BEGIN:VEVENT\r\nDTSTART:${ dateStr ( past ) } \r\nDURATION:P800D\r\nSUMMARY:Long running event\r\nEND:VEVENT\r\n` ;
595+ const result = CalendarFetcherUtils . preFilterICSText ( makeICS ( [ event ] ) , preFilterConfig ) ;
596+ expect ( result ) . toContain ( "Long running event" ) ;
597+ } ) ;
598+
599+ it ( "should handle folded DTSTART lines (RFC 5545 line folding)" , ( ) => {
600+ const dateOnlyStr = `${ past . clone ( ) . startOf ( "day" ) . format ( "YYYYMMDD" ) } T120000` ;
601+ // Fold after the parameter list — a CRLF + space continues the previous line.
602+ const event = `BEGIN:VEVENT\r\nDTSTART;TZID=Europe/Berlin\r\n :${ dateOnlyStr } \r\nSUMMARY:Folded event\r\nEND:VEVENT\r\n` ;
603+ const result = CalendarFetcherUtils . preFilterICSText ( makeICS ( [ event ] ) , preFilterConfig ) ;
604+ expect ( result ) . not . toContain ( "Folded event" ) ;
605+ } ) ;
606+
607+ it ( "should handle all-day events with VALUE=DATE format" , ( ) => {
608+ const recentDate = recent . clone ( ) . startOf ( "day" ) . format ( "YYYYMMDD" ) ;
609+ const event = `BEGIN:VEVENT\r\nDTSTART;VALUE=DATE:${ recentDate } \r\nDTEND;VALUE=DATE:${ recent . clone ( ) . add ( 1 , "days" ) . startOf ( "day" ) . format ( "YYYYMMDD" ) } \r\nSUMMARY:All day event\r\nEND:VEVENT\r\n` ;
610+ const icsText = makeICS ( [ event ] ) ;
611+ const result = CalendarFetcherUtils . preFilterICSText ( icsText , preFilterConfig ) ;
612+ expect ( result ) . toContain ( "All day event" ) ;
613+ } ) ;
614+
615+ it ( "should keep multiple events selectively" , ( ) => {
616+ const icsText = makeICS ( [
617+ makeEvent ( dateStr ( past ) , dateStr ( past . clone ( ) . add ( 1 , "days" ) ) , "Old event" ) ,
618+ makeEvent ( dateStr ( recent ) , dateStr ( tomorrow ) , "Current event" ) ,
619+ makeEvent ( dateStr ( future ) , dateStr ( future . clone ( ) . add ( 1 , "days" ) ) , "Far future event" )
620+ ] ) ;
621+ const result = CalendarFetcherUtils . preFilterICSText ( icsText , preFilterConfig ) ;
622+ expect ( result ) . not . toContain ( "Old event" ) ;
623+ expect ( result ) . toContain ( "Current event" ) ;
624+ expect ( result ) . not . toContain ( "Far future event" ) ;
625+ } ) ;
626+
627+ it ( "should include past events when includePastEvents is true" , ( ) => {
628+ const recentPast = now . clone ( ) . subtract ( 100 , "days" ) ;
629+ const icsText = makeICS ( [ makeEvent ( dateStr ( recentPast ) , dateStr ( recentPast . clone ( ) . add ( 1 , "days" ) ) , "Past event" ) ] ) ;
630+ const result = CalendarFetcherUtils . preFilterICSText ( icsText , { includePastEvents : true , maximumNumberOfDays : 365 } ) ;
631+ expect ( result ) . toContain ( "Past event" ) ;
632+ } ) ;
633+
634+ it ( "should preserve the ICS header and footer" , ( ) => {
635+ const icsText = makeICS ( [ makeEvent ( dateStr ( recent ) , dateStr ( tomorrow ) , "Event" ) ] ) ;
636+ const result = CalendarFetcherUtils . preFilterICSText ( icsText , preFilterConfig ) ;
637+ expect ( result ) . toContain ( "BEGIN:VCALENDAR" ) ;
638+ expect ( result ) . toContain ( "VERSION:2.0" ) ;
639+ expect ( result ) . toContain ( "END:VCALENDAR" ) ;
640+ } ) ;
641+ } ) ;
642+
500643} ) ;
0 commit comments