From 4d54c2cfe80dad044bc2b1a6d3b534df4f7b4957 Mon Sep 17 00:00:00 2001 From: Tobias Bocanegra Date: Tue, 3 Dec 2019 12:17:14 +0900 Subject: [PATCH] fix(parser): abort expanding recurring events when end is reached fixes #34 --- lib/Calendar.js | 2 +- lib/xml/parser.js | 63 ++++++++++--------- test/Calendar.test.js | 34 ++++++++++ test/fixtures/eventsAllRecurring.response.xml | 49 +++++++++++++++ test/fixtures/index.js | 1 + 5 files changed, 120 insertions(+), 29 deletions(-) create mode 100644 test/fixtures/eventsAllRecurring.response.xml diff --git a/lib/Calendar.js b/lib/Calendar.js index a62477c..07c81e5 100644 --- a/lib/Calendar.js +++ b/lib/Calendar.js @@ -90,7 +90,7 @@ function createCalendar(request) { const xmlRequest = byTimeTemplate({ start, end }); return request(this.config, "REPORT", 1, xmlRequest) - .then(xmlParser.parseEvents) + .then(xml => xmlParser.parseEvents(xml, start, end)) .then(events => { return events.filter(event => { const isNotRecurring = !event.data.type.recurring; diff --git a/lib/xml/parser.js b/lib/xml/parser.js index f0a3861..ff25744 100644 --- a/lib/xml/parser.js +++ b/lib/xml/parser.js @@ -133,7 +133,7 @@ function getModifiedOccurenceEventData(vevent, eventData) { }; } -function parseEvents(xml) { +function parseEvents(xml, start, end) { let parsed; const formatted = []; @@ -141,6 +141,7 @@ function parseEvents(xml) { if (!result.multistatus || !result.multistatus.response) { return formatted; } + const iEnd = end ? ICAL.Time.fromDateTimeString(moment(end).toISOString()) : null; parsed = result.multistatus.response; @@ -183,42 +184,48 @@ function parseEvents(xml) { // Since there are infinite rules, its a good idea to limit the scope // of the iteration then resume later on - // Expand upto a maximum of 10 upcoming occurances - for (let i = 0; i < 10; i++) { + // Expand upto a maximum of 10 upcoming occurances or when end is reached + for (let i = 0; iEnd || i < 10; i++) { const nextOccuranceTime = expand.next(); - if (!expand.complete) { - // Handle this next expanded occurence - const nextEvent = icalEvent.getOccurrenceDetails(nextOccuranceTime); + if (expand.complete) { + break; + } + // Handle this next expanded occurence + const nextEvent = icalEvent.getOccurrenceDetails(nextOccuranceTime); + + // if event is past specified range, terminate expansion + if (iEnd && nextEvent.startDate.compare(iEnd) > 0) { + break; + } - eventData = {}; + eventData = {}; + + if (modifiedOccurences.length === 0) { + // No events have been modified + formatted.push({ + ics: event.href[0], + etag, + data: getNormalOccurenceEventData(nextEvent, eventData, vevent) + }); + } else { + // There are modified occurences + if (isModified(nextOccuranceTime, modifiedOccurences)) { + // This is the event that has been modied + const key = getModifiedOccuranceKey(nextOccuranceTime, modifiedOccurences) || 0; - if (modifiedOccurences.length === 0) { - // No events have been modified formatted.push({ ics: event.href[0], etag, - data: getNormalOccurenceEventData(nextEvent, eventData, vevent) + data: getModifiedOccurenceEventData(vevents[key], eventData) }); } else { - // There are modified occurences - if (isModified(nextOccuranceTime, modifiedOccurences)) { - // This is the event that has been modied - const key = getModifiedOccuranceKey(nextOccuranceTime, modifiedOccurences) || 0; - - formatted.push({ - ics: event.href[0], - etag, - data: getModifiedOccurenceEventData(vevents[key], eventData) - }); - } else { - // Expand this event normally - formatted.push({ - ics: event.href[0], - etag, - data: getNormalOccurenceEventData(nextEvent, eventData, vevent) - }); - } + // Expand this event normally + formatted.push({ + ics: event.href[0], + etag, + data: getNormalOccurenceEventData(nextEvent, eventData, vevent) + }); } } } diff --git a/test/Calendar.test.js b/test/Calendar.test.js index aab50bc..245d5a5 100644 --- a/test/Calendar.test.js +++ b/test/Calendar.test.js @@ -307,6 +307,40 @@ describe("Calendar", () => { }); }); + it("should return an array of objects with all events that occur between start and end dates including recurring", () => { + const response = fixtures.getRecurringEventsResponse; + const request = Promise.resolve(response); + const Calendar = createCalendar(() => request); + const calendar = new Calendar(config); + + return calendar + .getEventsByTime("20170201T000000Z", "20180201T000000Z") + .then((response) => { + expect(response).to.be.an("array"); + expect(response).to.have.lengthOf(51); + expect(response[0]).to.have.property("ics"); + expect(response[0]).to.have.property("etag"); + expect(response[0]).to.have.property("data"); + }); + }); + + it("should return an array of objects with all events that occur between start and end dates including recurring open ended", () => { + const response = fixtures.getRecurringEventsResponse; + const request = Promise.resolve(response); + const Calendar = createCalendar(() => request); + const calendar = new Calendar(config); + + return calendar + .getEventsByTime("20170201T000000Z") + .then((response) => { + expect(response).to.be.an("array"); + expect(response).to.have.lengthOf(10); + expect(response[0]).to.have.property("ics"); + expect(response[0]).to.have.property("etag"); + expect(response[0]).to.have.property("data"); + }); + }); + it("should return an array of objects with all upcoming events from today if start and end are left out", () => { const response = fixtures.getFutureEventsResponse; const request = Promise.resolve(response); diff --git a/test/fixtures/eventsAllRecurring.response.xml b/test/fixtures/eventsAllRecurring.response.xml new file mode 100644 index 0000000..57036f2 --- /dev/null +++ b/test/fixtures/eventsAllRecurring.response.xml @@ -0,0 +1,49 @@ + + + + + + /cal.php/calendars/antosan/default/event5.ics + + + BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VEVENT +CREATED:20170118T143300Z +LAST-MODIFIED:20170125T142648Z +DTSTAMP:20170125T142648Z +UID:lcghvo9rqntv7n18s72dbt7fds@google.com +SUMMARY:Repeating Event on Wednesdays +STATUS:CONFIRMED +RRULE:FREQ=WEEKLY;COUNT=100;BYDAY=WE +EXDATE;VALUE=DATE:20170222 +EXDATE;VALUE=DATE:20170208 +DTSTART;VALUE=DATE:20170201 +DTEND;VALUE=DATE:20170202 +DESCRIPTION:#Repeating +LOCATION:Am Silbermannpark\, 86161 Augsburg\, Germany +SEQUENCE:0 +TRANSP:TRANSPARENT +X-MOZ-GENERATION:2 +END:VEVENT +END:VCALENDAR + + "049d0b4228f3c4aaf8ff097a4bebc166" + + HTTP/1.1 200 OK + + + diff --git a/test/fixtures/index.js b/test/fixtures/index.js index ecfd47e..26e2327 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -10,6 +10,7 @@ const fixtures = { getEtagsNoNamespaceResponse: fs.readFileSync(path.join(__dirname, "/etagsNoNamespace.response.xml"), "utf8"), getEventsResponse: fs.readFileSync(path.join(__dirname, "/events.response.xml"), "utf8"), getAllEventsResponse: fs.readFileSync(path.join(__dirname, "/eventsAll.response.xml"), "utf8"), + getRecurringEventsResponse: fs.readFileSync(path.join(__dirname, "/eventsAllRecurring.response.xml"), "utf8"), getAllEventsNoNamespaceResponse: fs.readFileSync(path.join(__dirname, "/eventsAllNoNamespace.response.xml"), "utf8"), getEventsByTimeResponse: fs.readFileSync(path.join(__dirname, "/eventsByTime.response.xml"), "utf8"), getFutureEventsResponse: fs.readFileSync(path.join(__dirname, "/eventsFuture.response.xml"), "utf8")