From 6f5966a2b47ce0f1d033099072b540c95b5b2032 Mon Sep 17 00:00:00 2001 From: Georg Ehrke Date: Wed, 1 Apr 2020 09:39:35 +0200 Subject: [PATCH] Cleanup models and add full unit test coverage Signed-off-by: Georg Ehrke --- package.json | 3 +- src/fullcalendar/eventDrop.js | 11 +- src/fullcalendar/eventResize.js | 7 +- src/fullcalendar/eventSourceFunction.js | 3 +- src/mixins/EditorMixin.js | 11 +- src/models/alarm.js | 119 +++ src/models/attendee.js | 70 ++ src/models/calendar.js | 52 +- src/models/calendarObject.js | 298 ++---- src/models/calendarObjectInstance.js | 677 ------------- src/models/calendarShare.js | 92 ++ src/models/consts.js | 58 ++ src/models/contact.js | 8 +- src/models/event.js | 185 ++++ src/models/principal.js | 104 +- src/models/recurrenceRule.js | 503 ++++++++++ src/models/rfcProps.js | 6 +- src/models/schedulingObject.js | 187 ++++ src/store/calendarObjectInstance.js | 46 +- src/store/calendarObjects.js | 87 +- src/store/calendars.js | 8 +- src/store/index.js | 4 +- src/utils/calendarObject.js | 65 ++ tests/assets/ics/alarms/absoluteAlarm.ics | 4 + .../assets/ics/alarms/relativeAlarmAfter.ics | 4 + .../relativeAlarmAfterWithin24hours.ics | 4 + .../assets/ics/alarms/relativeAlarmBefore.ics | 4 + .../ics/alarms/relativeAlarmRelatedEnd.ics | 4 + .../ics/alarms/relativeAlarmWeekBefore.ics | 4 + tests/assets/ics/attendees/attendee1.ics | 1 + tests/assets/ics/attendees/attendee2.ics | 1 + tests/assets/ics/attendees/attendee3.ics | 1 + tests/assets/ics/attendees/attendee4.ics | 1 + tests/assets/ics/attendees/attendee5.ics | 1 + tests/assets/ics/attendees/attendee6.ics | 1 + tests/assets/ics/attendees/attendee7.ics | 1 + .../ics/rrules/rrule-count-and-until.ics | 1 + tests/assets/ics/rrules/rrule-count.ics | 1 + tests/assets/ics/rrules/rrule-until.ics | 1 + tests/assets/ics/rrules/rrules1.ics | 1 + tests/assets/ics/rrules/rrules10.ics | 1 + tests/assets/ics/rrules/rrules11.ics | 1 + tests/assets/ics/rrules/rrules12.ics | 1 + tests/assets/ics/rrules/rrules13.ics | 1 + tests/assets/ics/rrules/rrules14.ics | 1 + tests/assets/ics/rrules/rrules15.ics | 1 + tests/assets/ics/rrules/rrules16.ics | 1 + tests/assets/ics/rrules/rrules17.ics | 1 + tests/assets/ics/rrules/rrules18.ics | 1 + tests/assets/ics/rrules/rrules19.ics | 1 + tests/assets/ics/rrules/rrules2.ics | 1 + tests/assets/ics/rrules/rrules20.ics | 1 + tests/assets/ics/rrules/rrules21.ics | 1 + tests/assets/ics/rrules/rrules22.ics | 1 + tests/assets/ics/rrules/rrules23.ics | 1 + tests/assets/ics/rrules/rrules24.ics | 1 + tests/assets/ics/rrules/rrules25.ics | 1 + tests/assets/ics/rrules/rrules26.ics | 1 + tests/assets/ics/rrules/rrules27.ics | 1 + tests/assets/ics/rrules/rrules28.ics | 1 + tests/assets/ics/rrules/rrules29.ics | 1 + tests/assets/ics/rrules/rrules3.ics | 1 + tests/assets/ics/rrules/rrules30.ics | 1 + tests/assets/ics/rrules/rrules31.ics | 1 + tests/assets/ics/rrules/rrules32.ics | 1 + tests/assets/ics/rrules/rrules4.ics | 1 + tests/assets/ics/rrules/rrules5.ics | 1 + tests/assets/ics/rrules/rrules6.ics | 1 + tests/assets/ics/rrules/rrules7.ics | 1 + tests/assets/ics/rrules/rrules8.ics | 1 + tests/assets/ics/rrules/rrules9.ics | 1 + .../assets/ics/vcalendars-scheduling/add.ics | 18 + .../ics/vcalendars-scheduling/cancel.ics | 16 + .../ics/vcalendars-scheduling/counter.ics | 23 + .../vcalendars-scheduling/declinecounter.ics | 13 + .../vcalendars-scheduling/freebusy-reply.ics | 14 + .../freebusy-request.ics | 15 + .../ics/vcalendars-scheduling/publish.ics | 18 + .../ics/vcalendars-scheduling/refresh.ics | 14 + .../ics/vcalendars-scheduling/reply.ics | 13 + .../ics/vcalendars-scheduling/request.ics | 19 + .../assets/ics/vcalendars/vcalendar-empty.ics | 5 + .../ics/vcalendars/vcalendar-event-alarms.ics | 40 + .../ics/vcalendars/vcalendar-event-allday.ics | 15 + .../vcalendars/vcalendar-event-attendees.ics | 37 + .../vcalendars/vcalendar-event-categories.ics | 33 + .../vcalendar-event-custom-color.ics | 33 + .../vcalendar-event-floating-time.ics | 32 + .../vcalendar-event-multiple-rrules.ics | 34 + .../vcalendar-event-recurring-allday.ics | 16 + .../vcalendars/vcalendar-event-recurring.ics | 85 ++ .../ics/vcalendars/vcalendar-event-timed.ics | 32 + .../vcalendars/vcalendar-event-utc-time.ics | 32 + .../ics/vcalendars/vcalendar-journal.ics | 20 + .../assets/ics/vcalendars/vcalendar-todo.ics | 14 + .../vcalendars/vcalendar-without-vobjects.ics | 22 + tests/assets/loadAsset.js | 94 ++ .../unit/fullcalendar/eventDrop.test.js | 81 +- .../unit/fullcalendar/eventResize.test.js | 44 +- .../fullcalendar/eventSourceFunction.test.js | 27 +- tests/javascript/unit/models/alarm.test.js | 325 ++++++ tests/javascript/unit/models/attendee.test.js | 159 +++ tests/javascript/unit/models/calendar.test.js | 175 ++-- .../unit/models/calendarObject.test.js | 219 ++++- .../models/calendarObjectInstance.test.js | 29 - .../unit/models/calendarShare.test.js | 186 ++++ tests/javascript/unit/models/contact.test.js | 40 +- tests/javascript/unit/models/event.test.js | 863 ++++++++++++++++ .../javascript/unit/models/principal.test.js | 249 ++++- .../unit/models/recurrenceRule.test.js | 927 ++++++++++++++++++ tests/javascript/unit/models/rfcProps.test.js | 2 +- .../unit/models/schedulingObject.test.js | 681 +++++++++++++ tests/javascript/unit/utils/date.test.js | 6 +- 113 files changed, 6213 insertions(+), 1177 deletions(-) create mode 100644 src/models/alarm.js create mode 100644 src/models/attendee.js delete mode 100644 src/models/calendarObjectInstance.js create mode 100644 src/models/calendarShare.js create mode 100644 src/models/consts.js create mode 100644 src/models/event.js create mode 100644 src/models/recurrenceRule.js create mode 100644 src/models/schedulingObject.js create mode 100644 src/utils/calendarObject.js create mode 100644 tests/assets/ics/alarms/absoluteAlarm.ics create mode 100644 tests/assets/ics/alarms/relativeAlarmAfter.ics create mode 100644 tests/assets/ics/alarms/relativeAlarmAfterWithin24hours.ics create mode 100644 tests/assets/ics/alarms/relativeAlarmBefore.ics create mode 100644 tests/assets/ics/alarms/relativeAlarmRelatedEnd.ics create mode 100644 tests/assets/ics/alarms/relativeAlarmWeekBefore.ics create mode 100644 tests/assets/ics/attendees/attendee1.ics create mode 100644 tests/assets/ics/attendees/attendee2.ics create mode 100644 tests/assets/ics/attendees/attendee3.ics create mode 100644 tests/assets/ics/attendees/attendee4.ics create mode 100644 tests/assets/ics/attendees/attendee5.ics create mode 100644 tests/assets/ics/attendees/attendee6.ics create mode 100644 tests/assets/ics/attendees/attendee7.ics create mode 100644 tests/assets/ics/rrules/rrule-count-and-until.ics create mode 100644 tests/assets/ics/rrules/rrule-count.ics create mode 100644 tests/assets/ics/rrules/rrule-until.ics create mode 100644 tests/assets/ics/rrules/rrules1.ics create mode 100644 tests/assets/ics/rrules/rrules10.ics create mode 100644 tests/assets/ics/rrules/rrules11.ics create mode 100644 tests/assets/ics/rrules/rrules12.ics create mode 100644 tests/assets/ics/rrules/rrules13.ics create mode 100644 tests/assets/ics/rrules/rrules14.ics create mode 100644 tests/assets/ics/rrules/rrules15.ics create mode 100644 tests/assets/ics/rrules/rrules16.ics create mode 100644 tests/assets/ics/rrules/rrules17.ics create mode 100644 tests/assets/ics/rrules/rrules18.ics create mode 100644 tests/assets/ics/rrules/rrules19.ics create mode 100644 tests/assets/ics/rrules/rrules2.ics create mode 100644 tests/assets/ics/rrules/rrules20.ics create mode 100644 tests/assets/ics/rrules/rrules21.ics create mode 100644 tests/assets/ics/rrules/rrules22.ics create mode 100644 tests/assets/ics/rrules/rrules23.ics create mode 100644 tests/assets/ics/rrules/rrules24.ics create mode 100644 tests/assets/ics/rrules/rrules25.ics create mode 100644 tests/assets/ics/rrules/rrules26.ics create mode 100644 tests/assets/ics/rrules/rrules27.ics create mode 100644 tests/assets/ics/rrules/rrules28.ics create mode 100644 tests/assets/ics/rrules/rrules29.ics create mode 100644 tests/assets/ics/rrules/rrules3.ics create mode 100644 tests/assets/ics/rrules/rrules30.ics create mode 100644 tests/assets/ics/rrules/rrules31.ics create mode 100644 tests/assets/ics/rrules/rrules32.ics create mode 100644 tests/assets/ics/rrules/rrules4.ics create mode 100644 tests/assets/ics/rrules/rrules5.ics create mode 100644 tests/assets/ics/rrules/rrules6.ics create mode 100644 tests/assets/ics/rrules/rrules7.ics create mode 100644 tests/assets/ics/rrules/rrules8.ics create mode 100644 tests/assets/ics/rrules/rrules9.ics create mode 100644 tests/assets/ics/vcalendars-scheduling/add.ics create mode 100644 tests/assets/ics/vcalendars-scheduling/cancel.ics create mode 100644 tests/assets/ics/vcalendars-scheduling/counter.ics create mode 100644 tests/assets/ics/vcalendars-scheduling/declinecounter.ics create mode 100644 tests/assets/ics/vcalendars-scheduling/freebusy-reply.ics create mode 100644 tests/assets/ics/vcalendars-scheduling/freebusy-request.ics create mode 100644 tests/assets/ics/vcalendars-scheduling/publish.ics create mode 100644 tests/assets/ics/vcalendars-scheduling/refresh.ics create mode 100644 tests/assets/ics/vcalendars-scheduling/reply.ics create mode 100644 tests/assets/ics/vcalendars-scheduling/request.ics create mode 100644 tests/assets/ics/vcalendars/vcalendar-empty.ics create mode 100644 tests/assets/ics/vcalendars/vcalendar-event-alarms.ics create mode 100644 tests/assets/ics/vcalendars/vcalendar-event-allday.ics create mode 100644 tests/assets/ics/vcalendars/vcalendar-event-attendees.ics create mode 100644 tests/assets/ics/vcalendars/vcalendar-event-categories.ics create mode 100644 tests/assets/ics/vcalendars/vcalendar-event-custom-color.ics create mode 100644 tests/assets/ics/vcalendars/vcalendar-event-floating-time.ics create mode 100644 tests/assets/ics/vcalendars/vcalendar-event-multiple-rrules.ics create mode 100644 tests/assets/ics/vcalendars/vcalendar-event-recurring-allday.ics create mode 100644 tests/assets/ics/vcalendars/vcalendar-event-recurring.ics create mode 100644 tests/assets/ics/vcalendars/vcalendar-event-timed.ics create mode 100644 tests/assets/ics/vcalendars/vcalendar-event-utc-time.ics create mode 100644 tests/assets/ics/vcalendars/vcalendar-journal.ics create mode 100644 tests/assets/ics/vcalendars/vcalendar-todo.ics create mode 100644 tests/assets/ics/vcalendars/vcalendar-without-vobjects.ics create mode 100644 tests/assets/loadAsset.js create mode 100644 tests/javascript/unit/models/alarm.test.js create mode 100644 tests/javascript/unit/models/attendee.test.js delete mode 100644 tests/javascript/unit/models/calendarObjectInstance.test.js create mode 100644 tests/javascript/unit/models/calendarShare.test.js create mode 100644 tests/javascript/unit/models/event.test.js create mode 100644 tests/javascript/unit/models/recurrenceRule.test.js create mode 100644 tests/javascript/unit/models/schedulingObject.test.js diff --git a/package.json b/package.json index 3a37b28e8..df9d78b24 100644 --- a/package.json +++ b/package.json @@ -152,7 +152,8 @@ "/node_modules/(?!calendar-js).+\\.js$" ], "setupFilesAfterEnv": [ - "./tests/javascript/jest.setup.js" + "./tests/javascript/jest.setup.js", + "./tests/assets/loadAsset.js" ] } } diff --git a/src/fullcalendar/eventDrop.js b/src/fullcalendar/eventDrop.js index 693babf11..b6169c900 100644 --- a/src/fullcalendar/eventDrop.js +++ b/src/fullcalendar/eventDrop.js @@ -22,6 +22,7 @@ import { getDurationValueFromFullCalendarDuration } from '../fullcalendar/duration' import getTimezoneManager from '../services/timezoneDataProviderService' import logger from '../utils/logger.js' +import { getObjectAtRecurrenceId } from '../utils/calendarObject.js' /** * Returns a function to drop an event at a different position @@ -60,7 +61,7 @@ export default function(store, fcAPI) { return } - const eventComponent = calendarObject.getObjectAtRecurrenceId(recurrenceIdDate) + const eventComponent = getObjectAtRecurrenceId(calendarObject, recurrenceIdDate) if (!eventComponent) { console.debug('Recurrence-id not found') revert() @@ -71,7 +72,9 @@ export default function(store, fcAPI) { // shiftByDuration may throw exceptions in certain cases eventComponent.shiftByDuration(deltaDuration, event.allDay, timezone, defaultAllDayDuration, defaultTimedDuration) } catch (error) { - calendarObject.resetToDav() + store.commit('resetCalendarObjectToDav', { + calendarObject, + }) console.debug(error) revert() return @@ -86,7 +89,9 @@ export default function(store, fcAPI) { calendarObject, }) } catch (error) { - calendarObject.resetToDav() + store.commit('resetCalendarObjectToDav', { + calendarObject, + }) console.debug(error) revert() } diff --git a/src/fullcalendar/eventResize.js b/src/fullcalendar/eventResize.js index 26ec69b08..e97e71451 100644 --- a/src/fullcalendar/eventResize.js +++ b/src/fullcalendar/eventResize.js @@ -20,6 +20,7 @@ * */ import { getDurationValueFromFullCalendarDuration } from './duration' +import { getObjectAtRecurrenceId } from '../utils/calendarObject.js' /** * Returns a function to resize an event @@ -50,7 +51,7 @@ export default function(store) { return } - const eventComponent = calendarObject.getObjectAtRecurrenceId(recurrenceIdDate) + const eventComponent = getObjectAtRecurrenceId(calendarObject, recurrenceIdDate) if (!eventComponent) { console.debug('Recurrence-id not found') revert() @@ -73,7 +74,9 @@ export default function(store) { calendarObject, }) } catch (error) { - calendarObject.resetToDav() + store.commit('resetCalendarObjectToDav', { + calendarObject, + }) console.debug(error) revert() } diff --git a/src/fullcalendar/eventSourceFunction.js b/src/fullcalendar/eventSourceFunction.js index 499a31195..30666df02 100644 --- a/src/fullcalendar/eventSourceFunction.js +++ b/src/fullcalendar/eventSourceFunction.js @@ -27,6 +27,7 @@ import { getHexForColorName, } from '../utils/color.js' import logger from '../utils/logger.js' +import { getAllObjectsInTimeRange } from '../utils/calendarObject.js' /** * convert an array of calendar-objects to events @@ -43,7 +44,7 @@ export function eventSourceFunction(calendarObjects, calendar, start, end, timez for (const calendarObject of calendarObjects) { let allObjectsInTimeRange try { - allObjectsInTimeRange = calendarObject.getAllObjectsInTimeRange(start, end) + allObjectsInTimeRange = getAllObjectsInTimeRange(calendarObject, start, end) } catch (error) { logger.error(error.message) continue diff --git a/src/mixins/EditorMixin.js b/src/mixins/EditorMixin.js index e617b3c4f..db5c628c1 100644 --- a/src/mixins/EditorMixin.js +++ b/src/mixins/EditorMixin.js @@ -300,7 +300,7 @@ export default { return false } - return this.calendarObject.existsOnServer() + return this.calendarObject.existsOnServer }, /** * Returns whether or not the user is allowed to create recurrence exceptions for this event @@ -336,7 +336,7 @@ export default { return false } - return this.calendarObject.existsOnServer() + return this.calendarObject.existsOnServer }, /** * Returns the download url as a string or null if event is loading or does not exist on the server (yet) @@ -385,7 +385,7 @@ export default { // override the internally stored calendarId. If we did not do this, // it would create the event in the default calendar first and move it // to the desired calendar as a second step. - if (this.calendarObject && !this.calendarObject.existsOnServer()) { + if (this.calendarObject && !this.calendarObject.existsOnServer) { this.calendarObject.calendarId = selectedCalendar.id } }, @@ -423,7 +423,10 @@ export default { return } - await this.$store.dispatch('resetCalendarObjectInstance') + this.$store.commit('resetCalendarObjectToDav', { + calendarObject: this.calendarObject, + }) + this.requiresActionOnRouteLeave = false this.closeEditor() }, diff --git a/src/models/alarm.js b/src/models/alarm.js new file mode 100644 index 000000000..594aa1459 --- /dev/null +++ b/src/models/alarm.js @@ -0,0 +1,119 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +import { + getAmountAndUnitForTimedEvents, + getAmountHoursMinutesAndUnitForAllDayEvents, +} from '../utils/alarms.js' +import { getDateFromDateTimeValue } from '../utils/date.js' + +/** + * Creates a complete alarm object based on given props + * + * @param {Object} props The alarm properties already provided + * @returns {Object} + */ +const getDefaultAlarmObject = (props = {}) => Object.assign({}, { + // The calendar-js alarm component + alarmComponent: null, + // Type of alarm: DISPLAY, EMAIL, AUDIO + type: null, + // Whether or not the alarm is relative + isRelative: false, + // Date object of an absolute alarm (if it's absolute, it must be DATE-TIME) + absoluteDate: null, + // Whether or not the relative alarm is before the event, + relativeIsBefore: null, + // Whether or not the alarm is relative to the event's start + relativeIsRelatedToStart: null, + // TIMED EVENTS: + // Unit (seconds, minutes, hours, ...) if this alarm is inside a timed event + relativeUnitTimed: null, + // The amount of unit if this alarm is inside a timed event + relativeAmountTimed: null, + // ALL-DAY EVENTS: + // Unit (seconds, minutes, hours, ...) if this alarm is inside an all-day event + relativeUnitAllDay: null, + // The amount of unit if this alarm is inside a all-day event + relativeAmountAllDay: null, + // The hours to display alarm for in an all-day event (e.g. 1 day before at 9:00 am) + relativeHoursAllDay: null, + // The minutes to display alarm for in an all-day event (e.g. 1 day before at 9:30 am) + relativeMinutesAllDay: null, + // The total amount of seconds for a relative alarm + relativeTrigger: null, +}, props) + +/** + * Map an alarm component to our alarm object + * + * @param {AlarmComponent} alarmComponent The calendar-js alarm-component to turn into an alarm object + * @returns {Object} + */ +const mapAlarmComponentToAlarmObject = (alarmComponent) => { + if (alarmComponent.trigger.isRelative()) { + const relativeIsBefore = alarmComponent.trigger.value.isNegative + const relativeIsRelatedToStart = alarmComponent.trigger.related === 'START' + + const { + amount: relativeAmountTimed, + unit: relativeUnitTimed, + } = getAmountAndUnitForTimedEvents(alarmComponent.trigger.value.totalSeconds) + + const { + unit: relativeUnitAllDay, + amount: relativeAmountAllDay, + hours: relativeHoursAllDay, + minutes: relativeMinutesAllDay, + } = getAmountHoursMinutesAndUnitForAllDayEvents(alarmComponent.trigger.value.totalSeconds) + + const relativeTrigger = alarmComponent.trigger.value.totalSeconds + + return getDefaultAlarmObject({ + alarmComponent, + type: alarmComponent.action, + isRelative: alarmComponent.trigger.isRelative(), + relativeIsBefore, + relativeIsRelatedToStart, + relativeUnitTimed, + relativeAmountTimed, + relativeUnitAllDay, + relativeAmountAllDay, + relativeHoursAllDay, + relativeMinutesAllDay, + relativeTrigger, + }) + } else { + const absoluteDate = getDateFromDateTimeValue(alarmComponent.trigger.value) + + return getDefaultAlarmObject({ + alarmComponent, + type: alarmComponent.action, + isRelative: alarmComponent.trigger.isRelative(), + absoluteDate, + }) + } +} + +export { + getDefaultAlarmObject, + mapAlarmComponentToAlarmObject, +} diff --git a/src/models/attendee.js b/src/models/attendee.js new file mode 100644 index 000000000..64567e938 --- /dev/null +++ b/src/models/attendee.js @@ -0,0 +1,70 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +/** + * Creates a complete attendee object based on given props + * + * TODO: + * - we should eventually support delegatedFrom and delegatedTo + * + * @param {Object} props The attendee properties already provided + * @returns {Object} + */ +const getDefaultAttendeeObject = (props = {}) => Object.assign({}, { + // The calendar-js attendee property + attendeeProperty: null, + // The display-name of the attendee + commonName: null, + // The calendar-user-type of the attendee + calendarUserType: 'INDIVIDUAL', + // The participation status of the attendee + participationStatus: 'NEEDS-ACTION', + // The role of the attendee + role: 'REQ-PARTICIPANT', + // The RSVP for the attendee + rsvp: false, + // The uri of the attendee + uri: null, +}, props) + +/** + * Maps a calendar-js attendee property to our attendee object + * + * @param {AttendeeProperty} attendeeProperty The calendar-js attendeeProperty to turn into a attendee object + * @returns {Object} + */ +const mapAttendeePropertyToAttendeeObject = (attendeeProperty) => { + return getDefaultAttendeeObject({ + attendeeProperty, + commonName: attendeeProperty.commonName, + calendarUserType: attendeeProperty.userType, + participationStatus: attendeeProperty.participationStatus, + role: attendeeProperty.role, + rsvp: attendeeProperty.rsvp, + uri: attendeeProperty.email, + }) +} + +export { + getDefaultAttendeeObject, + mapAttendeePropertyToAttendeeObject, +} diff --git a/src/models/calendar.js b/src/models/calendar.js index 0ba528b3c..435e275fb 100644 --- a/src/models/calendar.js +++ b/src/models/calendar.js @@ -20,6 +20,7 @@ * */ import { detectColor, uidToHexColor } from '../utils/color.js' +import { mapDavShareeToCalendarShareObject } from './calendarShare.js' /** * Creates a complete calendar-object based on given props @@ -27,7 +28,7 @@ import { detectColor, uidToHexColor } from '../utils/color.js' * @param {Object} props Calendar-props already provided * @returns {Object} */ -export const getDefaultCalendarObject = (props = {}) => Object.assign({}, { +const getDefaultCalendarObject = (props = {}) => Object.assign({}, { // Id of the calendar id: '', // Visible display name @@ -79,7 +80,7 @@ export const getDefaultCalendarObject = (props = {}) => Object.assign({}, { * @param {Object=} currentUserPrincipal The principal model of the current user principal * @returns {Object} */ -export function mapDavCollectionToCalendar(calendar, currentUserPrincipal) { +const mapDavCollectionToCalendar = (calendar, currentUserPrincipal) => { const id = btoa(calendar.url) const displayName = calendar.displayname || getCalendarUriFromUrl(calendar.url) @@ -134,11 +135,11 @@ export function mapDavCollectionToCalendar(calendar, currentUserPrincipal) { continue } - shares.push(mapDavShareeToSharee(share)) + shares.push(mapDavShareeToCalendarShareObject(share)) } } - return { + return getDefaultCalendarObject({ id, displayName, color, @@ -157,43 +158,7 @@ export function mapDavCollectionToCalendar(calendar, currentUserPrincipal) { shares, timezone, dav: calendar, - } -} - -/** - * Map a dav collection to our calendar object model - * - * @param {Object} sharee The sharee object from the cdav library shares - * @returns {Object} - */ -export function mapDavShareeToSharee(sharee) { - // sharee.href might contain non-latin characters, so let's uri encode it first - const id = btoa(encodeURI(sharee.href)) - - let displayName - if (sharee['common-name']) { - displayName = sharee['common-name'] - } else if (sharee.href.startsWith('principal:principals/groups/')) { - displayName = sharee.href.substr(28) - } else if (sharee.href.startsWith('principal:principals/users/')) { - displayName = sharee.href.substr(27) - } else { - displayName = sharee.href - } - - const writeable = sharee.access[0].endsWith('read-write') - const isGroup = sharee.href.indexOf('principal:principals/groups/') === 0 - const isCircle = sharee.href.indexOf('principal:principals/circles/') === 0 - const uri = sharee.href - - return { - id, - displayName, - writeable, - isGroup, - isCircle, - uri, - } + }) } /** @@ -209,3 +174,8 @@ function getCalendarUriFromUrl(url) { return url.substring(url.lastIndexOf('/') + 1) } + +export { + getDefaultCalendarObject, + mapDavCollectionToCalendar, +} diff --git a/src/models/calendarObject.js b/src/models/calendarObject.js index e8e925868..dbc25d354 100644 --- a/src/models/calendarObject.js +++ b/src/models/calendarObject.js @@ -19,218 +19,112 @@ * along with this program. If not, see . * */ - import { getParserManager } from 'calendar-js' -import DateTimeValue from 'calendar-js/src/values/dateTimeValue' -import CalendarComponent from 'calendar-js/src/components/calendarComponent' +import { + COMPONENT_NAME_EVENT, + COMPONENT_NAME_JOURNAL, + COMPONENT_NAME_VTODO, +} from './consts.js' /** - * This model represents exactly + * Creates a complete calendar-object-object based on given props * - * TODO: this should not be a class, but a simple object - * TODO: all methods should be converted to vuex commits + * @param {Object} props Calendar-object-props already provided + * @returns {Object} */ -export default class CalendarObject { +const getDefaultCalendarObjectObject = (props = {}) => Object.assign({}, { + // Id of this calendar-object + id: null, + // Id of the associated calendar + calendarId: null, + // The cdav-library object storing the calendar-object + dav: null, + // The parsed calendar-js object + calendarComponent: null, + // The uid of the calendar-object + uid: null, + // The uri of the calendar-object + uri: null, + // The type of calendar-object + objectType: null, + // Whether or not the calendar-object is an event + isEvent: false, + // Whether or not the calendar-object is a journal + isJournal: false, + // Whether or not the calendar-object is a task + isTodo: false, + // Whether or not the calendar-object exists on the server + existsOnServer: false, +}, props) - /** - * Constructor of calendar-object - * - * @param {String|CalendarComponent} calendarData The raw unparsed calendar-data - * @param {String} calendarId Id of the calendar this calendar-object belongs to - * @param {VObject} dav The dav object - */ - constructor(calendarData, calendarId, dav = null) { - /** - * Id of the calendar this calendar-object is part of - * - * @type {String} - */ - this.calendarId = calendarId +/** + * Maps a calendar-object from c-dav to our calendar-object object + * + * @param {VObject} dav The c-dav VObject + * @param {String} calendarId The calendar-id this object is associated with + * @returns {Object} + */ +const mapCDavObjectToCalendarObject = (dav, calendarId) => { + const parserManager = getParserManager() + const parser = parserManager.getParserForFileType('text/calendar') - /** - * Whether or not there has been a conflict with the server version - * - * @type {boolean} - */ - this.conflict = false - - /** - * The dav-object associated with this calendar-object - * - * @type {Object}|null - */ - this.dav = dav - - /** - * parsed calendar-js object - * @type {CalendarComponent} - */ - this.vcalendar = null - this.resetToDav(calendarData) + // This should not be the case, but let's just be on the safe side + if (typeof dav.data !== 'string' || dav.data.trim() === '') { + throw new Error('Empty calendar object') } - /** - * ID of the calendar-object - * - * @returns {string} - */ - get id() { - if (this.dav) { - return btoa(this.dav.url) - } - - return 'new' + parser.parse(dav.data) + const calendarComponentIterator = parser.getItemIterator() + const calendarComponent = calendarComponentIterator.next().value + if (!calendarComponent) { + throw new Error('Empty calendar object') } - /** - * @returns {string} - */ - getId() { - if (this.dav) { - return btoa(this.dav.url) - } - - return 'new' - } - - /** - * UID of the calendar-object - * - * @returns {null|String} - */ - get uid() { - const iterator = this.vcalendar.getVObjectIterator() - const firstVObject = iterator.next().value - if (firstVObject) { - return firstVObject.uid - } - - return null - } - - /** - * Type of the calendar-object - * - * @returns {null|String} - */ - get objectType() { - const iterator = this.vcalendar.getVObjectIterator() - const firstVObject = iterator.next().value - if (firstVObject) { - return firstVObject.name - } - - return null - } - - /** - * Whether or not this calendar-object is an event - * - * @returns {boolean} - */ - isEvent() { - return this.objectType === 'vevent' - } - - /** - * Whether or not this calendar-object is a task - * - * @returns {boolean} - */ - isTodo() { - return this.objectType === 'vtodo' - } - - /** - * Get all recurrence-items in given range - * - * @param {Date} start Begin of time-range - * @param {Date} end End of time-range - * @returns {Array} - */ - getAllObjectsInTimeRange(start, end) { - const iterator = this.vcalendar.getVObjectIterator() - const firstVObject = iterator.next().value - if (!firstVObject) { - return [] - } - - const s = DateTimeValue.fromJSDate(start, true) - const e = DateTimeValue.fromJSDate(end, true) - return firstVObject.recurrenceManager.getAllOccurrencesBetween(s, e) - } - - /** - * Get recurrence-item closest to the given recurrence-Id - * This is either the one next in the future or if none exist, - * the one closest in the past - * - * @param {Date} closeTo The time to get the - * @returns {AbstractRecurringComponent|null} - */ - getClosestRecurrence(closeTo) { - const iterator = this.vcalendar.getVObjectIterator() - const firstVObject = iterator.next().value - if (!firstVObject) { - return null - } - - const d = DateTimeValue.fromJSDate(closeTo, true) - return firstVObject.recurrenceManager.getClosestOccurrence(d) - } - - /** - * Get recurrence-item at exactly a given recurrence-Id - * - * @param {Date} recurrenceId RecurrenceId to retrieve - * @returns {AbstractRecurringComponent|null} - */ - getObjectAtRecurrenceId(recurrenceId) { - const iterator = this.vcalendar.getVObjectIterator() - const firstVObject = iterator.next().value - if (!firstVObject) { - return null - } - - const d = DateTimeValue.fromJSDate(recurrenceId, true) - return firstVObject.recurrenceManager.getOccurrenceAtExactly(d) - } - - /** - * resets the inter vcalendar to the dav data - * - * @param {CalendarComponent|String} data Data to reset to - */ - resetToDav(data = null) { - if (data instanceof CalendarComponent) { - this.vcalendar = data - return - } - - if (data === null && this.dav === null) { - return - } - - const parserManager = getParserManager() - const parser = parserManager.getParserForFileType('text/calendar') - - const calendarData = data || this.dav.data - parser.parse(calendarData) - - const itemIterator = parser.getItemIterator() - const firstVCalendar = itemIterator.next().value - if (firstVCalendar) { - this.vcalendar = firstVCalendar - } - } - - /** - * Whether or not this objects exists on the server - * - * @returns {boolean} - */ - existsOnServer() { - return !!this.dav - } + const vObjectIterator = calendarComponent.getVObjectIterator() + const firstVObject = vObjectIterator.next().value + return getDefaultCalendarObjectObject({ + id: btoa(dav.url), + calendarId, + dav, + calendarComponent, + uid: firstVObject.uid, + uri: dav.url, + objectType: firstVObject.name, + isEvent: firstVObject.name === COMPONENT_NAME_EVENT, + isJournal: firstVObject.name === COMPONENT_NAME_JOURNAL, + isTodo: firstVObject.name === COMPONENT_NAME_VTODO, + existsOnServer: true, + }) +} + +/** + * Maps a calendar-component from calendar-js to our calendar-object object + * + * @param {CalendarComponent} calendarComponent The calendarComponent to create the calendarObject from + * @param {String=} calendarId The associated calendar if applicable + * @returns {Object} + */ +const mapCalendarJsToCalendarObject = (calendarComponent, calendarId = null) => { + const vObjectIterator = calendarComponent.getVObjectIterator() + const firstVObject = vObjectIterator.next().value + if (!firstVObject) { + throw new Error('Calendar object without vobjects') + } + + return getDefaultCalendarObjectObject({ + calendarId, + calendarComponent, + uid: firstVObject.uid, + objectType: firstVObject.name, + isEvent: firstVObject.name === COMPONENT_NAME_EVENT, + isJournal: firstVObject.name === COMPONENT_NAME_JOURNAL, + isTodo: firstVObject.name === COMPONENT_NAME_VTODO, + }) +} + +export { + getDefaultCalendarObjectObject, + mapCDavObjectToCalendarObject, + mapCalendarJsToCalendarObject, } diff --git a/src/models/calendarObjectInstance.js b/src/models/calendarObjectInstance.js deleted file mode 100644 index 3e7f77649..000000000 --- a/src/models/calendarObjectInstance.js +++ /dev/null @@ -1,677 +0,0 @@ -/** - * @copyright Copyright (c) 2019 Georg Ehrke - * - * @author Georg Ehrke - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ -import { getDateFromDateTimeValue } from '../utils/date.js' -import DurationValue from 'calendar-js/src/values/durationValue.js' -import { getWeekDayFromDate } from '../utils/recurrence.js' -import { getAmountAndUnitForTimedEvents, getAmountHoursMinutesAndUnitForAllDayEvents } from '../utils/alarms.js' -import { getHexForColorName } from '../utils/color.js' - -/** - * Creates a complete calendar-object-instance-object based on given props - * - * @param {Object} props The props already provided - * @returns {Object} - */ -export const getDefaultCalendarObjectInstanceObject = (props = {}) => Object.assign({}, { - // Title of the event - title: null, - // Start date of the event - startDate: null, - // Timezone of the start date - startTimezoneId: null, - // End date of the event - endDate: null, - // Timezone of the end date - endTimezoneId: null, - // Indicator whether or not event is all-day - isAllDay: false, - // Location that the event takes places in - location: null, - // description of the event - description: null, - // Access class of the event (PUBLIC, PRIVATE, CONFIDENTIAL) - accessClass: null, - // Status of the event (CONFIRMED, TENTATIVE, CANCELLED) - status: null, - // Whether or not to block this event in Free-Busy reports (TRANSPARENT, OPAQUE) - timeTransparency: null, - // The recurrence rule of this event. We only support one recurrence-rule - recurrenceRule: { - frequency: 'NONE', - interval: 1, - count: null, - until: null, - byDay: [], - byMonth: [], - byMonthDay: [], - bySetPosition: null, - isUnsupported: false, - recurrenceRuleValue: null, - }, - // Attendees of this event - attendees: [], - // Organizer of the event - organizer: { - // name of the organizer - name: null, - // email of the organizer - uri: null, - }, - // Alarm of the event - alarms: [], - // Custom color of the event - customColor: null, - // Categories - categories: [], - // Whether or not the user is allowed to toggle the all-day checkbox - canModifyAllDay: true, - // The real event-component coming from calendar-js - eventComponent: null, -}, props) - -/** - * Map an EventComponent from calendar-js to our calendar object instance object - * - * @param {EventComponent} eventComponent The EventComponent object to map to an object - * @returns {{color: *, canModifyAllDay: *, timeTransparency: *, description: *, location: *, eventComponent: *, title: *, accessClass: *, status: *}} - */ -export const mapEventComponentToCalendarObjectInstanceObject = (eventComponent) => { - const calendarObjectInstanceObject = { - title: eventComponent.title, - location: eventComponent.location, - description: eventComponent.description, - accessClass: eventComponent.accessClass, - status: eventComponent.status, - timeTransparency: eventComponent.timeTransparency, - color: eventComponent.color, - canModifyAllDay: eventComponent.canModifyAllDay(), - eventComponent, - } - - // The end date of an event is non-inclusive. This is rather intuitive for timed-events, but very unintuitive for all-day events. - // That's why, when an events is from 2019-10-03 to 2019-10-04, we will show 2019-10-03 to 2019-10-03 in the editor. - calendarObjectInstanceObject.isAllDay = eventComponent.isAllDay() - calendarObjectInstanceObject.startDate = getDateFromDateTimeValue(eventComponent.startDate) - calendarObjectInstanceObject.startTimezoneId = eventComponent.startDate.timezoneId - - if (eventComponent.isAllDay()) { - const endDate = eventComponent.endDate.clone() - endDate.addDuration(DurationValue.fromSeconds(-1 * 60 * 60 * 24)) - calendarObjectInstanceObject.endDate = getDateFromDateTimeValue(endDate) - } else { - calendarObjectInstanceObject.endDate = getDateFromDateTimeValue(eventComponent.endDate) - } - calendarObjectInstanceObject.endTimezoneId = eventComponent.endDate.timezoneId - - calendarObjectInstanceObject.categories = getCategoriesFromEventComponent(eventComponent) - calendarObjectInstanceObject.organizer = getOrganizerFromEventComponent(eventComponent) - calendarObjectInstanceObject.recurrenceRule = getRecurrenceRuleFromEventComponent(eventComponent) - calendarObjectInstanceObject.hasMultipleRecurrenceRules - = Array.from(eventComponent.getPropertyIterator('RRULE')).length > 1 - calendarObjectInstanceObject.attendees = getAttendeesFromEventComponent(eventComponent) - calendarObjectInstanceObject.alarms = getAlarmsFromEventComponent(eventComponent) - - if (eventComponent.hasProperty('COLOR')) { - const hexColor = getHexForColorName(eventComponent.getFirstPropertyFirstValue('COLOR')) - if (hexColor !== null) { - calendarObjectInstanceObject.customColor = hexColor - } - } - - return calendarObjectInstanceObject -} - -/** - * Gets the organizer from the event component - * - * @param {EventComponent} eventComponent The event-component representing the instance - * @returns {null|{commonName: *, uri: *}} - */ -function getOrganizerFromEventComponent(eventComponent) { - if (eventComponent.organizer) { - const organizerProperty = eventComponent.getFirstProperty('ORGANIZER') - return { - commonName: organizerProperty.commonName, - uri: organizerProperty.email, - attendeeProperty: organizerProperty, - } - } - - return null -} - -/** - * Gets all categories (without a language tag) from the event component - * - * @param {EventComponent} eventComponent The event-component representing the instance - * @returns {String[]} - */ -function getCategoriesFromEventComponent(eventComponent) { - return Array.from(eventComponent.getCategoryIterator()) -} - -/** - * Gets the first recurrence rule from the event component - * - * @param {EventComponent} eventComponent The event-component representing the instance - * @returns {{byMonth: [], frequency: null, count: null, byDay: [], interval: number, until: null, bySetPosition: null, byMonthDay: []}|{byMonth: *, frequency: *, count: *, byDay: *, interval: *, until: *, bySetPosition: *, byMonthDay: *}} - */ -function getRecurrenceRuleFromEventComponent(eventComponent) { - /** @type {RecurValue} */ - const recurrenceRule = eventComponent.getFirstPropertyFirstValue('RRULE') - if (recurrenceRule) { - const component = { - frequency: recurrenceRule.frequency, - interval: parseInt(recurrenceRule.interval, 10) || 1, - count: recurrenceRule.count, - until: null, - byDay: [], - byMonth: [], - byMonthDay: [], - bySetPosition: null, - isUnsupported: false, - recurrenceRuleValue: recurrenceRule, - } - - if (recurrenceRule.until) { - component.until = recurrenceRule.until.jsDate - } - - switch (component.frequency) { - case 'DAILY': - getRecurrenceComponentFromDailyRule(recurrenceRule, component) - break - - case 'WEEKLY': - getRecurrenceComponentFromWeeklyRule(recurrenceRule, component, eventComponent) - break - - case 'MONTHLY': - getRecurrenceComponentFromMonthlyRule(recurrenceRule, component, eventComponent) - break - - case 'YEARLY': - getRecurrenceComponentFromYearlyRule(recurrenceRule, component, eventComponent) - break - - default: - component.isUnsupported = true - break - } - - return component - } - - return { - frequency: 'NONE', - interval: 1, - count: null, - until: null, - byDay: [], - byMonth: [], - byMonthDay: [], - bySetPosition: null, - isUnsupported: false, - recurrenceRuleValue: null, - } -} - -/** - * Checks if the recurrence-rule contains any of the given components - * - * @param {RecurValue} recurrenceRule The recurrence-rule value to check for the given components - * @param {String[]} components List of components to check for - * @returns {Boolean} - */ -function containsRecurrenceComponent(recurrenceRule, components) { - for (const component of components) { - const componentValue = recurrenceRule.getComponent(component) - if (componentValue.length > 0) { - return true - } - } - - return false -} - -/** - * Gets all attendees from the event component - * - * @param {EventComponent} eventComponent The event-component representing the instance - * @returns {[]} - */ -function getAttendeesFromEventComponent(eventComponent) { - const attendees = [] - - for (const attendee of eventComponent.getAttendeeIterator()) { - attendees.push({ - commonName: attendee.commonName, - participationStatus: attendee.participationStatus, - role: attendee.role, - rsvp: attendee.rsvp, - uri: attendee.email, - attendeeProperty: attendee, - }) - } - - return attendees -} - -/** - * Get all alarms from the event Component - * - * @param {EventComponent} eventComponent The event-component representing the instance - * @returns {[]} - */ -function getAlarmsFromEventComponent(eventComponent) { - const alarms = [] - - for (const alarm of eventComponent.getAlarmIterator()) { - alarms.push(getAlarmFromAlarmComponent(alarm)) - } - - return alarms -} - -/** - * Get all numbers between start and end as strings - * - * @param {Number} start Lower end of range - * @param {Number} end Upper end of range - * @returns {string[]} - */ -function getRangeAsStrings(start, end) { - return Array - .apply(null, Array((end - start) + 1)) - .map((_, n) => n + start) - .map((s) => s.toString()) -} - -/** - * Extracts the recurrence component from a daily recurrence rule - * - * @param {RecurValue} recurrenceRule The RecurValue to extract data from - * @param {Object} recurrenceComponent The recurrence component to write data into - */ -function getRecurrenceComponentFromDailyRule(recurrenceRule, recurrenceComponent) { - /** - * # Daily - * - * The Nextcloud-editor does not support any BY-parts for the daily rule, hence - * we will mark any DAILY rule with BY-parts as unsupported. - */ - const forbiddenComponents = [ - 'BYSECOND', - 'BYMINUTE', - 'BYHOUR', - 'BYDAY', - 'BYMONTHDAY', - 'BYYEARDAY', - 'BYWEEKNO', - 'BYMONTH', - 'BYSETPOS', - ] - - if (containsRecurrenceComponent(recurrenceRule, forbiddenComponents)) { - recurrenceComponent.isUnsupported = true - } -} - -/** - * Extracts the recurrence component from a weekly recurrence rule - * - * @param {RecurValue} recurrenceRule The RecurValue to extract data from - * @param {Object} recurrenceComponent The recurrence component to write data into - * @param {EventComponent} eventComponent The event component needed for default values - */ -function getRecurrenceComponentFromWeeklyRule(recurrenceRule, recurrenceComponent, eventComponent) { - /** - * # Weekly - * - * The Nextcloud-editor only supports BYDAY in order to expand the weekly rule. - * It does not support other BY-parts like BYSETPOS or BYMONTH - * - * As defined by RFC 5545, the individual BYDAY components may not be preceded - * by a positive or negative integer. - */ - const forbiddenComponents = [ - 'BYSECOND', - 'BYMINUTE', - 'BYHOUR', - 'BYMONTHDAY', - 'BYYEARDAY', - 'BYWEEKNO', - 'BYMONTH', - 'BYSETPOS', - ] - - if (containsRecurrenceComponent(recurrenceRule, forbiddenComponents)) { - recurrenceComponent.isUnsupported = true - } - - recurrenceComponent.byDay = recurrenceRule.getComponent('BYDAY') - .filter((weekDay) => ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'].includes(weekDay)) - - // If the BYDAY is empty, add the day that the event occurs in - // E.g. if the event is on a Wednesday, automatically set BYDAY:WE - if (recurrenceComponent.byDay.length === 0) { - recurrenceComponent.byDay.push(getWeekDayFromDate(eventComponent.startDate.jsDate)) - } -} - -/** - * Extracts the recurrence component from a monthly recurrence rule - * - * @param {RecurValue} recurrenceRule The RecurValue to extract data from - * @param {Object} recurrenceComponent The recurrence component to write data into - * @param {EventComponent} eventComponent The event component needed for default values - */ -function getRecurrenceComponentFromMonthlyRule(recurrenceRule, recurrenceComponent, eventComponent) { - /** - * # Monthly - * - * The Nextcloud-editor only supports BYMONTHDAY, BYDAY, BYSETPOS in order to expand the monthly rule. - * It supports either BYMONTHDAY or the combination of BYDAY and BYSETPOS. They have to be used exclusively - * and cannot be combined. - * - * It does not support other BY-parts like BYMONTH - * - * For monthly recurrence-rules, BYDAY components are allowed to be preceded by positive or negative integers. - * The Nextcloud-editor supports at most one BYDAY component with an integer. - * If it's presented with such a BYDAY component, it will internally be converted to BYDAY without integer and BYSETPOS. - * e.g. - * BYDAY=3WE => BYDAY=WE,BYSETPOS=3 - * - * BYSETPOS is limited to -2, -1, 1, 2, 3, 4, 5 - * Other values are not supported - * - * BYDAY is limited to "MO", "TU", "WE", "TH", "FR", "SA", "SU", - * "MO,TU,WE,TH,FR,SA,SU", "MO,TU,WE,TH,FR", "SA,SU" - * - * BYMONYHDAY is limited to "1", "2", ..., "31" - */ - const forbiddenComponents = [ - 'BYSECOND', - 'BYMINUTE', - 'BYHOUR', - 'BYYEARDAY', - 'BYWEEKNO', - 'BYMONTH', - ] - - if (containsRecurrenceComponent(recurrenceRule, forbiddenComponents)) { - recurrenceComponent.isUnsupported = true - } - - if (containsRecurrenceComponent(recurrenceRule, ['BYMONYHDAY'])) { - if (containsRecurrenceComponent(recurrenceRule, ['BYDAY', 'BYSETPOS'])) { - recurrenceComponent.isUnsupported = true - } - - const allowedValues = getRangeAsStrings(1, 31) - const byMonthDayComponent = recurrenceRule.getComponent('BYMONYHDAY') - recurrenceComponent.byMonthDay = byMonthDayComponent.filter((day) => - allowedValues.includes(day)) - - if (byMonthDayComponent.length !== recurrenceComponent.byMonthDay.length) { - recurrenceComponent.isUnsupported = true - } - // TODO: the following is duplicate code, the same as in the yearly function. - } else if (containsRecurrenceComponent(recurrenceRule, ['BYDAY']) && containsRecurrenceComponent(recurrenceRule, ['BYSETPOS'])) { - if (isAllowedByDay(recurrenceRule.getComponent('BYDAY'))) { - recurrenceComponent.byDay = recurrenceRule.getComponent('BYDAY') - } else { - recurrenceComponent.byDay = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'] - recurrenceComponent.isUnsupported = true - } - - const setPositionArray = recurrenceRule.getComponent('BYSETPOS') - if (setPositionArray.length === 1 && isAllowedBySetPos(setPositionArray[0])) { - recurrenceComponent.bySetPosition = setPositionArray[0] - } else { - recurrenceComponent.bySetPosition = 1 - recurrenceComponent.isUnsupported = true - } - } else if (containsRecurrenceComponent(recurrenceRule, ['BYDAY'])) { - const byDayArray = recurrenceRule.getComponent('BYDAY') - - if (byDayArray.length > 1) { - recurrenceComponent.byMonthDay.push(eventComponent.startDate.day.toString()) - recurrenceComponent.isUnsupported = true - } else { - const firstElement = byDayArray[0] - - const match = /^(-?\d)([A-Z]){2}$/.exec(firstElement) - if (match) { - const bySetPosition = match[1] - const byDay = match[2] - - if (isAllowedBySetPos(bySetPosition)) { - recurrenceComponent.byDay = [byDay] - recurrenceComponent.bySetPosition = bySetPosition - } else { - recurrenceComponent.byDay = [byDay] - recurrenceComponent.bySetPosition = 1 - recurrenceComponent.isUnsupported = true - } - } else { - recurrenceComponent.byMonthDay.push(eventComponent.startDate.day.toString()) - recurrenceComponent.isUnsupported = true - } - } - } else { - // If none of the previous rules are present, automatically set a BYMONTHDAY - recurrenceComponent.byMonthDay.push(eventComponent.startDate.day.toString()) - } -} - -/** - * Extracts the recurrence component from a yearly recurrence rule - * - * @param {RecurValue} recurrenceRule The RecurValue to extract data from - * @param {Object} recurrenceComponent The recurrence component to write data into - * @param {EventComponent} eventComponent The event component needed for default values - */ -function getRecurrenceComponentFromYearlyRule(recurrenceRule, recurrenceComponent, eventComponent) { - /** - * # YEARLY - * - * The Nextcloud-editor only supports BYMONTH, BYDAY, BYSETPOS in order to expand the monthly rule. - * - * - * - * - * - * - * BYSETPOS is limited to -2, -1, 1, 2, 3, 4, 5 - * Other values are not supported - * - * BYDAY is limited to "MO", "TU", "WE", "TH", "FR", "SA", "SU", - * "MO,TU,WE,TH,FR,SA,SU", "MO,TU,WE,TH,FR", "SA,SU" - */ - const forbiddenComponents = [ - 'BYSECOND', - 'BYMINUTE', - 'BYHOUR', - 'BYMONTHDAY', - 'BYYEARDAY', - 'BYWEEKNO', - ] - - if (containsRecurrenceComponent(recurrenceRule, forbiddenComponents)) { - recurrenceComponent.isUnsupported = true - } - - if (containsRecurrenceComponent(recurrenceRule, ['BYMONTH'])) { - recurrenceComponent.byMonth = recurrenceRule.getComponent('BYMONTH') - } else { - recurrenceComponent.byMonth.push(eventComponent.startDate.month.toString()) - } - - // TODO: the following is duplicate code, the same as in the month function. - if (containsRecurrenceComponent(recurrenceRule, ['BYDAY']) && containsRecurrenceComponent(recurrenceRule, ['BYSETPOS'])) { - if (isAllowedByDay(recurrenceRule.getComponent('BYDAY'))) { - recurrenceComponent.byDay = recurrenceRule.getComponent('BYDAY') - } else { - recurrenceComponent.byDay = ['MO', 'TU', 'W E', 'TH', 'FR', 'SA', 'SU'] - recurrenceComponent.isUnsupported = true - } - - const setPositionArray = recurrenceRule.getComponent('BYSETPOS') - if (setPositionArray.length === 1 && isAllowedBySetPos(setPositionArray[0])) { - recurrenceComponent.bySetPosition = setPositionArray[0] - } else { - recurrenceComponent.bySetPosition = 1 - recurrenceComponent.isUnsupported = true - } - } else if (containsRecurrenceComponent(recurrenceRule, ['BYDAY'])) { - const byDayArray = recurrenceRule.getComponent('BYDAY') - - if (byDayArray.length > 1) { - recurrenceComponent.byMonthDay.push(eventComponent.startDate.day.toString()) - recurrenceComponent.isUnsupported = true - } else { - const firstElement = byDayArray[0] - - const match = /^(-?\d)([A-Z]){2}$/.exec(firstElement) - if (match) { - const bySetPosition = match[1] - const byDay = match[2] - - if (isAllowedBySetPos(bySetPosition)) { - recurrenceComponent.byDay = [byDay] - recurrenceComponent.bySetPosition = bySetPosition - } else { - recurrenceComponent.byDay = [byDay] - recurrenceComponent.bySetPosition = 1 - recurrenceComponent.isUnsupported = true - } - } else { - recurrenceComponent.byMonthDay.push(eventComponent.startDate.day.toString()) - recurrenceComponent.isUnsupported = true - } - } - } -} - -/** - * Checks if the given parameter is a supported BYDAY value - * - * @param {String[]} byDay The byDay component to check - * @returns {Boolean} - */ -function isAllowedByDay(byDay) { - const allowedByDay = [ - 'MO', - 'TU', - 'WE', - 'TH', - 'FR', - 'SA', - 'SU', - 'FR,MO,SA,SU,TH,TU,WE', - 'FR,MO,TH,TU,WE', - 'SA,SU', - ] - - return allowedByDay.includes(byDay.slice().sort().join(',')) -} - -/** - * Checks if the given parameter is a supported BYSETPOS value - * - * @param {String} bySetPos The bySetPos component to check - * @returns {Boolean} - */ -function isAllowedBySetPos(bySetPos) { - const allowedBySetPos = [ - '-2', - '-1', - '1', - '2', - '3', - '4', - '5', - ] - - return allowedBySetPos.includes(bySetPos.toString()) -} - -/** - * - * @param {Object} alarm The alarm to set / update - * @param {AlarmComponent} alarmComponent The alarm component to read from - */ -export function updateAlarmFromAlarmComponent(alarm, alarmComponent) { - alarm.type = alarmComponent.action - alarm.isRelative = alarmComponent.trigger.isRelative() - - alarm.absoluteDate = null - alarm.absoluteTimezoneId = null - - alarm.relativeIsBefore = null - alarm.relativeIsRelatedToStart = null - - alarm.relativeUnitTimed = null - alarm.relativeAmountTimed = null - - alarm.relativeUnitAllDay = null - alarm.relativeAmountAllDay = null - alarm.relativeHoursAllDay = null - alarm.relativeMinutesAllDay = null - - alarm.relativeTrigger = null - - alarm.alarmComponent = alarmComponent - - if (alarm.isRelative) { - alarm.relativeIsBefore = alarmComponent.trigger.value.isNegative - alarm.relativeIsRelatedToStart = alarmComponent.trigger.related === 'START' - - const timedData = getAmountAndUnitForTimedEvents(alarmComponent.trigger.value.totalSeconds) - alarm.relativeAmountTimed = timedData.amount - alarm.relativeUnitTimed = timedData.unit - - const allDayData = getAmountHoursMinutesAndUnitForAllDayEvents(alarmComponent.trigger.value.totalSeconds) - alarm.relativeUnitAllDay = allDayData.unit - alarm.relativeAmountAllDay = allDayData.amount - alarm.relativeHoursAllDay = allDayData.hours - alarm.relativeMinutesAllDay = allDayData.minutes - - alarm.relativeTrigger = alarmComponent.trigger.value.totalSeconds - } else { - alarm.absoluteDate = getDateFromDateTimeValue(alarmComponent.trigger.value) - alarm.absoluteTimezoneId = alarmComponent.trigger.value.timezoneId - } -} - -/** - * - * @param {AlarmComponent} alarmComponent The alarm component to read from - * @returns {Object} - */ -export function getAlarmFromAlarmComponent(alarmComponent) { - const alarmObject = {} - updateAlarmFromAlarmComponent(alarmObject, alarmComponent) - - return alarmObject -} diff --git a/src/models/calendarShare.js b/src/models/calendarShare.js new file mode 100644 index 000000000..b02fb10b9 --- /dev/null +++ b/src/models/calendarShare.js @@ -0,0 +1,92 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +import { + PRINCIPAL_PREFIX_CIRCLE, + PRINCIPAL_PREFIX_GROUP, + PRINCIPAL_PREFIX_USER, +} from './consts.js' + +/** + * Creates a complete calendar-share-object based on given props + * + * @param {Object} props Calendar-share-props already provided + * @returns {Object} + */ +const getDefaultCalendarShareObject = (props = {}) => Object.assign({}, { + // Unique identifier + id: null, + // Displayname of the sharee + displayName: null, + // Whether or not share is writable + writeable: false, + // Whether or not sharee is an individual user + isUser: false, + // Whether or not sharee is an admin-defined group + isGroup: false, + // Whether or not sharee is a user-defined group + isCircle: false, + // Uri necessary for deleting / updating share + uri: null, +}, props) + +/** + * Map a dav collection to our calendar object model + * + * @param {Object} sharee The sharee object from the cdav library shares + * @returns {Object} + */ +const mapDavShareeToCalendarShareObject = (sharee) => { + // sharee.href might contain non-latin characters, so let's uri encode it first + const id = btoa(encodeURI(sharee.href)) + + let displayName + if (sharee['common-name'] && sharee['common-name'].trim() !== '') { + displayName = sharee['common-name'] + } else if (sharee.href.startsWith(PRINCIPAL_PREFIX_GROUP)) { + displayName = sharee.href.substr(28) + } else if (sharee.href.startsWith(PRINCIPAL_PREFIX_USER)) { + displayName = sharee.href.substr(27) + } else { + displayName = sharee.href + } + + const writeable = sharee.access[0].endsWith('read-write') + const isUser = sharee.href.startsWith(PRINCIPAL_PREFIX_USER) + const isGroup = sharee.href.startsWith(PRINCIPAL_PREFIX_GROUP) + const isCircle = sharee.href.startsWith(PRINCIPAL_PREFIX_CIRCLE) + const uri = sharee.href + + return getDefaultCalendarShareObject({ + id, + displayName, + writeable, + isUser, + isGroup, + isCircle, + uri, + }) +} + +export { + getDefaultCalendarShareObject, + mapDavShareeToCalendarShareObject, +} diff --git a/src/models/consts.js b/src/models/consts.js new file mode 100644 index 000000000..af649b296 --- /dev/null +++ b/src/models/consts.js @@ -0,0 +1,58 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +const COMPONENT_NAME_EVENT = 'VEVENT' +const COMPONENT_NAME_JOURNAL = 'VJOURNAL' +const COMPONENT_NAME_VTODO = 'VTODO' + +const ITIP_MESSAGE_ADD = 'ADD' +const ITIP_MESSAGE_CANCEL = 'CANCEL' +const ITIP_MESSAGE_COUNTER = 'COUNTER' +const ITIP_MESSAGE_DECLINECOUNTER = 'DECLINECOUNTER' +const ITIP_MESSAGE_PUBLISH = 'PUBLISH' +const ITIP_MESSAGE_REFRESH = 'REFRESH' +const ITIP_MESSAGE_REPLY = 'REPLY' +const ITIP_MESSAGE_REQUEST = 'REQUEST' + +const PRINCIPAL_PREFIX_USER = 'principal:principals/users/' +const PRINCIPAL_PREFIX_GROUP = 'principal:principals/groups/' +const PRINCIPAL_PREFIX_CIRCLE = 'principal:principals/circles/' +const PRINCIPAL_PREFIX_CALENDAR_RESOURCE = 'principal:principals/calendar-resources/' +const PRINCIPAL_PREFIX_CALENDAR_ROOM = 'principal:principals/calendar-rooms/' + +export { + COMPONENT_NAME_EVENT, + COMPONENT_NAME_JOURNAL, + COMPONENT_NAME_VTODO, + ITIP_MESSAGE_ADD, + ITIP_MESSAGE_CANCEL, + ITIP_MESSAGE_COUNTER, + ITIP_MESSAGE_DECLINECOUNTER, + ITIP_MESSAGE_PUBLISH, + ITIP_MESSAGE_REFRESH, + ITIP_MESSAGE_REPLY, + ITIP_MESSAGE_REQUEST, + PRINCIPAL_PREFIX_USER, + PRINCIPAL_PREFIX_GROUP, + PRINCIPAL_PREFIX_CIRCLE, + PRINCIPAL_PREFIX_CALENDAR_RESOURCE, + PRINCIPAL_PREFIX_CALENDAR_ROOM, +} diff --git a/src/models/contact.js b/src/models/contact.js index 6365f0770..7e6266485 100644 --- a/src/models/contact.js +++ b/src/models/contact.js @@ -26,9 +26,9 @@ * @param {Object} props Contacts-props already provided * @returns {Object} */ -export const getDefaultContactsObject = (props = {}) => Object.assign({}, { +const getDefaultContactsObject = (props = {}) => Object.assign({}, { // The name of the contact - name: '', + name: null, // Calendar-user-type of the contact calendarUserType: 'INDIVIDUAL', // Whether or not this is a user @@ -51,3 +51,7 @@ export const getDefaultContactsObject = (props = {}) => Object.assign({}, { // Timezone of the user timezoneId: null, }, props) + +export { + getDefaultContactsObject, +} diff --git a/src/models/event.js b/src/models/event.js new file mode 100644 index 000000000..69712dbdc --- /dev/null +++ b/src/models/event.js @@ -0,0 +1,185 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { getDateFromDateTimeValue } from '../utils/date.js' +import DurationValue from 'calendar-js/src/values/durationValue.js' +import { getHexForColorName } from '../utils/color.js' +import { mapAlarmComponentToAlarmObject } from './alarm.js' +import { mapAttendeePropertyToAttendeeObject } from './attendee.js' +import { + getDefaultRecurrenceRuleObject, + mapRecurrenceRuleValueToRecurrenceRuleObject, +} from './recurrenceRule.js' + +/** + * Creates a complete calendar-object-instance-object based on given props + * + * @param {Object} props The props already provided + * @returns {Object} + */ +const getDefaultEventObject = (props = {}) => Object.assign({}, { + // The real event-component coming from calendar-js + eventComponent: null, + // Title of the event + title: null, + // Start date of the event + startDate: null, + // Timezone of the start date + startTimezoneId: null, + // End date of the event + endDate: null, + // Timezone of the end date + endTimezoneId: null, + // Indicator whether or not event is all-day + isAllDay: false, + // Whether or not the user is allowed to toggle the all-day checkbox + canModifyAllDay: true, + // Location that the event takes places in + location: null, + // description of the event + description: null, + // Access class of the event (PUBLIC, PRIVATE, CONFIDENTIAL) + accessClass: null, + // Status of the event (CONFIRMED, TENTATIVE, CANCELLED) + status: null, + // Whether or not to block this event in Free-Busy reports (TRANSPARENT, OPAQUE) + timeTransparency: null, + // The recurrence rule of this event. We only support one recurrence-rule + recurrenceRule: getDefaultRecurrenceRuleObject(), + // Whether or not this event has multiple recurrence-rules + hasMultipleRRules: false, + // Whether or not this is the master item + isMasterItem: false, + // Whether or not this is a recurrence-exception + isRecurrenceException: false, + // Whether or not the applied modifications require to update this and all future + forceThisAndAllFuture: false, + // Whether or not it's possible to create a recurrence-exception for this event + canCreateRecurrenceException: false, + // Attendees of this event + attendees: [], + // Organizer of the event + organizer: null, + // Alarm of the event + alarms: [], + // Custom color of the event + customColor: null, + // Categories + categories: [], +}, props) + +/** + * + * @param {EventComponent} eventComponent The calendar-js eventComponent + * @returns {Object} + */ +const mapEventComponentToEventObject = (eventComponent) => { + const eventObject = getDefaultEventObject({ + eventComponent, + title: eventComponent.title, + isAllDay: eventComponent.isAllDay(), + canModifyAllDay: eventComponent.canModifyAllDay(), + location: eventComponent.location, + description: eventComponent.description, + accessClass: eventComponent.accessClass, + status: eventComponent.status, + timeTransparency: eventComponent.timeTransparency, + categories: Array.from(eventComponent.getCategoryIterator()), + isMasterItem: eventComponent.isMasterItem(), + isRecurrenceException: eventComponent.isRecurrenceException(), + canCreateRecurrenceException: eventComponent.canCreateRecurrenceExceptions(), + }) + + /** + * According to RFC5545, DTEND is exclusive. This is rather intuitive for timed-events + * but rather unintuitive for all-day events + * + * That's why, when an event is all-day from 2019-10-03 to 2019-10-04, + * it will be displayed as 2019-10-03 to 2019-10-03 in the editor. + */ + eventObject.startDate = getDateFromDateTimeValue(eventComponent.startDate) + eventObject.startTimezoneId = eventComponent.startDate.timezoneId + + if (eventComponent.isAllDay()) { + const endDate = eventComponent.endDate.clone() + endDate.addDuration(DurationValue.fromSeconds(-1 * 60 * 60 * 24)) + eventObject.endDate = getDateFromDateTimeValue(endDate) + } else { + eventObject.endDate = getDateFromDateTimeValue(eventComponent.endDate) + } + eventObject.endTimezoneId = eventComponent.endDate.timezoneId + + /** + * Extract organizer if there is any + */ + if (eventComponent.organizer) { + const organizerProperty = eventComponent.getFirstProperty('ORGANIZER') + eventObject.organizer = { + commonName: organizerProperty.commonName, + uri: organizerProperty.email, + attendeeProperty: organizerProperty, + } + } + + /** + * Extract alarms + */ + for (const alarm of eventComponent.getAlarmIterator()) { + eventObject.alarms.push(mapAlarmComponentToAlarmObject(alarm)) + } + + /** + * Extract attendees + */ + for (const attendee of eventComponent.getAttendeeIterator()) { + eventObject.attendees.push(mapAttendeePropertyToAttendeeObject(attendee)) + } + + /** + * Extract recurrence-rule + */ + const recurrenceRuleIterator = eventComponent.getPropertyIterator('RRULE') + const recurrenceRuleFirstIteration = recurrenceRuleIterator.next() + + const firstRecurrenceRule = recurrenceRuleFirstIteration.value + if (firstRecurrenceRule) { + eventObject.recurrenceRule = mapRecurrenceRuleValueToRecurrenceRuleObject(firstRecurrenceRule.getFirstValue(), eventComponent.startDate) + eventObject.hasMultipleRRules = !recurrenceRuleIterator.next().done + } + + /** + * Convert the CSS 3 color name to a hex color + */ + if (eventComponent.hasProperty('COLOR')) { + const hexColor = getHexForColorName(eventComponent.getFirstPropertyFirstValue('COLOR')) + if (hexColor !== null) { + eventObject.customColor = hexColor + } + } + + return eventObject +} + +export { + getDefaultEventObject, + mapEventComponentToEventObject, +} diff --git a/src/models/principal.js b/src/models/principal.js index a2e18ad87..a736cb07d 100644 --- a/src/models/principal.js +++ b/src/models/principal.js @@ -20,46 +20,108 @@ * */ +import { + PRINCIPAL_PREFIX_CALENDAR_RESOURCE, + PRINCIPAL_PREFIX_CALENDAR_ROOM, + PRINCIPAL_PREFIX_CIRCLE, + PRINCIPAL_PREFIX_GROUP, + PRINCIPAL_PREFIX_USER, +} from './consts.js' + /** * Creates a complete principal-object based on given props * * @param {Object} props Principal-props already provided * @returns {any} */ -export const getDefaultPrincipalObject = (props) => Object.assign({}, { +const getDefaultPrincipalObject = (props) => Object.assign({}, { // Id of the principal - id: '', + id: null, // Calendar-user-type. This can be INDIVIDUAL, GROUP, RESOURCE or ROOM - calendarUserType: '', + calendarUserType: 'INDIVIDUAL', // E-Mail address of principal used for scheduling - emailAddress: '', + emailAddress: null, // The principals display-name - displayname: '', + // TODO: this should be renamed to displayName + displayname: null, // principalScheme - principalScheme: '', + principalScheme: null, // The internal user-id in case it is of type INDIVIDUAL and a user - userId: '', + // TODO: userId is deprecrated, use principalId instead + userId: null, // url to the DAV-principal-resource - url: '', + url: null, // The cdav-library object - dav: false, + dav: null, + // Whether or not this principal represents a circle + isCircle: false, + // Whether or not this principal represents a user + isUser: false, + // Whether or not this principal represents a group + isGroup: false, + // Whether or not this principal represents a calendar-resource + isCalendarResource: false, + // Whether or not this principal represents a calendar-room + isCalendarRoom: false, + // The id of the principal without prefix. e.g. userId / groupId / etc. + principalId: null, }, props) /** * converts a dav principal into a vuex object * - * @param {Object} principal cdav-library Principal object - * @returns {{emailAddress: *, displayname: *, dav: *, id: *, calendarUserType: *, userId: *, url: *}} + * @param {Object} dav cdav-library Principal object + * @returns {Object} */ -export function mapDavToPrincipal(principal) { - return { - id: btoa(principal.url), - calendarUserType: principal.calendarUserType, - principalScheme: principal.principalScheme, - emailAddress: principal.email, - displayname: principal.displayname, - userId: principal.userId, - url: principal.principalUrl, - dav: principal, +const mapDavToPrincipal = (dav) => { + const id = btoa(encodeURI(dav.url)) + const calendarUserType = dav.calendarUserType + const principalScheme = dav.principalScheme + const emailAddress = dav.email + + const displayname = dav.displayname + + const isUser = dav.principalScheme.startsWith(PRINCIPAL_PREFIX_USER) + const isGroup = dav.principalScheme.startsWith(PRINCIPAL_PREFIX_GROUP) + const isCircle = dav.principalScheme.startsWith(PRINCIPAL_PREFIX_CIRCLE) + const isCalendarResource = dav.principalScheme.startsWith(PRINCIPAL_PREFIX_CALENDAR_RESOURCE) + const isCalendarRoom = dav.principalScheme.startsWith(PRINCIPAL_PREFIX_CALENDAR_ROOM) + + let principalId = null + if (isUser) { + principalId = dav.principalScheme.substring(PRINCIPAL_PREFIX_USER.length) + } else if (isGroup) { + principalId = dav.principalScheme.substring(PRINCIPAL_PREFIX_GROUP.length) + } else if (isCircle) { + principalId = dav.principalScheme.substring(PRINCIPAL_PREFIX_CIRCLE.length) + } else if (isCalendarResource) { + principalId = dav.principalScheme.substring(PRINCIPAL_PREFIX_CALENDAR_RESOURCE.length) + } else if (isCalendarRoom) { + principalId = dav.principalScheme.substring(PRINCIPAL_PREFIX_CALENDAR_ROOM.length) } + + const url = dav.principalUrl + const userId = dav.userId + + return getDefaultPrincipalObject({ + id, + calendarUserType, + principalScheme, + emailAddress, + displayname, + url, + dav, + isUser, + isGroup, + isCircle, + isCalendarResource, + isCalendarRoom, + principalId, + userId, + }) +} + +export { + getDefaultPrincipalObject, + mapDavToPrincipal, } diff --git a/src/models/recurrenceRule.js b/src/models/recurrenceRule.js new file mode 100644 index 000000000..970739d41 --- /dev/null +++ b/src/models/recurrenceRule.js @@ -0,0 +1,503 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +import { getWeekDayFromDate } from '../utils/recurrence.js' +import { getDateFromDateTimeValue } from '../utils/date.js' + +/** + * Creates a complete recurrence-rule-object based on given props + * + * @param {Object} props Recurrence-rule-object-props already provided + * @returns {Object} + */ +const getDefaultRecurrenceRuleObject = (props = {}) => Object.assign({}, { + // The calendar-js recurrence-rule value + recurrenceRuleValue: null, + // The frequency of the recurrence-rule (DAILY, WEEKLY, ...) + frequency: 'NONE', + // The interval of the recurrence-rule, must be a positive integer + interval: 1, + // Positive integer if recurrence-rule limited by count, null otherwise + count: null, + // Date if recurrence-rule limited by date, null otherwise + // We do not store a timezone here, since we only care about the date part + until: null, + // List of byDay components to limit/expand the recurrence-rule + byDay: [], + // List of byMonth components to limit/expand the recurrence-rule + byMonth: [], + // List of byMonthDay components to limit/expand the recurrence-rule + byMonthDay: [], + // A position to limit the recurrence-rule (e.g. -1 for last Friday) + bySetPosition: null, + // Whether or not the rule is not supported for editing + isUnsupported: false, +}, props) + +/** + * Maps a calendar-js recurrence-rule-value to an recurrence-rule-object + * + * @param {RecurValue} recurrenceRuleValue The calendar-js recurrence rule value + * @param {DateTimeValue} baseDate The base-date used to fill unset values + * @returns {Object} + */ +const mapRecurrenceRuleValueToRecurrenceRuleObject = (recurrenceRuleValue, baseDate) => { + switch (recurrenceRuleValue.frequency) { + case 'DAILY': + return mapDailyRuleValueToRecurrenceRuleObject(recurrenceRuleValue) + + case 'WEEKLY': + return mapWeeklyRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate) + + case 'MONTHLY': + return mapMonthlyRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate) + + case 'YEARLY': + return mapYearlyRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate) + + default: // SECONDLY, MINUTELY, HOURLY + return getDefaultRecurrenceRuleObjectForRecurrenceValue(recurrenceRuleValue, { + isUnsupported: true, + }) + } +} + +const FORBIDDEN_BY_PARTS_DAILY = [ + 'BYSECOND', + 'BYMINUTE', + 'BYHOUR', + 'BYDAY', + 'BYMONTHDAY', + 'BYYEARDAY', + 'BYWEEKNO', + 'BYMONTH', + 'BYSETPOS', +] +const FORBIDDEN_BY_PARTS_WEEKLY = [ + 'BYSECOND', + 'BYMINUTE', + 'BYHOUR', + 'BYMONTHDAY', + 'BYYEARDAY', + 'BYWEEKNO', + 'BYMONTH', + 'BYSETPOS', +] +const FORBIDDEN_BY_PARTS_MONTHLY = [ + 'BYSECOND', + 'BYMINUTE', + 'BYHOUR', + 'BYYEARDAY', + 'BYWEEKNO', + 'BYMONTH', +] +const FORBIDDEN_BY_PARTS_YEARLY = [ + 'BYSECOND', + 'BYMINUTE', + 'BYHOUR', + 'BYMONTHDAY', + 'BYYEARDAY', + 'BYWEEKNO', +] + +const SUPPORTED_BY_DAY_WEEKLY = [ + 'SU', + 'MO', + 'TU', + 'WE', + 'TH', + 'FR', + 'SA', +] + +/** + * Get all numbers between start and end as strings + * + * @param {Number} start Lower end of range + * @param {Number} end Upper end of range + * @returns {string[]} + */ +const getRangeAsStrings = (start, end) => { + return Array + .apply(null, Array((end - start) + 1)) + .map((_, n) => n + start) + .map((s) => s.toString()) +} + +const SUPPORTED_BY_MONTHDAY_MONTHLY = getRangeAsStrings(1, 31) + +const SUPPORTED_BY_MONTH_YEARLY = getRangeAsStrings(1, 12) + +/** + * Maps a daily calendar-js recurrence-rule-value to an recurrence-rule-object + * + * @param {RecurValue} recurrenceRuleValue The calendar-js recurrence rule value + * @returns {Object} + */ +const mapDailyRuleValueToRecurrenceRuleObject = (recurrenceRuleValue) => { + /** + * We only support DAILY rules without any by-parts in the editor. + * If the recurrence-rule contains any by-parts, mark it as unsupported. + */ + const isUnsupported = containsRecurrenceComponent(recurrenceRuleValue, FORBIDDEN_BY_PARTS_DAILY) + + return getDefaultRecurrenceRuleObjectForRecurrenceValue(recurrenceRuleValue, { + isUnsupported, + }) +} + +/** + * Maps a weekly calendar-js recurrence-rule-value to an recurrence-rule-object + * + * @param {RecurValue} recurrenceRuleValue The calendar-js recurrence rule value + * @param {DateTimeValue} baseDate The base-date used to fill unset values + * @returns {Object} + */ +const mapWeeklyRuleValueToRecurrenceRuleObject = (recurrenceRuleValue, baseDate) => { + /** + * For WEEKLY recurrences, our editor only allows BYDAY + * + * As defined in RFC5545 3.3.10. Recurrence Rule: + * > Each BYDAY value can also be preceded by a positive (+n) or + * > negative (-n) integer. If present, this indicates the nth + * > occurrence of a specific day within the MONTHLY or YEARLY "RRULE". + * + * RFC 5545 specifies other components, which can be used along WEEKLY. + * Among them are BYMONTH and BYSETPOS. We don't support those. + */ + const containsUnsupportedByParts = containsRecurrenceComponent(recurrenceRuleValue, FORBIDDEN_BY_PARTS_WEEKLY) + const containsInvalidByDayPart = recurrenceRuleValue.getComponent('BYDAY') + .some((weekday) => !SUPPORTED_BY_DAY_WEEKLY.includes(weekday)) + + const isUnsupported = containsUnsupportedByParts || containsInvalidByDayPart + + const byDay = recurrenceRuleValue.getComponent('BYDAY') + .filter((weekday) => SUPPORTED_BY_DAY_WEEKLY.includes(weekday)) + + // If the BYDAY is empty, add the day that the event occurs in + // E.g. if the event is on a Wednesday, automatically set BYDAY:WE + if (byDay.length === 0) { + byDay.push(getWeekDayFromDate(baseDate.jsDate)) + } + + return getDefaultRecurrenceRuleObjectForRecurrenceValue(recurrenceRuleValue, { + byDay, + isUnsupported, + }) +} + +/** + * Maps a monthly calendar-js recurrence-rule-value to an recurrence-rule-object + * + * @param {RecurValue} recurrenceRuleValue The calendar-js recurrence rule value + * @param {DateTimeValue} baseDate The base-date used to fill unset values + * @returns {Object} + */ +const mapMonthlyRuleValueToRecurrenceRuleObject = (recurrenceRuleValue, baseDate) => { + /** + * We only supports BYMONTHDAY, BYDAY, BYSETPOS in order to expand the monthly rule. + * It supports either BYMONTHDAY or the combination of BYDAY and BYSETPOS. They have to be used exclusively + * and cannot be combined. + * + * We do not support other BY-parts like BYMONTH + * + * For monthly recurrence-rules, BYDAY components are allowed to be preceded by positive or negative integers. + * The Nextcloud-editor supports at most one BYDAY component with an integer. + * If it's presented with such a BYDAY component, it will internally be converted to BYDAY without integer and BYSETPOS. + * e.g. + * BYDAY=3WE => BYDAY=WE,BYSETPOS=3 + * + * BYSETPOS is limited to -2, -1, 1, 2, 3, 4, 5 + * Other values are not supported + * + * BYDAY is limited to "MO", "TU", "WE", "TH", "FR", "SA", "SU", + * "MO,TU,WE,TH,FR,SA,SU", "MO,TU,WE,TH,FR", "SA,SU" + * + * BYMONTHDAY is limited to "1", "2", ..., "31" + */ + let isUnsupported = containsRecurrenceComponent(recurrenceRuleValue, FORBIDDEN_BY_PARTS_MONTHLY) + + let byDay = [] + let bySetPosition = null + let byMonthDay = [] + + // This handles the first case, where we have a BYMONTHDAY rule + if (containsRecurrenceComponent(recurrenceRuleValue, ['BYMONTHDAY'])) { + // verify there is no BYDAY or BYSETPOS at the same time + if (containsRecurrenceComponent(recurrenceRuleValue, ['BYDAY', 'BYSETPOS'])) { + isUnsupported = true + } + + const containsInvalidByMonthDay = recurrenceRuleValue.getComponent('BYMONTHDAY') + .some((monthDay) => !SUPPORTED_BY_MONTHDAY_MONTHLY.includes(monthDay.toString())) + isUnsupported = isUnsupported || containsInvalidByMonthDay + + byMonthDay = recurrenceRuleValue.getComponent('BYMONTHDAY') + .filter((monthDay) => SUPPORTED_BY_MONTHDAY_MONTHLY.includes(monthDay.toString())) + .map((monthDay) => monthDay.toString()) + + // This handles cases where we have both BYDAY and BYSETPOS + } else if (containsRecurrenceComponent(recurrenceRuleValue, ['BYDAY']) && containsRecurrenceComponent(recurrenceRuleValue, ['BYSETPOS'])) { + + if (isAllowedByDay(recurrenceRuleValue.getComponent('BYDAY'))) { + byDay = recurrenceRuleValue.getComponent('BYDAY') + } else { + byDay = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'] + isUnsupported = true + } + + const setPositionArray = recurrenceRuleValue.getComponent('BYSETPOS') + if (setPositionArray.length === 1 && isAllowedBySetPos(setPositionArray[0])) { + bySetPosition = setPositionArray[0] + } else { + bySetPosition = 1 + isUnsupported = true + } + + // This handles cases where we only have a BYDAY + } else if (containsRecurrenceComponent(recurrenceRuleValue, ['BYDAY'])) { + + const byDayArray = recurrenceRuleValue.getComponent('BYDAY') + + if (byDayArray.length > 1) { + byMonthDay.push(baseDate.day.toString()) + isUnsupported = true + } else { + const firstElement = byDayArray[0] + + const match = /^(-?\d)([A-Z]{2})$/.exec(firstElement) + if (match) { + const matchedBySetPosition = match[1] + const matchedByDay = match[2] + + if (isAllowedBySetPos(matchedBySetPosition)) { + byDay = [matchedByDay] + bySetPosition = parseInt(matchedBySetPosition, 10) + } else { + byDay = [matchedByDay] + bySetPosition = 1 + isUnsupported = true + } + } else { + byMonthDay.push(baseDate.day.toString()) + isUnsupported = true + } + } + + // This is a fallback where we just default BYMONTHDAY to the start date of the event + } else { + byMonthDay.push(baseDate.day.toString()) + } + + return getDefaultRecurrenceRuleObjectForRecurrenceValue(recurrenceRuleValue, { + byDay, + bySetPosition, + byMonthDay, + isUnsupported, + }) +} + +/** + * Maps a yearly calendar-js recurrence-rule-value to an recurrence-rule-object + * + * @param {RecurValue} recurrenceRuleValue The calendar-js recurrence rule value + * @param {DateTimeValue} baseDate The base-date used to fill unset values + * @returns {Object} + */ +const mapYearlyRuleValueToRecurrenceRuleObject = (recurrenceRuleValue, baseDate) => { + /** + * We only supports BYMONTH, BYDAY, BYSETPOS in order to expand the yearly rule. + * It supports a combination of them. + * + * We do not support other BY-parts. + * + * For yearly recurrence-rules, BYDAY components are allowed to be preceded by positive or negative integers. + * The Nextcloud-editor supports at most one BYDAY component with an integer. + * If it's presented with such a BYDAY component, it will internally be converted to BYDAY without integer and BYSETPOS. + * e.g. + * BYDAY=3WE => BYDAY=WE,BYSETPOS=3 + * + * BYSETPOS is limited to -2, -1, 1, 2, 3, 4, 5 + * Other values are not supported + * + * BYDAY is limited to "MO", "TU", "WE", "TH", "FR", "SA", "SU", + * "MO,TU,WE,TH,FR,SA,SU", "MO,TU,WE,TH,FR", "SA,SU" + */ + let isUnsupported = containsRecurrenceComponent(recurrenceRuleValue, FORBIDDEN_BY_PARTS_YEARLY) + + let byDay = [] + let bySetPosition = null + let byMonth = [] + + if (containsRecurrenceComponent(recurrenceRuleValue, ['BYMONTH'])) { + const containsInvalidByMonthDay = recurrenceRuleValue.getComponent('BYMONTH') + .some((month) => !SUPPORTED_BY_MONTH_YEARLY.includes(month.toString())) + isUnsupported = isUnsupported || containsInvalidByMonthDay + + byMonth = recurrenceRuleValue.getComponent('BYMONTH') + .filter((monthDay) => SUPPORTED_BY_MONTH_YEARLY.includes(monthDay.toString())) + .map((month) => month.toString()) + } else { + byMonth.push(baseDate.month.toString()) + } + + if (containsRecurrenceComponent(recurrenceRuleValue, ['BYDAY']) && containsRecurrenceComponent(recurrenceRuleValue, ['BYSETPOS'])) { + + if (isAllowedByDay(recurrenceRuleValue.getComponent('BYDAY'))) { + byDay = recurrenceRuleValue.getComponent('BYDAY') + } else { + byDay = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'] + isUnsupported = true + } + + const setPositionArray = recurrenceRuleValue.getComponent('BYSETPOS') + if (setPositionArray.length === 1 && isAllowedBySetPos(setPositionArray[0])) { + bySetPosition = setPositionArray[0] + } else { + bySetPosition = 1 + isUnsupported = true + } + + } else if (containsRecurrenceComponent(recurrenceRuleValue, ['BYDAY'])) { + + const byDayArray = recurrenceRuleValue.getComponent('BYDAY') + if (byDayArray.length > 1) { + isUnsupported = true + } else { + const firstElement = byDayArray[0] + + const match = /^(-?\d)([A-Z]{2})$/.exec(firstElement) + if (match) { + const matchedBySetPosition = match[1] + const matchedByDay = match[2] + + if (isAllowedBySetPos(matchedBySetPosition)) { + byDay = [matchedByDay] + bySetPosition = parseInt(matchedBySetPosition, 10) + } else { + byDay = [matchedByDay] + bySetPosition = 1 + isUnsupported = true + } + } else { + isUnsupported = true + } + } + } + + return getDefaultRecurrenceRuleObjectForRecurrenceValue(recurrenceRuleValue, { + byDay, + bySetPosition, + byMonth, + isUnsupported, + }) +} + +/** + * Checks if the given parameter is a supported BYDAY value + * + * @param {String[]} byDay The byDay component to check + * @returns {Boolean} + */ +const isAllowedByDay = (byDay) => { + return [ + 'MO', + 'TU', + 'WE', + 'TH', + 'FR', + 'SA', + 'SU', + 'FR,MO,SA,SU,TH,TU,WE', + 'FR,MO,TH,TU,WE', + 'SA,SU', + ].includes(byDay.slice().sort().join(',')) +} + +/** + * Checks if the given parameter is a supported BYSETPOS value + * + * @param {String} bySetPos The bySetPos component to check + * @returns {Boolean} + */ +const isAllowedBySetPos = (bySetPos) => { + return [ + '-2', + '-1', + '1', + '2', + '3', + '4', + '5', + ].includes(bySetPos.toString()) +} + +/** + * Checks if the recurrence-rule contains any of the given components + * + * @param {RecurValue} recurrenceRule The recurrence-rule value to check for the given components + * @param {String[]} components List of components to check for + * @returns {Boolean} + */ +const containsRecurrenceComponent = (recurrenceRule, components) => { + for (const component of components) { + const componentValue = recurrenceRule.getComponent(component) + if (componentValue.length > 0) { + return true + } + } + + return false +} + +/** + * Returns a full recurrence-rule-object with default values derived from recurrenceRuleValue + * and additional props + * + * @param {RecurValue} recurrenceRuleValue The recurrence-rule value to get default values from + * @param {Object} props The properties to provide on top of default one + * @returns {Object} + */ +const getDefaultRecurrenceRuleObjectForRecurrenceValue = (recurrenceRuleValue, props) => { + const isUnsupported = recurrenceRuleValue.count !== null && recurrenceRuleValue.until !== null + let isUnsupportedProps = {} + + if (isUnsupported) { + isUnsupportedProps = { + isUnsupported, + } + } + + return getDefaultRecurrenceRuleObject(Object.assign({}, { + recurrenceRuleValue, + frequency: recurrenceRuleValue.frequency, + interval: parseInt(recurrenceRuleValue.interval, 10) || 1, + count: recurrenceRuleValue.count, + until: recurrenceRuleValue.until + ? getDateFromDateTimeValue(recurrenceRuleValue.until) + : null, + }, props, isUnsupportedProps)) +} + +export { + getDefaultRecurrenceRuleObject, + mapRecurrenceRuleValueToRecurrenceRuleObject, +} diff --git a/src/models/rfcProps.js b/src/models/rfcProps.js index d6bf532ed..3f3a3feee 100644 --- a/src/models/rfcProps.js +++ b/src/models/rfcProps.js @@ -27,7 +27,7 @@ import { getDefaultCategories } from '../defaults/defaultCategories.js' * * @returns {{color: {readableName: *, icon: string, multiple: boolean, info: *}, timeTransparency: {readableName: *, defaultValue: string, icon: string, multiple: boolean, options: *[], info: *}, description: {readableName: *, icon: string, placeholder: *, defaultNumberOfRows: number}, location: {readableName: *, icon: string, placeholder: *}, categories: {readableName: *, icon: string, multiple: boolean, options: *, tagPlaceholder: *, placeholder: *, info: *}, accessClass: {readableName: *, defaultValue: string, icon: string, options: *[], multiple: boolean, info: *}, status: {readableName: *, defaultValue: string, icon: string, options: *[], multiple: boolean, info: *}}} */ -export function getRFCProperties() { +const getRFCProperties = () => { return { /** * https://tools.ietf.org/html/rfc5545#section-3.8.1.3 @@ -114,4 +114,6 @@ export function getRFCProperties() { } } -export default getRFCProperties +export { + getRFCProperties, +} diff --git a/src/models/schedulingObject.js b/src/models/schedulingObject.js new file mode 100644 index 000000000..35cc71aaa --- /dev/null +++ b/src/models/schedulingObject.js @@ -0,0 +1,187 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +import { getParserManager } from 'calendar-js' +import { getDateFromDateTimeValue } from '../utils/date.js' +import { + ITIP_MESSAGE_ADD, + ITIP_MESSAGE_CANCEL, + ITIP_MESSAGE_COUNTER, + ITIP_MESSAGE_DECLINECOUNTER, + ITIP_MESSAGE_PUBLISH, + ITIP_MESSAGE_REFRESH, + ITIP_MESSAGE_REPLY, + ITIP_MESSAGE_REQUEST, +} from './consts.js' + +/** + * Creates a complete scheduling-object-object based on given props + * + * @param {Object} props Scheduling-object-props already provided + * @returns {Object} + */ +const getDefaultSchedulingObject = (props = {}) => Object.assign({}, { + // Id of the scheduling-object + id: null, + // The cdav-library object storing the scheduling-object + dav: null, + // The parsed calendar-js object + calendarComponent: null, + // The uid of the scheduling-object + uid: null, + // Recurrence-id of the scheduling-object + recurrenceId: null, + // The uri of the scheduling-object + uri: null, + // The scheduling method + method: null, + // Whether or not the method is PUBLISH + isPublish: false, + // Whether or not the method is REQUEST + isRequest: false, + // Whether or not the method is REPLY + isReply: false, + // Whether or not the method is ADD + isAdd: false, + // Whether or not the method is CANCEL + isCancel: false, + // Whether or not the method is REFRESH + isRefresh: false, + // Whether or not the method is COUNTER + isCounter: false, + // Whether or not the method is DECLINECOUNTER + isDeclineCounter: false, + // Whether or not the scheduling-object exists on the server + existsOnServer: false, +}, props) + +/** + * Maps a calendar-object from c-dav to our calendar-object object + * + * @param {VObject} dav The c-dav VObject + * @returns {Object} + */ +const mapCDavObjectToSchedulingObject = (dav) => { + const parserManager = getParserManager() + const parser = parserManager.getParserForFileType('text/calendar', { + preserveMethod: true, + processFreeBusy: true, + }) + + // This should not be the case, but let's just be on the safe side + if (typeof dav.data !== 'string' || dav.data.trim() === '') { + throw new Error('Empty scheduling object') + } + + parser.parse(dav.data) + const calendarComponentIterator = parser.getItemIterator() + const calendarComponent = calendarComponentIterator.next().value + if (!calendarComponent) { + throw new Error('Empty scheduling object') + } + + const firstVObject = getFirstObjectFromCalendarComponent(calendarComponent) + + let recurrenceId = null + if (firstVObject.recurrenceId) { + recurrenceId = getDateFromDateTimeValue(firstVObject.recurrenceId) + } + + if (!calendarComponent.method) { + throw new Error('Scheduling-object does not have method') + } + + return getDefaultSchedulingObject({ + id: btoa(dav.url), + dav, + calendarComponent, + uid: firstVObject.uid, + uri: dav.url, + recurrenceId, + method: calendarComponent.method, + isPublish: calendarComponent.method === ITIP_MESSAGE_PUBLISH, + isRequest: calendarComponent.method === ITIP_MESSAGE_REQUEST, + isReply: calendarComponent.method === ITIP_MESSAGE_REPLY, + isAdd: calendarComponent.method === ITIP_MESSAGE_ADD, + isCancel: calendarComponent.method === ITIP_MESSAGE_CANCEL, + isRefresh: calendarComponent.method === ITIP_MESSAGE_REFRESH, + isCounter: calendarComponent.method === ITIP_MESSAGE_COUNTER, + isDeclineCounter: calendarComponent.method === ITIP_MESSAGE_DECLINECOUNTER, + existsOnServer: true, + }) +} + +/** + * Maps a calendar-component from calendar-js to our calendar-object object + * + * @param {CalendarComponent} calendarComponent The calendarComponent to create the calendarObject from + * @returns {Object} + */ +const mapCalendarJsToSchedulingObject = (calendarComponent) => { + const firstVObject = getFirstObjectFromCalendarComponent(calendarComponent) + + let recurrenceId = null + if (firstVObject.recurrenceId) { + recurrenceId = getDateFromDateTimeValue(firstVObject.recurrenceId) + } + + if (!calendarComponent.method) { + throw new Error('Scheduling-object does not have method') + } + + return getDefaultSchedulingObject({ + calendarComponent, + uid: firstVObject.uid, + recurrenceId, + method: calendarComponent.method, + isPublish: calendarComponent.method === ITIP_MESSAGE_PUBLISH, + isRequest: calendarComponent.method === ITIP_MESSAGE_REQUEST, + isReply: calendarComponent.method === ITIP_MESSAGE_REPLY, + isAdd: calendarComponent.method === ITIP_MESSAGE_ADD, + isCancel: calendarComponent.method === ITIP_MESSAGE_CANCEL, + isRefresh: calendarComponent.method === ITIP_MESSAGE_REFRESH, + isCounter: calendarComponent.method === ITIP_MESSAGE_COUNTER, + isDeclineCounter: calendarComponent.method === ITIP_MESSAGE_DECLINECOUNTER, + }) +} + +/** + * Extracts the first object from the calendar-component + * + * @param {CalendarComponent} calendarComponent The calendar-component + * @returns {any} First VEvent / VJournal / VTodo / VFreeBusy + */ +const getFirstObjectFromCalendarComponent = (calendarComponent) => { + const vObjectIterator = calendarComponent.getVObjectIterator() + const firstVObject = vObjectIterator.next().value + if (firstVObject) { + return firstVObject + } + + const vFreeBusyIterator = calendarComponent.getFreebusyIterator() + return vFreeBusyIterator.next().value +} + +export { + getDefaultSchedulingObject, + mapCDavObjectToSchedulingObject, + mapCalendarJsToSchedulingObject, +} diff --git a/src/store/calendarObjectInstance.js b/src/store/calendarObjectInstance.js index 7b149541e..3d6ccd688 100644 --- a/src/store/calendarObjectInstance.js +++ b/src/store/calendarObjectInstance.js @@ -31,9 +31,9 @@ import RecurValue from 'calendar-js/src/values/recurValue.js' import Property from 'calendar-js/src/properties/property.js' import { getBySetPositionAndBySetFromDate, getWeekDayFromDate } from '../utils/recurrence.js' import { - getAlarmFromAlarmComponent, - getDefaultCalendarObjectInstanceObject, mapEventComponentToCalendarObjectInstanceObject, -} from '../models/calendarObjectInstance.js' + getDefaultEventObject, + mapEventComponentToEventObject, +} from '../models/event.js' import { getAmountAndUnitForTimedEvents, getAmountHoursMinutesAndUnitForAllDayEvents, @@ -43,6 +43,8 @@ import { getClosestCSS3ColorNameForHex, getHexForColorName, } from '../utils/color.js' +import { mapAlarmComponentToAlarmObject } from '../models/alarm.js' +import { getObjectAtRecurrenceId } from '../utils/calendarObject.js' const state = { isNew: null, @@ -970,7 +972,7 @@ const mutations = { removeRecurrenceRuleFromCalendarObjectInstance(state, { calendarObjectInstance, recurrenceRule }) { if (recurrenceRule.recurrenceRuleValue) { calendarObjectInstance.eventComponent.deleteAllProperties('RRULE') - Vue.set(calendarObjectInstance, 'recurrenceRule', getDefaultCalendarObjectInstanceObject().recurrenceRule) + Vue.set(calendarObjectInstance, 'recurrenceRule', getDefaultEventObject().recurrenceRule) console.debug(calendarObjectInstance) console.debug(recurrenceRule) @@ -1300,7 +1302,7 @@ const mutations = { const duration = DurationValue.fromSeconds(totalSeconds) const alarmComponent = eventComponent.addRelativeAlarm(type, duration) - const alarmObject = getAlarmFromAlarmComponent(alarmComponent) + const alarmObject = mapAlarmComponentToAlarmObject(alarmComponent) calendarObjectInstance.alarms.push(alarmObject) @@ -1349,9 +1351,15 @@ const actions = { */ async resolveClosestRecurrenceIdForCalendarObject({ state, dispatch, commit }, { objectId, closeToDate }) { const calendarObject = await dispatch('getEventByObjectId', { objectId }) - const eventComponent = calendarObject.getClosestRecurrence(closeToDate) + const iterator = calendarObject.calendarComponent.getVObjectIterator() + const firstVObject = iterator.next().value - return eventComponent.getReferenceRecurrenceId().unixTime + const d = DateTimeValue.fromJSDate(closeToDate, true) + return firstVObject + .recurrenceManager + .getClosestOccurrence(d) + .getReferenceRecurrenceId() + .unixTime }, /** @@ -1380,12 +1388,12 @@ const actions = { const recurrenceIdDate = new Date(recurrenceId * 1000) const calendarObject = await dispatch('getEventByObjectId', { objectId }) - const eventComponent = calendarObject.getObjectAtRecurrenceId(recurrenceIdDate) + const eventComponent = getObjectAtRecurrenceId(calendarObject, recurrenceIdDate) if (eventComponent === null) { throw new Error('Not a valid recurrence-id') } - const calendarObjectInstance = mapEventComponentToCalendarObjectInstanceObject(eventComponent) + const calendarObjectInstance = mapEventComponentToEventObject(eventComponent) commit('setCalendarObjectInstanceForExistingEvent', { calendarObject, calendarObjectInstance, @@ -1423,8 +1431,8 @@ const actions = { const calendarObject = await dispatch('createNewEvent', { start, end, isAllDay, timezoneId }) const startDate = new Date(start * 1000) - const eventComponent = calendarObject.getObjectAtRecurrenceId(startDate) - const calendarObjectInstance = mapEventComponentToCalendarObjectInstanceObject(eventComponent) + const eventComponent = getObjectAtRecurrenceId(calendarObject, startDate) + const calendarObjectInstance = mapEventComponentToEventObject(eventComponent) commit('setCalendarObjectInstanceForNewEvent', { calendarObject, @@ -1539,22 +1547,6 @@ const actions = { } }, - /** - * Resets a calendar-object-instance to it's original data and - * removes all data from the calendar-object-instance store - * - * @param {Object} vuex The vuex destructuring object - * @param {Object} vuex.state The Vuex state - * @param {Function} vuex.dispatch The Vuex dispatch function - * @param {Function} vuex.commit The Vuex commit function - * @returns {Promise} - */ - async resetCalendarObjectInstance({ state, commit }) { - if (state.calendarObject) { - state.calendarObject.resetToDav() - } - }, - /** * * @param {Object} data The destructuring object for Vuex diff --git a/src/store/calendarObjects.js b/src/store/calendarObjects.js index 8a644c313..4c5d9cca4 100644 --- a/src/store/calendarObjects.js +++ b/src/store/calendarObjects.js @@ -22,10 +22,14 @@ * */ import Vue from 'vue' -import CalendarObject from '../models/calendarObject' +import { mapCalendarJsToCalendarObject } from '../models/calendarObject' import logger from '../utils/logger.js' import DateTimeValue from 'calendar-js/src/values/dateTimeValue' -import { createEvent, getTimezoneManager } from 'calendar-js' +import { + createEvent, + getParserManager, + getTimezoneManager, +} from 'calendar-js' const state = { calendarObjects: {}, @@ -43,12 +47,8 @@ const mutations = { */ appendCalendarObjects(state, { calendarObjects = [] }) { for (const calendarObject of calendarObjects) { - if (!state.calendarObjects[calendarObject.getId()]) { - if (calendarObject instanceof CalendarObject) { - Vue.set(state.calendarObjects, calendarObject.getId(), calendarObject) - } else { - logger.error('Invalid calendarObject object') - } + if (!state.calendarObjects[calendarObject.id]) { + Vue.set(state.calendarObjects, calendarObject.id, calendarObject) } } }, @@ -61,12 +61,49 @@ const mutations = { * @param {Object} data.calendarObject Calendar-object to add */ appendCalendarObject(state, { calendarObject }) { - if (!state.calendarObjects[calendarObject.getId()]) { - if (calendarObject instanceof CalendarObject) { - Vue.set(state.calendarObjects, calendarObject.getId(), calendarObject) - } else { - logger.error('Invalid calendarObject object') - } + if (!state.calendarObjects[calendarObject.id]) { + Vue.set(state.calendarObjects, calendarObject.id, calendarObject) + } + }, + + /** + * Updates a calendar-object id + * + * @param {Object} state The store data + * @param {Object} data The destructuring object + * @param {Object} data.calendarObject Calendar-object to update + */ + updateCalendarObjectId(state, { calendarObject }) { + if (calendarObject.dav === null) { + calendarObject.id = null + } else { + calendarObject.id = btoa(calendarObject.dav.url) + } + }, + + /** + * Resets a calendar-object to it's original server state + * + * @param {Object} state The store data + * @param {Object} data The destructuring object + * @param {Object} data.calendarObject Calendar-object to reset + */ + resetCalendarObjectToDav(state, { calendarObject }) { + calendarObject = state.calendarObjects[calendarObject.id] + + // If this object does not exist on the server yet, there is nothing to do + if (!calendarObject || !calendarObject.existsOnServer) { + return + } + + const parserManager = getParserManager() + const parser = parserManager.getParserForFileType('text/calendar') + parser.parse(calendarObject.dav.data) + + const itemIterator = parser.getItemIterator() + const firstVCalendar = itemIterator.next().value + if (firstVCalendar) { + calendarObject.calendarComponent = firstVCalendar } }, @@ -78,7 +115,7 @@ const mutations = { * @param {Object} data.calendarObject Calendar-object to delete */ deleteCalendarObject(state, { calendarObject }) { - Vue.delete(state.calendarObjects, calendarObject.getId()) + Vue.delete(state.calendarObjects, calendarObject.id) }, /** @@ -114,7 +151,7 @@ const actions = { * @returns {Promise} */ async moveCalendarObject(context, { calendarObject, newCalendarId }) { - if (!calendarObject.existsOnServer()) { + if (!calendarObject.existsOnServer) { return } @@ -172,8 +209,8 @@ const actions = { * @returns {Promise} */ async updateCalendarObject(context, { calendarObject }) { - if (calendarObject.existsOnServer()) { - calendarObject.dav.data = calendarObject.vcalendar.toICS() + if (calendarObject.existsOnServer) { + calendarObject.dav.data = calendarObject.calendarComponent.toICS() await calendarObject.dav.update() context.commit('addCalendarObjectIdToAllTimeRangesOfCalendar', { @@ -188,7 +225,9 @@ const actions = { } const calendar = context.getters.getCalendarById(calendarObject.calendarId) - calendarObject.dav = await calendar.dav.createVObject(calendarObject.vcalendar.toICS()) + calendarObject.dav = await calendar.dav.createVObject(calendarObject.calendarComponent.toICS()) + calendarObject.existsOnServer = true + context.commit('updateCalendarObjectId', { calendarObject }) context.commit('appendCalendarObject', { calendarObject }) context.commit('addCalendarObjectToCalendar', { @@ -215,8 +254,10 @@ const actions = { */ async createCalendarObjectFromFork(context, { eventComponent, calendarId }) { const calendar = context.getters.getCalendarById(calendarId) - const calendarObject = new CalendarObject(eventComponent.root, calendar.id) - calendarObject.dav = await calendar.dav.createVObject(calendarObject.vcalendar.toICS()) + const calendarObject = mapCalendarJsToCalendarObject(eventComponent.root, calendar.id) + calendarObject.dav = await calendar.dav.createVObject(calendarObject.calendarComponent.toICS()) + calendarObject.existsOnServer = true + context.commit('updateCalendarObjectId', { calendarObject }) context.commit('appendCalendarObject', { calendarObject }) context.commit('addCalendarObjectToCalendar', { @@ -243,7 +284,7 @@ const actions = { async deleteCalendarObject(context, { calendarObject }) { // If this calendar-object was not created on the server yet, // no need to send requests to the server - if (calendarObject.existsOnServer()) { + if (calendarObject.existsOnServer) { await calendarObject.dav.delete() } @@ -296,7 +337,7 @@ const actions = { } const firstCalendar = context.getters.sortedCalendars[0].id - return Promise.resolve(new CalendarObject(calendar, firstCalendar)) + return Promise.resolve(mapCalendarJsToCalendarObject(calendar, firstCalendar)) }, /** diff --git a/src/store/calendars.js b/src/store/calendars.js index 0da1ed4f2..4d2e9767f 100644 --- a/src/store/calendars.js +++ b/src/store/calendars.js @@ -25,7 +25,7 @@ */ import Vue from 'vue' import client from '../services/caldavService.js' -import CalendarObject from '../models/calendarObject' +import { mapCDavObjectToCalendarObject } from '../models/calendarObject' import { dateFactory, getUnixTimestampFromDate } from '../utils/date.js' import { getDefaultCalendarObject, mapDavCollectionToCalendar } from '../models/calendar' import pLimit from 'p-limit' @@ -706,7 +706,7 @@ const actions = { const calendarObjects = [] const calendarObjectIds = [] for (const r of response) { - const calendarObject = new CalendarObject(r.data, calendar.id, r) + const calendarObject = mapCDavObjectToCalendarObject(r, calendar.id) calendarObjects.push(calendarObject) calendarObjectIds.push(calendarObject.id) } @@ -751,7 +751,7 @@ const actions = { const calendar = context.state.calendarsById[calendarId] const vObject = await calendar.dav.find(objectFileName) - const calendarObject = new CalendarObject(vObject.data, calendar.id, vObject) + const calendarObject = mapCDavObjectToCalendarObject(vObject, calendar.id) context.commit('appendCalendarObject', { calendarObject }) context.commit('addCalendarObjectToCalendar', { calendar: { @@ -831,7 +831,7 @@ const actions = { return } - const calendarObject = new CalendarObject(davObject.data, calendarId, davObject) + const calendarObject = mapCDavObjectToCalendarObject(davObject, calendarId) context.commit('appendCalendarObject', { calendarObject }) context.commit('addCalendarObjectToCalendar', { calendar, diff --git a/src/store/index.js b/src/store/index.js index f01d0331a..68584d5fd 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -21,11 +21,9 @@ * along with this program. If not, see . * */ -/* eslint-disable import/first */ import Vue from 'vue' -Vue.config.devtools = true - import Vuex from 'vuex' + import calendarObjectInstance from './calendarObjectInstance' import calendarObjects from './calendarObjects' import calendars from './calendars.js' diff --git a/src/utils/calendarObject.js b/src/utils/calendarObject.js new file mode 100644 index 000000000..fd2cc709b --- /dev/null +++ b/src/utils/calendarObject.js @@ -0,0 +1,65 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +import DateTimeValue from 'calendar-js/src/values/dateTimeValue.js' + +/** + * Get all recurrence-items in given range + * + * @param {Object} calendarObject Calendar-object model + * @param {Date} start Begin of time-range + * @param {Date} end End of time-range + * @returns {Array} + */ +const getAllObjectsInTimeRange = (calendarObject, start, end) => { + const iterator = calendarObject.calendarComponent.getVObjectIterator() + const firstVObject = iterator.next().value + if (!firstVObject) { + return [] + } + + const s = DateTimeValue.fromJSDate(start, true) + const e = DateTimeValue.fromJSDate(end, true) + return firstVObject.recurrenceManager.getAllOccurrencesBetween(s, e) +} + +/** + * Get recurrence-item at exactly a given recurrence-Id + * + * @param {Object} calendarObject Calendar-object model + * @param {Date} recurrenceId RecurrenceId to retrieve + * @returns {AbstractRecurringComponent|null} + */ +const getObjectAtRecurrenceId = (calendarObject, recurrenceId) => { + const iterator = calendarObject.calendarComponent.getVObjectIterator() + const firstVObject = iterator.next().value + if (!firstVObject) { + return null + } + + const d = DateTimeValue.fromJSDate(recurrenceId, true) + return firstVObject.recurrenceManager.getOccurrenceAtExactly(d) +} + +export { + getAllObjectsInTimeRange, + getObjectAtRecurrenceId, +} diff --git a/tests/assets/ics/alarms/absoluteAlarm.ics b/tests/assets/ics/alarms/absoluteAlarm.ics new file mode 100644 index 000000000..14500dfd3 --- /dev/null +++ b/tests/assets/ics/alarms/absoluteAlarm.ics @@ -0,0 +1,4 @@ +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER;VALUE=DATE-TIME:20200306T083000Z +END:VALARM diff --git a/tests/assets/ics/alarms/relativeAlarmAfter.ics b/tests/assets/ics/alarms/relativeAlarmAfter.ics new file mode 100644 index 000000000..1f4b345e2 --- /dev/null +++ b/tests/assets/ics/alarms/relativeAlarmAfter.ics @@ -0,0 +1,4 @@ +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER;RELATED=START:P1DT9H +END:VALARM diff --git a/tests/assets/ics/alarms/relativeAlarmAfterWithin24hours.ics b/tests/assets/ics/alarms/relativeAlarmAfterWithin24hours.ics new file mode 100644 index 000000000..db191f542 --- /dev/null +++ b/tests/assets/ics/alarms/relativeAlarmAfterWithin24hours.ics @@ -0,0 +1,4 @@ +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER;RELATED=START:PT9H +END:VALARM diff --git a/tests/assets/ics/alarms/relativeAlarmBefore.ics b/tests/assets/ics/alarms/relativeAlarmBefore.ics new file mode 100644 index 000000000..8aea43b30 --- /dev/null +++ b/tests/assets/ics/alarms/relativeAlarmBefore.ics @@ -0,0 +1,4 @@ +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER;RELATED=START:-PT15H +END:VALARM diff --git a/tests/assets/ics/alarms/relativeAlarmRelatedEnd.ics b/tests/assets/ics/alarms/relativeAlarmRelatedEnd.ics new file mode 100644 index 000000000..5dc2bf1a3 --- /dev/null +++ b/tests/assets/ics/alarms/relativeAlarmRelatedEnd.ics @@ -0,0 +1,4 @@ +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER;RELATED=END:-PT15H +END:VALARM diff --git a/tests/assets/ics/alarms/relativeAlarmWeekBefore.ics b/tests/assets/ics/alarms/relativeAlarmWeekBefore.ics new file mode 100644 index 000000000..6e926d3d2 --- /dev/null +++ b/tests/assets/ics/alarms/relativeAlarmWeekBefore.ics @@ -0,0 +1,4 @@ +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER;RELATED=START:-P6DT15H +END:VALARM diff --git a/tests/assets/ics/attendees/attendee1.ics b/tests/assets/ics/attendees/attendee1.ics new file mode 100644 index 000000000..f2945e175 --- /dev/null +++ b/tests/assets/ics/attendees/attendee1.ics @@ -0,0 +1 @@ +ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT:mailto:jsmith@example.com diff --git a/tests/assets/ics/attendees/attendee2.ics b/tests/assets/ics/attendees/attendee2.ics new file mode 100644 index 000000000..1500998fd --- /dev/null +++ b/tests/assets/ics/attendees/attendee2.ics @@ -0,0 +1 @@ +ATTENDEE;CUTYPE=GROUP:mailto:ietf-calsch@example.org diff --git a/tests/assets/ics/attendees/attendee3.ics b/tests/assets/ics/attendees/attendee3.ics new file mode 100644 index 000000000..2279709b5 --- /dev/null +++ b/tests/assets/ics/attendees/attendee3.ics @@ -0,0 +1 @@ +ATTENDEE;PARTSTAT=DECLINED:mailto:jsmith@example.com diff --git a/tests/assets/ics/attendees/attendee4.ics b/tests/assets/ics/attendees/attendee4.ics new file mode 100644 index 000000000..5b76a59a0 --- /dev/null +++ b/tests/assets/ics/attendees/attendee4.ics @@ -0,0 +1 @@ +ATTENDEE;ROLE=CHAIR:mailto:mrbig@example.com diff --git a/tests/assets/ics/attendees/attendee5.ics b/tests/assets/ics/attendees/attendee5.ics new file mode 100644 index 000000000..ad8e167ce --- /dev/null +++ b/tests/assets/ics/attendees/attendee5.ics @@ -0,0 +1 @@ +ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=TENTATIVE;DELEGATED-FROM="mailto:iamboss@example.com";CN=Henry Cabot:mailto:hcabot@example.com diff --git a/tests/assets/ics/attendees/attendee6.ics b/tests/assets/ics/attendees/attendee6.ics new file mode 100644 index 000000000..e421cccf2 --- /dev/null +++ b/tests/assets/ics/attendees/attendee6.ics @@ -0,0 +1 @@ +ATTENDEE;ROLE=NON-PARTICIPANT;PARTSTAT=DELEGATED;DELEGATED-TO="mailto:hcabot@example.com";CN=The Big Cheese:mailto:iamboss@example.com diff --git a/tests/assets/ics/attendees/attendee7.ics b/tests/assets/ics/attendees/attendee7.ics new file mode 100644 index 000000000..67ebc133a --- /dev/null +++ b/tests/assets/ics/attendees/attendee7.ics @@ -0,0 +1 @@ +ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Jane Doe:mailto:jdoe@example.com diff --git a/tests/assets/ics/rrules/rrule-count-and-until.ics b/tests/assets/ics/rrules/rrule-count-and-until.ics new file mode 100644 index 000000000..9a856dd8b --- /dev/null +++ b/tests/assets/ics/rrules/rrule-count-and-until.ics @@ -0,0 +1 @@ +FREQ=MONTHLY;UNTIL=20201122T001122Z;COUNT=5 diff --git a/tests/assets/ics/rrules/rrule-count.ics b/tests/assets/ics/rrules/rrule-count.ics new file mode 100644 index 000000000..f8ada7628 --- /dev/null +++ b/tests/assets/ics/rrules/rrule-count.ics @@ -0,0 +1 @@ +FREQ=MONTHLY;COUNT=42 diff --git a/tests/assets/ics/rrules/rrule-until.ics b/tests/assets/ics/rrules/rrule-until.ics new file mode 100644 index 000000000..c39b1aeb1 --- /dev/null +++ b/tests/assets/ics/rrules/rrule-until.ics @@ -0,0 +1 @@ +FREQ=MONTHLY;UNTIL=20201122T001122Z diff --git a/tests/assets/ics/rrules/rrules1.ics b/tests/assets/ics/rrules/rrules1.ics new file mode 100644 index 000000000..d9cccfce5 --- /dev/null +++ b/tests/assets/ics/rrules/rrules1.ics @@ -0,0 +1 @@ +FREQ=SECONDLY diff --git a/tests/assets/ics/rrules/rrules10.ics b/tests/assets/ics/rrules/rrules10.ics new file mode 100644 index 000000000..97fefc80f --- /dev/null +++ b/tests/assets/ics/rrules/rrules10.ics @@ -0,0 +1 @@ +FREQ=MONTHLY diff --git a/tests/assets/ics/rrules/rrules11.ics b/tests/assets/ics/rrules/rrules11.ics new file mode 100644 index 000000000..bd483e5e2 --- /dev/null +++ b/tests/assets/ics/rrules/rrules11.ics @@ -0,0 +1 @@ +FREQ=MONTHLY;BYMONTHDAY=1,2,3,30,31 diff --git a/tests/assets/ics/rrules/rrules12.ics b/tests/assets/ics/rrules/rrules12.ics new file mode 100644 index 000000000..05e58e707 --- /dev/null +++ b/tests/assets/ics/rrules/rrules12.ics @@ -0,0 +1 @@ +FREQ=MONTHLY;BYMONTHDAY=-1,2,-3,30,31,-31 diff --git a/tests/assets/ics/rrules/rrules13.ics b/tests/assets/ics/rrules/rrules13.ics new file mode 100644 index 000000000..8d6f34572 --- /dev/null +++ b/tests/assets/ics/rrules/rrules13.ics @@ -0,0 +1 @@ +FREQ=MONTHLY;BYMONTHDAY=1,2,3,30,31;BYDAY=MO;BYSETPOS=-1 diff --git a/tests/assets/ics/rrules/rrules14.ics b/tests/assets/ics/rrules/rrules14.ics new file mode 100644 index 000000000..228a03896 --- /dev/null +++ b/tests/assets/ics/rrules/rrules14.ics @@ -0,0 +1 @@ +FREQ=MONTHLY;BYDAY=MO;BYSETPOS=3 diff --git a/tests/assets/ics/rrules/rrules15.ics b/tests/assets/ics/rrules/rrules15.ics new file mode 100644 index 000000000..6bd8bad58 --- /dev/null +++ b/tests/assets/ics/rrules/rrules15.ics @@ -0,0 +1 @@ +FREQ=MONTHLY;BYDAY=MO,TU,WE;BYSETPOS=3 diff --git a/tests/assets/ics/rrules/rrules16.ics b/tests/assets/ics/rrules/rrules16.ics new file mode 100644 index 000000000..21691e002 --- /dev/null +++ b/tests/assets/ics/rrules/rrules16.ics @@ -0,0 +1 @@ +FREQ=MONTHLY;BYDAY=MO;BYSETPOS=-3 diff --git a/tests/assets/ics/rrules/rrules17.ics b/tests/assets/ics/rrules/rrules17.ics new file mode 100644 index 000000000..8147cd4d6 --- /dev/null +++ b/tests/assets/ics/rrules/rrules17.ics @@ -0,0 +1 @@ +FREQ=MONTHLY;BYDAY=MO;BYSETPOS=1,2,3 diff --git a/tests/assets/ics/rrules/rrules18.ics b/tests/assets/ics/rrules/rrules18.ics new file mode 100644 index 000000000..5d4ee49e3 --- /dev/null +++ b/tests/assets/ics/rrules/rrules18.ics @@ -0,0 +1 @@ +FREQ=YEARLY diff --git a/tests/assets/ics/rrules/rrules19.ics b/tests/assets/ics/rrules/rrules19.ics new file mode 100644 index 000000000..7e910532a --- /dev/null +++ b/tests/assets/ics/rrules/rrules19.ics @@ -0,0 +1 @@ +FREQ=YEARLY;BYMONTH=1,2,3 diff --git a/tests/assets/ics/rrules/rrules2.ics b/tests/assets/ics/rrules/rrules2.ics new file mode 100644 index 000000000..fde7b51bc --- /dev/null +++ b/tests/assets/ics/rrules/rrules2.ics @@ -0,0 +1 @@ +FREQ=MINUTELY diff --git a/tests/assets/ics/rrules/rrules20.ics b/tests/assets/ics/rrules/rrules20.ics new file mode 100644 index 000000000..fe2371038 --- /dev/null +++ b/tests/assets/ics/rrules/rrules20.ics @@ -0,0 +1 @@ +FREQ=YEARLY;BYMONTH=1,2,3,0 diff --git a/tests/assets/ics/rrules/rrules21.ics b/tests/assets/ics/rrules/rrules21.ics new file mode 100644 index 000000000..71ef41732 --- /dev/null +++ b/tests/assets/ics/rrules/rrules21.ics @@ -0,0 +1 @@ +FREQ=YEARLY;BYDAY=MO;BYSETPOS=3 diff --git a/tests/assets/ics/rrules/rrules22.ics b/tests/assets/ics/rrules/rrules22.ics new file mode 100644 index 000000000..b9b972f2e --- /dev/null +++ b/tests/assets/ics/rrules/rrules22.ics @@ -0,0 +1 @@ +FREQ=YEARLY;BYDAY=MO,TU,WE;BYSETPOS=3 diff --git a/tests/assets/ics/rrules/rrules23.ics b/tests/assets/ics/rrules/rrules23.ics new file mode 100644 index 000000000..291a54403 --- /dev/null +++ b/tests/assets/ics/rrules/rrules23.ics @@ -0,0 +1 @@ +FREQ=YEARLY;BYDAY=MO;BYSETPOS=-3 diff --git a/tests/assets/ics/rrules/rrules24.ics b/tests/assets/ics/rrules/rrules24.ics new file mode 100644 index 000000000..4369c1d20 --- /dev/null +++ b/tests/assets/ics/rrules/rrules24.ics @@ -0,0 +1 @@ +FREQ=YEARLY;BYDAY=MO;BYSETPOS=1,2,3 diff --git a/tests/assets/ics/rrules/rrules25.ics b/tests/assets/ics/rrules/rrules25.ics new file mode 100644 index 000000000..f63c71f12 --- /dev/null +++ b/tests/assets/ics/rrules/rrules25.ics @@ -0,0 +1 @@ +FREQ=MONTHLY;BYDAY=MO,TU,WE diff --git a/tests/assets/ics/rrules/rrules26.ics b/tests/assets/ics/rrules/rrules26.ics new file mode 100644 index 000000000..09ec2ec10 --- /dev/null +++ b/tests/assets/ics/rrules/rrules26.ics @@ -0,0 +1 @@ +FREQ=MONTHLY;BYDAY=3MO diff --git a/tests/assets/ics/rrules/rrules27.ics b/tests/assets/ics/rrules/rrules27.ics new file mode 100644 index 000000000..f80eb2613 --- /dev/null +++ b/tests/assets/ics/rrules/rrules27.ics @@ -0,0 +1 @@ +FREQ=MONTHLY;BYDAY=-3MO diff --git a/tests/assets/ics/rrules/rrules28.ics b/tests/assets/ics/rrules/rrules28.ics new file mode 100644 index 000000000..bf113fca5 --- /dev/null +++ b/tests/assets/ics/rrules/rrules28.ics @@ -0,0 +1 @@ +FREQ=MONTHLY;BYDAY=MO diff --git a/tests/assets/ics/rrules/rrules29.ics b/tests/assets/ics/rrules/rrules29.ics new file mode 100644 index 000000000..c32b0bbd1 --- /dev/null +++ b/tests/assets/ics/rrules/rrules29.ics @@ -0,0 +1 @@ +FREQ=YEARLY;BYDAY=MO,TU,WE diff --git a/tests/assets/ics/rrules/rrules3.ics b/tests/assets/ics/rrules/rrules3.ics new file mode 100644 index 000000000..d74c2ecd2 --- /dev/null +++ b/tests/assets/ics/rrules/rrules3.ics @@ -0,0 +1 @@ +FREQ=HOURLY diff --git a/tests/assets/ics/rrules/rrules30.ics b/tests/assets/ics/rrules/rrules30.ics new file mode 100644 index 000000000..6cc417036 --- /dev/null +++ b/tests/assets/ics/rrules/rrules30.ics @@ -0,0 +1 @@ +FREQ=YEARLY;BYDAY=3MO diff --git a/tests/assets/ics/rrules/rrules31.ics b/tests/assets/ics/rrules/rrules31.ics new file mode 100644 index 000000000..d7aebb37c --- /dev/null +++ b/tests/assets/ics/rrules/rrules31.ics @@ -0,0 +1 @@ +FREQ=YEARLY;BYDAY=-3MO diff --git a/tests/assets/ics/rrules/rrules32.ics b/tests/assets/ics/rrules/rrules32.ics new file mode 100644 index 000000000..eeafafb7d --- /dev/null +++ b/tests/assets/ics/rrules/rrules32.ics @@ -0,0 +1 @@ +FREQ=YEARLY;BYDAY=MO diff --git a/tests/assets/ics/rrules/rrules4.ics b/tests/assets/ics/rrules/rrules4.ics new file mode 100644 index 000000000..0a34332e3 --- /dev/null +++ b/tests/assets/ics/rrules/rrules4.ics @@ -0,0 +1 @@ +FREQ=DAILY;INTERVAL=5 diff --git a/tests/assets/ics/rrules/rrules5.ics b/tests/assets/ics/rrules/rrules5.ics new file mode 100644 index 000000000..d318a51db --- /dev/null +++ b/tests/assets/ics/rrules/rrules5.ics @@ -0,0 +1 @@ +FREQ=DAILY;INTERVAL=42;BYMONTH=1 diff --git a/tests/assets/ics/rrules/rrules6.ics b/tests/assets/ics/rrules/rrules6.ics new file mode 100644 index 000000000..4d8f81b63 --- /dev/null +++ b/tests/assets/ics/rrules/rrules6.ics @@ -0,0 +1 @@ +FREQ=WEEKLY diff --git a/tests/assets/ics/rrules/rrules7.ics b/tests/assets/ics/rrules/rrules7.ics new file mode 100644 index 000000000..d05549140 --- /dev/null +++ b/tests/assets/ics/rrules/rrules7.ics @@ -0,0 +1 @@ +FREQ=WEEKLY;BYDAY=MO,TU,WE diff --git a/tests/assets/ics/rrules/rrules8.ics b/tests/assets/ics/rrules/rrules8.ics new file mode 100644 index 000000000..2a20d671e --- /dev/null +++ b/tests/assets/ics/rrules/rrules8.ics @@ -0,0 +1 @@ +FREQ=WEEKLY;BYDAY=MO,2TU,-3WE diff --git a/tests/assets/ics/rrules/rrules9.ics b/tests/assets/ics/rrules/rrules9.ics new file mode 100644 index 000000000..922f7b9bd --- /dev/null +++ b/tests/assets/ics/rrules/rrules9.ics @@ -0,0 +1 @@ +FREQ=WEEKLY;BYDAY=MO,TU,WE;BYMONTH=1,2 diff --git a/tests/assets/ics/vcalendars-scheduling/add.ics b/tests/assets/ics/vcalendars-scheduling/add.ics new file mode 100644 index 000000000..bf14ce540 --- /dev/null +++ b/tests/assets/ics/vcalendars-scheduling/add.ics @@ -0,0 +1,18 @@ +BEGIN:VCALENDAR +METHOD:ADD +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +UID:123456789@example.com +SEQUENCE:2 +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;RSVP=TRUE:mailto:b@example.com +SUMMARY:Review Accounts +DTSTART:19980315T180000Z +DTEND:19980315T200000Z +DTSTAMP:19980307T193000Z +LOCATION:Conference Room A +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR diff --git a/tests/assets/ics/vcalendars-scheduling/cancel.ics b/tests/assets/ics/vcalendars-scheduling/cancel.ics new file mode 100644 index 000000000..e91cccdac --- /dev/null +++ b/tests/assets/ics/vcalendars-scheduling/cancel.ics @@ -0,0 +1,16 @@ +BEGIN:VCALENDAR +METHOD:CANCEL +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +UID:guid-1@example.com +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE:mailto:b@example.com +ATTENDEE:mailto:c@example.com +ATTENDEE:mailto:d@example.com +DTSTAMP:19970721T103000Z +STATUS:CANCELLED +SEQUENCE:3 +END:VEVENT +END:VCALENDAR diff --git a/tests/assets/ics/vcalendars-scheduling/counter.ics b/tests/assets/ics/vcalendars-scheduling/counter.ics new file mode 100644 index 000000000..3588c8a0f --- /dev/null +++ b/tests/assets/ics/vcalendars-scheduling/counter.ics @@ -0,0 +1,23 @@ +BEGIN:VCALENDAR +METHOD:COUNTER +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +UID:guid-1@example.com +RECURRENCE-ID:19970715T210000Z +SEQUENCE:4 +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;RSVP=TRUE:mailto:a@example.com +ATTENDEE;RSVP=TRUE:mailto:b@example.com +ATTENDEE;RSVP=TRUE:mailto:c@example.com +ATTENDEE;RSVP=TRUE:mailto:d@example.com +DESCRIPTION:IETF-C&S Conference Call +CLASS:PUBLIC +SUMMARY:IETF Calendaring Working Group Meeting +DTSTART:19970715T220000Z +DTEND:19970715T230000Z +LOCATION:Conference Call +COMMENT:May we bump this by an hour? I have a conflict +DTSTAMP:19970629T094000Z +END:VEVENT +END:VCALENDAR diff --git a/tests/assets/ics/vcalendars-scheduling/declinecounter.ics b/tests/assets/ics/vcalendars-scheduling/declinecounter.ics new file mode 100644 index 000000000..61c0f6754 --- /dev/null +++ b/tests/assets/ics/vcalendars-scheduling/declinecounter.ics @@ -0,0 +1,13 @@ +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +METHOD:DECLINECOUNTER +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL:mailto:b@example.com +COMMENT:Sorry, I cannot change this meeting time +UID:calsrv.example.com-873970198738777@example.com +SEQUENCE:0 +DTSTAMP:19970614T190000Z +END:VEVENT +END:VCALENDAR diff --git a/tests/assets/ics/vcalendars-scheduling/freebusy-reply.ics b/tests/assets/ics/vcalendars-scheduling/freebusy-reply.ics new file mode 100644 index 000000000..75284e331 --- /dev/null +++ b/tests/assets/ics/vcalendars-scheduling/freebusy-reply.ics @@ -0,0 +1,14 @@ +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +METHOD:REPLY +VERSION:2.0 +BEGIN:VFREEBUSY +ORGANIZER:mailto:a@example.com +ATTENDEE:mailto:b@example.com +DTSTART:19970701T080000Z +DTEND:19970701T200000Z +UID:calsrv.example.com-873970198738777@example.com +FREEBUSY:19970701T090000Z/PT1H,19970701T140000Z/PT30M +DTSTAMP:19970613T190030Z +END:VFREEBUSY +END:VCALENDAR diff --git a/tests/assets/ics/vcalendars-scheduling/freebusy-request.ics b/tests/assets/ics/vcalendars-scheduling/freebusy-request.ics new file mode 100644 index 000000000..079177c43 --- /dev/null +++ b/tests/assets/ics/vcalendars-scheduling/freebusy-request.ics @@ -0,0 +1,15 @@ +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +METHOD:REQUEST +VERSION:2.0 +BEGIN:VFREEBUSY +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR:mailto:a@example.com +ATTENDEE:mailto:b@example.com +ATTENDEE:mailto:c@example.com +DTSTAMP:19970613T190000Z +DTSTART:19970701T080000Z +DTEND:19970701T200000 +UID:calsrv.example.com-873970198738777@example.com +END:VFREEBUSY +END:VCALENDAR diff --git a/tests/assets/ics/vcalendars-scheduling/publish.ics b/tests/assets/ics/vcalendars-scheduling/publish.ics new file mode 100644 index 000000000..06425a3b4 --- /dev/null +++ b/tests/assets/ics/vcalendars-scheduling/publish.ics @@ -0,0 +1,18 @@ +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +METHOD:PUBLISH +BEGIN:VFREEBUSY +DTSTAMP:19980101T124100Z +ORGANIZER:mailto:a@example.com +DTSTART:19980101T124200Z +DTEND:19980108T124200Z +FREEBUSY:19980101T180000Z/19980101T190000Z +FREEBUSY:19980103T020000Z/19980103T050000Z +FREEBUSY:19980107T020000Z/19980107T050000Z +FREEBUSY:19980113T000000Z/19980113T010000Z +FREEBUSY:19980115T190000Z/19980115T200000Z +FREEBUSY:19980115T220000Z/19980115T230000Z +FREEBUSY:19980116T013000Z/19980116T043000Z +END:VFREEBUSY +END:VCALENDAR diff --git a/tests/assets/ics/vcalendars-scheduling/refresh.ics b/tests/assets/ics/vcalendars-scheduling/refresh.ics new file mode 100644 index 000000000..0e74599e1 --- /dev/null +++ b/tests/assets/ics/vcalendars-scheduling/refresh.ics @@ -0,0 +1,14 @@ +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +METHOD:REFRESH +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE:mailto:b@example.com +ATTENDEE:mailto:c@example.com +ATTENDEE:mailto:d@example.com +UID:guid-1-12345@example.com +DTSTAMP:19970603T094000 +END:VEVENT +END:VCALENDAR diff --git a/tests/assets/ics/vcalendars-scheduling/reply.ics b/tests/assets/ics/vcalendars-scheduling/reply.ics new file mode 100644 index 000000000..e10efe582 --- /dev/null +++ b/tests/assets/ics/vcalendars-scheduling/reply.ics @@ -0,0 +1,13 @@ +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +METHOD:REPLY +VERSION:2.0 +BEGIN:VEVENT +ATTENDEE;PARTSTAT=ACCEPTED:mailto:b@example.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777@example.com +SEQUENCE:0 +REQUEST-STATUS:2.0;Success +DTSTAMP:19970612T190000Z +END:VEVENT +END:VCALENDAR diff --git a/tests/assets/ics/vcalendars-scheduling/request.ics b/tests/assets/ics/vcalendars-scheduling/request.ics new file mode 100644 index 000000000..fb8a66e98 --- /dev/null +++ b/tests/assets/ics/vcalendars-scheduling/request.ics @@ -0,0 +1,19 @@ +BEGIN:VCALENDAR +METHOD:REQUEST +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +UID:123456789@example.com +SEQUENCE:1 +RECURRENCE-ID:19980311T180000Z +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;RSVP=TRUE:mailto:b@example.com +SUMMARY:Review Accounts +DTSTART:19980311T160000Z +DTEND:19980311T180000Z +DTSTAMP:19980306T193000Z +LOCATION:The Small conference room +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR diff --git a/tests/assets/ics/vcalendars/vcalendar-empty.ics b/tests/assets/ics/vcalendars/vcalendar-empty.ics new file mode 100644 index 000000000..b73a359a9 --- /dev/null +++ b/tests/assets/ics/vcalendars/vcalendar-empty.ics @@ -0,0 +1,5 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Tests// +CALSCALE:GREGORIAN +END:VCALENDAR diff --git a/tests/assets/ics/vcalendars/vcalendar-event-alarms.ics b/tests/assets/ics/vcalendars/vcalendar-event-alarms.ics new file mode 100644 index 000000000..5cdbdfa95 --- /dev/null +++ b/tests/assets/ics/vcalendars/vcalendar-event-alarms.ics @@ -0,0 +1,40 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Tests// +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:GMT+2 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:GMT+1 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +DTEND;TZID=Europe/Berlin:20160816T100000 +TRANSP:OPAQUE +SUMMARY:Test Europe Berlin +DTSTART;TZID=Europe/Berlin:20160816T090000 +DTSTAMP:20160809T163632Z +SEQUENCE:0 +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER;RELATED=START:P1DT9H +END:VALARM +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER;VALUE=DATE-TIME:20200306T083000Z +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/tests/assets/ics/vcalendars/vcalendar-event-allday.ics b/tests/assets/ics/vcalendars/vcalendar-event-allday.ics new file mode 100644 index 000000000..3456698a3 --- /dev/null +++ b/tests/assets/ics/vcalendars/vcalendar-event-allday.ics @@ -0,0 +1,15 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +CREATED:20161004T144433Z +UID:85560E76-1B0D-47E1-A735-21625767FCA4 +DTEND;VALUE=DATE:20161008 +TRANSP:TRANSPARENT +DTSTART;VALUE=DATE:20161005 +DTSTAMP:20161004T144437Z +SUMMARY:allday event +SEQUENCE:0 +END:VEVENT +END:VCALENDAR diff --git a/tests/assets/ics/vcalendars/vcalendar-event-attendees.ics b/tests/assets/ics/vcalendars/vcalendar-event-attendees.ics new file mode 100644 index 000000000..6d9d60909 --- /dev/null +++ b/tests/assets/ics/vcalendars/vcalendar-event-attendees.ics @@ -0,0 +1,37 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Tests// +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:GMT+2 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:GMT+1 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +DTEND;TZID=Europe/Berlin:20160816T100000 +TRANSP:OPAQUE +SUMMARY:Test Europe Berlin +DTSTART;TZID=Europe/Berlin:20160816T090000 +DTSTAMP:20160809T163632Z +SEQUENCE:0 +ORGANIZER;CN=John Smith:mailto:jsmith@example.com +ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT:mailto:jsmith@example.com +ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=TENTATIVE;CN=Henry Cabot:mailto:hcabot@example.com +ATTENDEE;ROLE=REQ-PARTICIPANT;DELEGATED-FROM="mailto:bob@example.com";PARTSTAT=ACCEPTED;CN=Jane Doe:mailto:jdoe@example.com +ATTENDEE;ROLE=NON-PARTICIPANT;PARTSTAT=DELEGATED;DELEGATED-TO="mailto:hcabot@example.com";CN=The Big Cheese:mailto:iamboss@example.com +END:VEVENT +END:VCALENDAR diff --git a/tests/assets/ics/vcalendars/vcalendar-event-categories.ics b/tests/assets/ics/vcalendars/vcalendar-event-categories.ics new file mode 100644 index 000000000..242ba00c3 --- /dev/null +++ b/tests/assets/ics/vcalendars/vcalendar-event-categories.ics @@ -0,0 +1,33 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Tests// +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:GMT+2 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:GMT+1 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +DTEND;TZID=Europe/Berlin:20160816T100000 +TRANSP:OPAQUE +SUMMARY:Test Europe Berlin +DTSTART;TZID=Europe/Berlin:20160816T090000 +DTSTAMP:20160809T163632Z +SEQUENCE:0 +CATEGORIES:BUSINESS,HUMAN RESOURCES +END:VEVENT +END:VCALENDAR diff --git a/tests/assets/ics/vcalendars/vcalendar-event-custom-color.ics b/tests/assets/ics/vcalendars/vcalendar-event-custom-color.ics new file mode 100644 index 000000000..dbca39fff --- /dev/null +++ b/tests/assets/ics/vcalendars/vcalendar-event-custom-color.ics @@ -0,0 +1,33 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Tests// +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:GMT+2 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:GMT+1 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +DTEND;TZID=Europe/Berlin:20160816T100000 +TRANSP:OPAQUE +SUMMARY:Test Europe Berlin +DTSTART;TZID=Europe/Berlin:20160816T090000 +DTSTAMP:20160809T163632Z +SEQUENCE:0 +COLOR:turquoise +END:VEVENT +END:VCALENDAR diff --git a/tests/assets/ics/vcalendars/vcalendar-event-floating-time.ics b/tests/assets/ics/vcalendars/vcalendar-event-floating-time.ics new file mode 100644 index 000000000..dd8d7acf7 --- /dev/null +++ b/tests/assets/ics/vcalendars/vcalendar-event-floating-time.ics @@ -0,0 +1,32 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Tests// +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:GMT+2 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:GMT+1 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +DTEND:20160816T100000 +TRANSP:OPAQUE +SUMMARY:Test Europe Berlin +DTSTART:20160816T090000 +DTSTAMP:20160809T163632Z +SEQUENCE:0 +END:VEVENT +END:VCALENDAR diff --git a/tests/assets/ics/vcalendars/vcalendar-event-multiple-rrules.ics b/tests/assets/ics/vcalendars/vcalendar-event-multiple-rrules.ics new file mode 100644 index 000000000..cdbdf6644 --- /dev/null +++ b/tests/assets/ics/vcalendars/vcalendar-event-multiple-rrules.ics @@ -0,0 +1,34 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Tests// +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:GMT+2 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:GMT+1 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +DTEND;TZID=Europe/Berlin:20160816T100000 +TRANSP:OPAQUE +SUMMARY:Test Europe Berlin +DTSTART;TZID=Europe/Berlin:20160816T090000 +DTSTAMP:20160809T163632Z +SEQUENCE:0 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU +RRULE:FREQ=DAILY;BYMONTH=1 +END:VEVENT +END:VCALENDAR diff --git a/tests/assets/ics/vcalendars/vcalendar-event-recurring-allday.ics b/tests/assets/ics/vcalendars/vcalendar-event-recurring-allday.ics new file mode 100644 index 000000000..f356516a1 --- /dev/null +++ b/tests/assets/ics/vcalendars/vcalendar-event-recurring-allday.ics @@ -0,0 +1,16 @@ +BEGIN:VCALENDAR +PRODID:-//IDN nextcloud.com//Calendar app 2.0.2//EN +CALSCALE:GREGORIAN +VERSION:2.0 +BEGIN:VEVENT +CREATED:20200401T142357Z +DTSTAMP:20200401T142406Z +LAST-MODIFIED:20200401T142406Z +SEQUENCE:2 +UID:4f7a5e63-6ae5-43da-b949-7bad490882c5 +DTSTART;VALUE=DATE:20200401 +DTEND;VALUE=DATE:20200402 +RRULE:FREQ=WEEKLY;BYDAY=WE +SUMMARY:Weekly test +END:VEVENT +END:VCALENDAR diff --git a/tests/assets/ics/vcalendars/vcalendar-event-recurring.ics b/tests/assets/ics/vcalendars/vcalendar-event-recurring.ics new file mode 100644 index 000000000..1e34cfbc0 --- /dev/null +++ b/tests/assets/ics/vcalendars/vcalendar-event-recurring.ics @@ -0,0 +1,85 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Tests// +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:GMT+2 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:GMT+1 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20160809T163629Z +DTSTAMP:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +SUMMARY:TEST +RRULE:FREQ=WEEKLY +DTSTART;TZID=Europe/Berlin:20200301T150000 +DTEND;TZID=Europe/Berlin:20200301T160000 +END:VEVENT +BEGIN:VEVENT +CREATED:20160809T163629Z +DTSTAMP:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +SUMMARY:TEST EX 1 +RECURRENCE-ID;TZID=Europe/Berlin:20200308T150000 +DTSTART;TZID=Europe/Berlin:20200401T150000 +DTEND;TZID=Europe/Berlin:20200401T160000 +END:VEVENT +BEGIN:VEVENT +CREATED:20160809T163629Z +DTSTAMP:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +SUMMARY:TEST EX 2 +RECURRENCE-ID;TZID=Europe/Berlin:20200315T150000 +DTSTART;TZID=Europe/Berlin:20201101T150000 +DTEND;TZID=Europe/Berlin:20201101T160000 +END:VEVENT +BEGIN:VEVENT +CREATED:20160809T163629Z +DTSTAMP:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +SUMMARY:TEST EX 3 +RECURRENCE-ID;TZID=Europe/Berlin:20200405T150000 +DTSTART;TZID=Europe/Berlin:20200406T150000 +DTEND;TZID=Europe/Berlin:20200406T160000 +END:VEVENT +BEGIN:VEVENT +CREATED:20160809T163629Z +DTSTAMP:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +SUMMARY:TEST EX 4 +RECURRENCE-ID;TZID=Europe/Berlin:20200412T150000 +DTSTART;TZID=Europe/Berlin:20201201T150000 +DTEND;TZID=Europe/Berlin:20201201T160000 +END:VEVENT +BEGIN:VEVENT +CREATED:20160809T163629Z +DTSTAMP:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +SUMMARY:TEST EX 5 +RECURRENCE-ID;TZID=Europe/Berlin:20200426T150000 +DTSTART;TZID=Europe/Berlin:20200410T150000 +DTEND;TZID=Europe/Berlin:20200410T160000 +END:VEVENT +BEGIN:VEVENT +CREATED:20160809T163629Z +DTSTAMP:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +SUMMARY:INVALID RECURRENCE-ID +RECURRENCE-ID;TZID=Europe/Berlin:20200427T150000 +DTSTART;TZID=Europe/Berlin:20200420T150000 +DTEND;TZID=Europe/Berlin:20200420T160000 +END:VEVENT +END:VCALENDAR diff --git a/tests/assets/ics/vcalendars/vcalendar-event-timed.ics b/tests/assets/ics/vcalendars/vcalendar-event-timed.ics new file mode 100644 index 000000000..1cf038a17 --- /dev/null +++ b/tests/assets/ics/vcalendars/vcalendar-event-timed.ics @@ -0,0 +1,32 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Tests// +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:GMT+2 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:GMT+1 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +DTEND;TZID=Europe/Berlin:20160816T100000 +TRANSP:OPAQUE +SUMMARY:Test Europe Berlin +DTSTART;TZID=Europe/Berlin:20160816T090000 +DTSTAMP:20160809T163632Z +SEQUENCE:0 +END:VEVENT +END:VCALENDAR diff --git a/tests/assets/ics/vcalendars/vcalendar-event-utc-time.ics b/tests/assets/ics/vcalendars/vcalendar-event-utc-time.ics new file mode 100644 index 000000000..4e2fe8f86 --- /dev/null +++ b/tests/assets/ics/vcalendars/vcalendar-event-utc-time.ics @@ -0,0 +1,32 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Tests// +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:GMT+2 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:GMT+1 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +DTEND:20160816T100000Z +TRANSP:OPAQUE +SUMMARY:Test Europe Berlin +DTSTART:20160816T090000Z +DTSTAMP:20160809T163632Z +SEQUENCE:0 +END:VEVENT +END:VCALENDAR diff --git a/tests/assets/ics/vcalendars/vcalendar-journal.ics b/tests/assets/ics/vcalendars/vcalendar-journal.ics new file mode 100644 index 000000000..4d0c8ebd6 --- /dev/null +++ b/tests/assets/ics/vcalendars/vcalendar-journal.ics @@ -0,0 +1,20 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Tests// +CALSCALE:GREGORIAN +BEGIN:VJOURNAL +UID:19970901T130000Z-123405@example.com +DTSTAMP:19970901T130000Z +DTSTART;VALUE=DATE:19970317 +SUMMARY:Staff meeting minutes +DESCRIPTION:1. Staff meeting: Participants include Joe\, + Lisa\, and Bob. Aurora project plans were reviewed. + There is currently no budget reserves for this project. + Lisa will escalate to management. Next meeting on Tuesday.\n +2. Telephone Conference: ABC Corp. sales representative + called to discuss new printer. Promised to get us a demo by + Friday.\n3. Henry Miller (Handsoff Insurance): Car was + totaled by tree. Is looking into a loaner car. 555-2323 + (tel). +END:VJOURNAL +END:VCALENDAR diff --git a/tests/assets/ics/vcalendars/vcalendar-todo.ics b/tests/assets/ics/vcalendars/vcalendar-todo.ics new file mode 100644 index 000000000..48fd9db26 --- /dev/null +++ b/tests/assets/ics/vcalendars/vcalendar-todo.ics @@ -0,0 +1,14 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Tests// +CALSCALE:GREGORIAN +BEGIN:VTODO +UID:20070313T123432Z-456553@example.com +DTSTAMP:20070313T123432Z +DUE;VALUE=DATE:20070501 +SUMMARY:Submit Quebec Income Tax Return for 2006 +CLASS:CONFIDENTIAL +CATEGORIES:FAMILY,FINANCE +STATUS:NEEDS-ACTION +END:VTODO +END:VCALENDAR diff --git a/tests/assets/ics/vcalendars/vcalendar-without-vobjects.ics b/tests/assets/ics/vcalendars/vcalendar-without-vobjects.ics new file mode 100644 index 000000000..7bc0b1ba8 --- /dev/null +++ b/tests/assets/ics/vcalendars/vcalendar-without-vobjects.ics @@ -0,0 +1,22 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Tests// +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:GMT+2 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:GMT+1 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +END:VCALENDAR diff --git a/tests/assets/loadAsset.js b/tests/assets/loadAsset.js new file mode 100644 index 000000000..fe0070491 --- /dev/null +++ b/tests/assets/loadAsset.js @@ -0,0 +1,94 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +import ICAL from 'ical.js' +import AlarmComponent from 'calendar-js/src/components/nested/alarmComponent.js' +import AttendeeProperty from "calendar-js/src/properties/attendeeProperty.js"; +import RecurValue from "calendar-js/src/values/recurValue.js"; +import {getParserManager} from "calendar-js"; + +const fs = require('fs') + +/** + * global helper function to load an ics asset by name + * + * @param {string} assetName + * @returns {string} + */ +global.loadICS = (assetName) => { + return fs.readFileSync('tests/assets/ics/' + assetName + '.ics', 'UTF8') +} + +/** + * Loads an AlarmComponent from an asset + * + * @param {String} assetName Name of the asset + * @returns {AlarmComponent} + */ +global.getAlarmComponentFromAsset = (assetName) => { + const ics = loadICS(assetName) + const jCal = ICAL.parse(ics.trim()) + const iCalComp = new ICAL.Component(jCal) + + return AlarmComponent.fromICALJs(iCalComp) +} + +/** + * Loads an AttendeeProperty from an asset + * + * @param {String} assetName Name of the asset + * @returns {AttendeeProperty} + */ +global.getAttendeePropertyFromAsset = (assetName) => { + const ics = loadICS(assetName) + const iCalProp = ICAL.Property.fromString(ics.trim()) + + return AttendeeProperty.fromICALJs(iCalProp) +} + +/** + * Loads a RecurValue from an asset + * + * @param {String} assetName Name of the asset + * @returns {RecurValue} + */ +global.getRecurValueFromAsset = (assetName) => { + const ics = loadICS(assetName) + const iCalValue = ICAL.Recur.fromString(ics.trim()) + + return RecurValue.fromICALJs(iCalValue) +} + +/** + * Loads an eventComponent from an asset + * + * @param {String} assetName Name of the asset + * @param {DateTimeValue} recurrenceId RecurrenceId of instance + */ +global.getEventComponentFromAsset = (assetName, recurrenceId) => { + const ics = loadICS(assetName) + const parser = getParserManager().getParserForFileType('text/calendar') + parser.parse(ics) + + const calendarComponent = parser.getAllItems()[0] + const firstVObject = Array.from(calendarComponent.getVObjectIterator())[0] + return firstVObject.recurrenceManager.getOccurrenceAtExactly(recurrenceId) +} diff --git a/tests/javascript/unit/fullcalendar/eventDrop.test.js b/tests/javascript/unit/fullcalendar/eventDrop.test.js index 69f372de7..88ef8553e 100644 --- a/tests/javascript/unit/fullcalendar/eventDrop.test.js +++ b/tests/javascript/unit/fullcalendar/eventDrop.test.js @@ -22,15 +22,18 @@ import eventDrop from "../../../../src/fullcalendar/eventDrop.js"; import { getDurationValueFromFullCalendarDuration} from "../../../../src/fullcalendar/duration.js"; import getTimezoneManager from '../../../../src/services/timezoneDataProviderService.js' +import {getObjectAtRecurrenceId} from "../../../../src/utils/calendarObject.js"; jest.mock("../../../../src/fullcalendar/duration.js") jest.mock('../../../../src/services/timezoneDataProviderService.js') +jest.mock("../../../../src/utils/calendarObject.js") describe('fullcalendar/eventDrop test suite', () => { beforeEach(() => { getDurationValueFromFullCalendarDuration.mockClear() getTimezoneManager.mockClear() + getObjectAtRecurrenceId.mockClear() }) it('should properly drop a non-recurring event', async () => { @@ -74,12 +77,14 @@ describe('fullcalendar/eventDrop test suite', () => { createRecurrenceException: jest.fn(), } const calendarObject = { - getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent), - resetToDav: jest.fn() + _isCalendarObject: true, } + getObjectAtRecurrenceId + .mockReturnValue(eventComponent) - store.dispatch.mockResolvedValueOnce(calendarObject) // getEventByObjectId - store.dispatch.mockResolvedValueOnce() // updateCalendarObject + store.dispatch + .mockResolvedValueOnce(calendarObject) // getEventByObjectId + .mockResolvedValueOnce() // updateCalendarObject const eventDropFunction = eventDrop(store, fcAPI) await eventDropFunction({ event, delta, revert }) @@ -104,7 +109,6 @@ describe('fullcalendar/eventDrop test suite', () => { expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(1) expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0) - expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0) expect(revert).toHaveBeenCalledTimes(0) }) @@ -149,13 +153,15 @@ describe('fullcalendar/eventDrop test suite', () => { canCreateRecurrenceExceptions: jest.fn().mockReturnValue(false), createRecurrenceException: jest.fn(), } + const calendarObject = { - getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent), - resetToDav: jest.fn() + _isCalendarObject: true, } + getObjectAtRecurrenceId + .mockReturnValue(eventComponent) store.dispatch.mockResolvedValueOnce(calendarObject) // getEventByObjectId - store.dispatch.mockResolvedValueOnce() // updateCalendarObject + .mockResolvedValueOnce() // updateCalendarObject const eventDropFunction = eventDrop(store, fcAPI) await eventDropFunction({ event, delta, revert }) @@ -184,7 +190,6 @@ describe('fullcalendar/eventDrop test suite', () => { expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(1) expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0) - expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0) expect(revert).toHaveBeenCalledTimes(0) }) @@ -229,9 +234,10 @@ describe('fullcalendar/eventDrop test suite', () => { createRecurrenceException: jest.fn(), } const calendarObject = { - getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent), - resetToDav: jest.fn() + _isCalendarObject: true, } + getObjectAtRecurrenceId + .mockReturnValue(eventComponent) store.dispatch.mockResolvedValueOnce(calendarObject) // getEventByObjectId store.dispatch.mockResolvedValueOnce() // updateCalendarObject @@ -260,7 +266,6 @@ describe('fullcalendar/eventDrop test suite', () => { expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(1) expect(eventComponent.createRecurrenceException).toHaveBeenNthCalledWith(1) - expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0) expect(revert).toHaveBeenCalledTimes(0) }) @@ -305,9 +310,10 @@ describe('fullcalendar/eventDrop test suite', () => { createRecurrenceException: jest.fn(), } const calendarObject = { - getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent), - resetToDav: jest.fn() + _isCalendarObject: true, } + getObjectAtRecurrenceId + .mockReturnValue(eventComponent) store.dispatch.mockResolvedValueOnce(calendarObject) // getEventByObjectId store.dispatch.mockResolvedValueOnce() // updateCalendarObject @@ -331,7 +337,6 @@ describe('fullcalendar/eventDrop test suite', () => { expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(0) expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0) - expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0) expect(revert).toHaveBeenCalledTimes(1) }) @@ -376,9 +381,10 @@ describe('fullcalendar/eventDrop test suite', () => { createRecurrenceException: jest.fn(), } const calendarObject = { - getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent), - resetToDav: jest.fn() + _isCalendarObject: true, } + getObjectAtRecurrenceId + .mockReturnValue(eventComponent) store.dispatch.mockResolvedValueOnce(calendarObject) // getEventByObjectId store.dispatch.mockResolvedValueOnce() // updateCalendarObject @@ -402,7 +408,6 @@ describe('fullcalendar/eventDrop test suite', () => { expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(0) expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0) - expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0) expect(revert).toHaveBeenCalledTimes(1) }) @@ -447,9 +452,10 @@ describe('fullcalendar/eventDrop test suite', () => { createRecurrenceException: jest.fn(), } const calendarObject = { - getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent), - resetToDav: jest.fn() + _isCalendarObject: true, } + getObjectAtRecurrenceId + .mockReturnValue(eventComponent) store.dispatch.mockResolvedValueOnce(calendarObject) // getEventByObjectId store.dispatch.mockResolvedValueOnce() // updateCalendarObject @@ -473,7 +479,6 @@ describe('fullcalendar/eventDrop test suite', () => { expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(0) expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0) - expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0) expect(revert).toHaveBeenCalledTimes(1) }) @@ -518,9 +523,10 @@ describe('fullcalendar/eventDrop test suite', () => { createRecurrenceException: jest.fn(), } const calendarObject = { - getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent), - resetToDav: jest.fn() + _isCalendarObject: true, } + getObjectAtRecurrenceId + .mockReturnValue(eventComponent) store.dispatch.mockRejectedValueOnce({ message: 'error message' }) // getEventByObjectId @@ -544,7 +550,6 @@ describe('fullcalendar/eventDrop test suite', () => { expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(0) expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0) - expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0) expect(revert).toHaveBeenCalledTimes(1) }) @@ -584,9 +589,10 @@ describe('fullcalendar/eventDrop test suite', () => { }) const calendarObject = { - getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(null), - resetToDav: jest.fn() + _isCalendarObject: true, } + getObjectAtRecurrenceId + .mockReturnValue(null) store.dispatch.mockResolvedValueOnce(calendarObject) // getEventByObjectId store.dispatch.mockResolvedValueOnce() // updateCalendarObject @@ -607,7 +613,6 @@ describe('fullcalendar/eventDrop test suite', () => { expect(store.dispatch).toHaveBeenCalledTimes(1) expect(store.dispatch).toHaveBeenNthCalledWith(1, 'getEventByObjectId', { objectId: 'object123' }) - expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0) expect(revert).toHaveBeenCalledTimes(1) }) @@ -654,13 +659,16 @@ describe('fullcalendar/eventDrop test suite', () => { createRecurrenceException: jest.fn(), } const calendarObject = { - getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent), - resetToDav: jest.fn() + _isCalendarObject: true, } + getObjectAtRecurrenceId + .mockReturnValue(eventComponent) store.dispatch.mockResolvedValueOnce(calendarObject) // getEventByObjectId store.dispatch.mockResolvedValueOnce() // updateCalendarObject + store.commit = jest.fn() + const eventDropFunction = eventDrop(store, fcAPI) await eventDropFunction({ event, delta, revert }) @@ -677,13 +685,15 @@ describe('fullcalendar/eventDrop test suite', () => { expect(store.dispatch).toHaveBeenCalledTimes(1) expect(store.dispatch).toHaveBeenNthCalledWith(1, 'getEventByObjectId', { objectId: 'object123' }) + expect(store.commit).toHaveBeenCalledTimes(1) + expect(store.commit).toHaveBeenNthCalledWith(1, 'resetCalendarObjectToDav', { calendarObject: calendarObject }) + expect(eventComponent.shiftByDuration).toHaveBeenCalledTimes(1) expect(eventComponent.shiftByDuration).toHaveBeenNthCalledWith(1, { calendarJsDurationValue: true, hours: 5 }, false, { calendarJsTimezone: true, tzid: 'America/New_York' }, { calendarJsDurationValue: true, days: 1 }, { calendarJsDurationValue: true, hours: 2 }) expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(0) expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0) - expect(calendarObject.resetToDav).toHaveBeenCalledTimes(1) expect(revert).toHaveBeenCalledTimes(1) }) @@ -728,15 +738,18 @@ describe('fullcalendar/eventDrop test suite', () => { createRecurrenceException: jest.fn(), } const calendarObject = { - getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent), - resetToDav: jest.fn() + _isCalendarObject: true, } + getObjectAtRecurrenceId + .mockReturnValue(eventComponent) store.dispatch.mockResolvedValueOnce(calendarObject) // getEventByObjectId store.dispatch.mockImplementationOnce(() => { throw new Error() }) // updateCalendarObject + store.commit = jest.fn() + const eventDropFunction = eventDrop(store, fcAPI) await eventDropFunction({ event, delta, revert }) @@ -754,13 +767,15 @@ describe('fullcalendar/eventDrop test suite', () => { expect(store.dispatch).toHaveBeenNthCalledWith(1, 'getEventByObjectId', { objectId: 'object123' }) expect(store.dispatch).toHaveBeenNthCalledWith(2, 'updateCalendarObject', { calendarObject }) + expect(store.commit).toHaveBeenCalledTimes(1) + expect(store.commit).toHaveBeenNthCalledWith(1, 'resetCalendarObjectToDav', { calendarObject: calendarObject }) + expect(eventComponent.shiftByDuration).toHaveBeenCalledTimes(1) expect(eventComponent.shiftByDuration).toHaveBeenNthCalledWith(1, { calendarJsDurationValue: true, hours: 5 }, false, { calendarJsTimezone: true, tzid: 'America/New_York' }, { calendarJsDurationValue: true, days: 1 }, { calendarJsDurationValue: true, hours: 2 }) expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(1) expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0) - expect(calendarObject.resetToDav).toHaveBeenCalledTimes(1) expect(revert).toHaveBeenCalledTimes(1) }) }) diff --git a/tests/javascript/unit/fullcalendar/eventResize.test.js b/tests/javascript/unit/fullcalendar/eventResize.test.js index 84d383b7c..97ea4cbad 100644 --- a/tests/javascript/unit/fullcalendar/eventResize.test.js +++ b/tests/javascript/unit/fullcalendar/eventResize.test.js @@ -22,12 +22,15 @@ import eventResize from "../../../../src/fullcalendar/eventResize.js"; import { getDurationValueFromFullCalendarDuration} from '../../../../src/fullcalendar/duration.js' +import {getObjectAtRecurrenceId} from "../../../../src/utils/calendarObject.js"; jest.mock('../../../../src/fullcalendar/duration.js') +jest.mock("../../../../src/utils/calendarObject.js") describe('fullcalendar/eventResize test suite', () => { beforeEach(() => { getDurationValueFromFullCalendarDuration.mockClear() + getObjectAtRecurrenceId.mockClear() }) it('should properly resize a non-recurring event', async () => { @@ -57,9 +60,10 @@ describe('fullcalendar/eventResize test suite', () => { createRecurrenceException: jest.fn(), } const calendarObject = { - getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent), - resetToDav: jest.fn() + _isCalendarObject: true, } + getObjectAtRecurrenceId + .mockReturnValue(eventComponent) store.dispatch .mockResolvedValueOnce(calendarObject) // getEventByObjectId @@ -84,7 +88,6 @@ describe('fullcalendar/eventResize test suite', () => { expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(1) expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0) - expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0) expect(revert).toHaveBeenCalledTimes(0) }) @@ -115,9 +118,10 @@ describe('fullcalendar/eventResize test suite', () => { createRecurrenceException: jest.fn(), } const calendarObject = { - getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent), - resetToDav: jest.fn() + _isCalendarObject: true, } + getObjectAtRecurrenceId + .mockReturnValue(eventComponent) store.dispatch .mockResolvedValueOnce(calendarObject) // getEventByObjectId @@ -142,7 +146,6 @@ describe('fullcalendar/eventResize test suite', () => { expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(1) expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(1) - expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0) expect(revert).toHaveBeenCalledTimes(0) }) @@ -171,9 +174,10 @@ describe('fullcalendar/eventResize test suite', () => { createRecurrenceException: jest.fn(), } const calendarObject = { - getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent), - resetToDav: jest.fn() + _isCalendarObject: true, } + getObjectAtRecurrenceId + .mockReturnValue(eventComponent) store.dispatch .mockResolvedValueOnce(calendarObject) // getEventByObjectId @@ -194,7 +198,6 @@ describe('fullcalendar/eventResize test suite', () => { expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(0) expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0) - expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0) expect(revert).toHaveBeenCalledTimes(1) }) @@ -225,9 +228,10 @@ describe('fullcalendar/eventResize test suite', () => { createRecurrenceException: jest.fn(), } const calendarObject = { - getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent), - resetToDav: jest.fn() + _isCalendarObject: true, } + getObjectAtRecurrenceId + .mockReturnValue(eventComponent) store.dispatch .mockImplementationOnce(() => { @@ -251,7 +255,6 @@ describe('fullcalendar/eventResize test suite', () => { expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(0) expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0) - expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0) expect(revert).toHaveBeenCalledTimes(1) }) @@ -282,9 +285,10 @@ describe('fullcalendar/eventResize test suite', () => { createRecurrenceException: jest.fn(), } const calendarObject = { - getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(null), - resetToDav: jest.fn() + _isCalendarObject: true, } + getObjectAtRecurrenceId + .mockReturnValue(null) store.dispatch .mockResolvedValueOnce(calendarObject) // getEventByObjectId @@ -306,7 +310,6 @@ describe('fullcalendar/eventResize test suite', () => { expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(0) expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0) - expect(calendarObject.resetToDav).toHaveBeenCalledTimes(0) expect(revert).toHaveBeenCalledTimes(1) }) @@ -337,9 +340,10 @@ describe('fullcalendar/eventResize test suite', () => { createRecurrenceException: jest.fn(), } const calendarObject = { - getObjectAtRecurrenceId: jest.fn().mockReturnValueOnce(eventComponent), - resetToDav: jest.fn() + _isCalendarObject: true, } + getObjectAtRecurrenceId + .mockReturnValue(eventComponent) store.dispatch .mockResolvedValueOnce(calendarObject) // getEventByObjectId @@ -347,6 +351,8 @@ describe('fullcalendar/eventResize test suite', () => { throw new Error() }) // updateCalendarObject + store.commit = jest.fn() + const eventResizeFunction = eventResize(store) await eventResizeFunction({ event, startDelta, endDelta, revert }) @@ -358,6 +364,9 @@ describe('fullcalendar/eventResize test suite', () => { expect(store.dispatch).toHaveBeenNthCalledWith(1, 'getEventByObjectId', { objectId: 'object123' }) expect(store.dispatch).toHaveBeenNthCalledWith(2, 'updateCalendarObject', { calendarObject }) + expect(store.commit).toHaveBeenCalledTimes(1) + expect(store.commit).toHaveBeenNthCalledWith(1, 'resetCalendarObjectToDav', { calendarObject: calendarObject }) + expect(eventComponent.addDurationToStart).toHaveBeenCalledTimes(1) expect(eventComponent.addDurationToStart).toHaveBeenNthCalledWith(1, { calendarJsDurationValue: true, hours: 5 }) @@ -366,7 +375,6 @@ describe('fullcalendar/eventResize test suite', () => { expect(eventComponent.canCreateRecurrenceExceptions).toHaveBeenCalledTimes(1) expect(eventComponent.createRecurrenceException).toHaveBeenCalledTimes(0) - expect(calendarObject.resetToDav).toHaveBeenCalledTimes(1) expect(revert).toHaveBeenCalledTimes(1) }) diff --git a/tests/javascript/unit/fullcalendar/eventSourceFunction.test.js b/tests/javascript/unit/fullcalendar/eventSourceFunction.test.js index 442e7f91a..8e17aaf2c 100644 --- a/tests/javascript/unit/fullcalendar/eventSourceFunction.test.js +++ b/tests/javascript/unit/fullcalendar/eventSourceFunction.test.js @@ -27,8 +27,10 @@ import { getHexForColorName, } from '../../../../src/utils/color.js' import { translate } from '@nextcloud/l10n' +import {getAllObjectsInTimeRange} from "../../../../src/utils/calendarObject.js"; jest.mock('@nextcloud/l10n') jest.mock('../../../../src/utils/color.js') +jest.mock("../../../../src/utils/calendarObject.js") describe('fullcalendar/eventSourceFunction test suite', () => { @@ -36,6 +38,7 @@ describe('fullcalendar/eventSourceFunction test suite', () => { translate.mockClear() getHexForColorName.mockClear() generateTextColorForHex.mockClear() + getAllObjectsInTimeRange.mockClear() }) it('should provide fc-events', () => { @@ -149,28 +152,26 @@ describe('fullcalendar/eventSourceFunction test suite', () => { color: 'red', }] + getAllObjectsInTimeRange + .mockReturnValueOnce(eventComponentSet1) + .mockReturnValueOnce(eventComponentSet2) + .mockImplementationOnce(() => { + throw new Error('Error while getting all objects in time-range') + }) + .mockReturnValueOnce(eventComponentSet4) + const calendarObjects = [{ calendarObject: true, id: '1', - getAllObjectsInTimeRange: jest.fn() - .mockReturnValueOnce(eventComponentSet1), }, { calendarObject: true, id: '2', - getAllObjectsInTimeRange: jest.fn() - .mockReturnValueOnce(eventComponentSet2), }, { calendarObject: true, id: '3', - getAllObjectsInTimeRange: jest.fn() - .mockImplementationOnce(() => { - throw new Error('Error while getting all objects in time-range') - }), }, { calendarObject: true, id: '4', - getAllObjectsInTimeRange: jest.fn() - .mockReturnValueOnce(eventComponentSet4), }] const start = new Date(Date.UTC(2019, 0, 1, 0, 0, 0, 0)) const end = new Date(Date.UTC(2020, 0, 31, 59, 59, 59, 999)) @@ -304,6 +305,12 @@ describe('fullcalendar/eventSourceFunction test suite', () => { expect(translate).toHaveBeenNthCalledWith(4, 'calendar', 'Untitled event') expect(translate).toHaveBeenNthCalledWith(5, 'calendar', 'Untitled event') + expect(getAllObjectsInTimeRange).toHaveBeenCalledTimes(4) + expect(getAllObjectsInTimeRange).toHaveBeenNthCalledWith(1, calendarObjects[0], start, end) + expect(getAllObjectsInTimeRange).toHaveBeenNthCalledWith(2, calendarObjects[1], start, end) + expect(getAllObjectsInTimeRange).toHaveBeenNthCalledWith(3, calendarObjects[2], start, end) + expect(getAllObjectsInTimeRange).toHaveBeenNthCalledWith(4, calendarObjects[3], start, end) + expect(getHexForColorName).toHaveBeenCalledTimes(1) expect(getHexForColorName).toHaveBeenNthCalledWith(1, 'red') diff --git a/tests/javascript/unit/models/alarm.test.js b/tests/javascript/unit/models/alarm.test.js new file mode 100644 index 000000000..57b6ab832 --- /dev/null +++ b/tests/javascript/unit/models/alarm.test.js @@ -0,0 +1,325 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +import {getDefaultAlarmObject, mapAlarmComponentToAlarmObject} from '../../../../src/models/alarm.js' +import { + getAmountAndUnitForTimedEvents, + getAmountHoursMinutesAndUnitForAllDayEvents +} from '../../../../src/utils/alarms.js' +import { getDateFromDateTimeValue } from '../../../../src/utils/date.js' + +jest.mock('../../../../src/utils/alarms.js') +jest.mock('../../../../src/utils/date.js') + +describe('Test suite: Alarm model (models/alarm.js)', () => { + + beforeEach(() => { + getAmountAndUnitForTimedEvents.mockClear() + getAmountHoursMinutesAndUnitForAllDayEvents.mockClear() + getDateFromDateTimeValue.mockClear() + }) + + it('should return a default alarm object', () => { + expect(getDefaultAlarmObject()).toEqual({ + alarmComponent: null, + type: null, + isRelative: false, + absoluteDate: null, + relativeIsBefore: null, + relativeIsRelatedToStart: null, + relativeUnitTimed: null, + relativeAmountTimed: null, + relativeUnitAllDay: null, + relativeAmountAllDay: null, + relativeHoursAllDay: null, + relativeMinutesAllDay: null, + relativeTrigger: null + }) + }) + + it('should fill up an object with default values', () => { + expect(getDefaultAlarmObject({ + type: 'DISPLAY', + otherProp: 'foo', + })).toEqual({ + alarmComponent: null, + type: 'DISPLAY', + isRelative: false, + absoluteDate: null, + relativeIsBefore: null, + relativeIsRelatedToStart: null, + relativeUnitTimed: null, + relativeAmountTimed: null, + relativeUnitAllDay: null, + relativeAmountAllDay: null, + relativeHoursAllDay: null, + relativeMinutesAllDay: null, + relativeTrigger: null, + otherProp: 'foo', + }) + }) + + it('should properly load an absolute alarm', () => { + const mockDate = new Date() + getDateFromDateTimeValue + .mockReturnValueOnce(mockDate) + + const alarmComponent = getAlarmComponentFromAsset('alarms/absoluteAlarm') + const alarmModel = mapAlarmComponentToAlarmObject(alarmComponent) + + expect(alarmModel).toEqual({ + alarmComponent, + type: 'DISPLAY', + isRelative: false, + absoluteDate: mockDate, + relativeIsBefore: null, + relativeIsRelatedToStart: null, + relativeUnitTimed: null, + relativeAmountTimed: null, + relativeUnitAllDay: null, + relativeAmountAllDay: null, + relativeHoursAllDay: null, + relativeMinutesAllDay: null, + relativeTrigger: null + }) + + expect(getDateFromDateTimeValue.mock.calls[0][0].jsDate.toISOString()).toEqual('2020-03-06T08:30:00.000Z') + + expect(getAmountAndUnitForTimedEvents).toHaveBeenCalledTimes(0) + expect(getAmountHoursMinutesAndUnitForAllDayEvents).toHaveBeenCalledTimes(0) + }) + + it('should properly load a relative alarm a week before the event', () => { + const alarmComponent = getAlarmComponentFromAsset('alarms/relativeAlarmWeekBefore') + + getAmountAndUnitForTimedEvents + .mockReturnValueOnce({ + amount: 159, + unit: 'hours', + }) + getAmountHoursMinutesAndUnitForAllDayEvents + .mockReturnValueOnce({ + amount: 1, + unit: 'weeks', + hours: 9, + minutes: 0 + }) + + const alarmModel = mapAlarmComponentToAlarmObject(alarmComponent) + + expect(alarmModel).toEqual({ + alarmComponent, + type: 'DISPLAY', + isRelative: true, + absoluteDate: null, + relativeIsBefore: true, + relativeIsRelatedToStart: true, + relativeUnitTimed: 'hours', + relativeAmountTimed: 159, + relativeUnitAllDay: 'weeks', + relativeAmountAllDay: 1, + relativeHoursAllDay: 9, + relativeMinutesAllDay: 0, + relativeTrigger: -572400, + }) + + expect(getAmountAndUnitForTimedEvents).toHaveBeenCalledTimes(1) + expect(getAmountAndUnitForTimedEvents).toHaveBeenNthCalledWith(1, -572400) + expect(getAmountHoursMinutesAndUnitForAllDayEvents).toHaveBeenCalledTimes(1) + expect(getAmountHoursMinutesAndUnitForAllDayEvents).toHaveBeenNthCalledWith(1, -572400) + }) + + + + + + + + + + + + + + + + it('should properly load a relative alarm days before the event', () => { + const alarmComponent = getAlarmComponentFromAsset('alarms/relativeAlarmBefore') + + getAmountAndUnitForTimedEvents + .mockReturnValueOnce({ + amount: 15, + unit: 'hours', + }) + getAmountHoursMinutesAndUnitForAllDayEvents + .mockReturnValueOnce({ + amount: 1, + unit: 'days', + hours: 9, + minutes: 0 + }) + + const alarmModel = mapAlarmComponentToAlarmObject(alarmComponent) + + expect(alarmModel).toEqual({ + alarmComponent, + type: 'DISPLAY', + isRelative: true, + absoluteDate: null, + relativeIsBefore: true, + relativeIsRelatedToStart: true, + relativeUnitTimed: 'hours', + relativeAmountTimed: 15, + relativeUnitAllDay: 'days', + relativeAmountAllDay: 1, + relativeHoursAllDay: 9, + relativeMinutesAllDay: 0, + relativeTrigger: -54000, + }) + + expect(getAmountAndUnitForTimedEvents).toHaveBeenCalledTimes(1) + expect(getAmountAndUnitForTimedEvents).toHaveBeenNthCalledWith(1, -54000) + expect(getAmountHoursMinutesAndUnitForAllDayEvents).toHaveBeenCalledTimes(1) + expect(getAmountHoursMinutesAndUnitForAllDayEvents).toHaveBeenNthCalledWith(1, -54000) + }) + + it('should properly load a relative alarm within 24 hours after the event', () => { + const alarmComponent = getAlarmComponentFromAsset('alarms/relativeAlarmAfterWithin24hours') + + getAmountAndUnitForTimedEvents + .mockReturnValueOnce({ + amount: 9, + unit: 'hours', + }) + getAmountHoursMinutesAndUnitForAllDayEvents + .mockReturnValueOnce({ + amount: 0, + unit: 'days', + hours: 9, + minutes: 0 + }) + + const alarmModel = mapAlarmComponentToAlarmObject(alarmComponent) + + expect(alarmModel).toEqual({ + alarmComponent, + type: 'DISPLAY', + isRelative: true, + absoluteDate: null, + relativeIsBefore: false, + relativeIsRelatedToStart: true, + relativeUnitTimed: 'hours', + relativeAmountTimed: 9, + relativeUnitAllDay: 'days', + relativeAmountAllDay: 0, + relativeHoursAllDay: 9, + relativeMinutesAllDay: 0, + relativeTrigger: 32400, + }) + + expect(getAmountAndUnitForTimedEvents).toHaveBeenCalledTimes(1) + expect(getAmountAndUnitForTimedEvents).toHaveBeenNthCalledWith(1, 32400) + expect(getAmountHoursMinutesAndUnitForAllDayEvents).toHaveBeenCalledTimes(1) + expect(getAmountHoursMinutesAndUnitForAllDayEvents).toHaveBeenNthCalledWith(1, 32400) + }) + + it('should properly load a relative alarm after the alarm', () => { + const alarmComponent = getAlarmComponentFromAsset('alarms/relativeAlarmAfter') + + getAmountAndUnitForTimedEvents + .mockReturnValueOnce({ + amount: 33, + unit: 'hours', + }) + getAmountHoursMinutesAndUnitForAllDayEvents + .mockReturnValueOnce({ + amount: 1, + unit: 'days', + hours: 9, + minutes: 0 + }) + + const alarmModel = mapAlarmComponentToAlarmObject(alarmComponent) + + expect(alarmModel).toEqual({ + alarmComponent, + type: 'DISPLAY', + isRelative: true, + absoluteDate: null, + relativeIsBefore: false, + relativeIsRelatedToStart: true, + relativeUnitTimed: 'hours', + relativeAmountTimed: 33, + relativeUnitAllDay: 'days', + relativeAmountAllDay: 1, + relativeHoursAllDay: 9, + relativeMinutesAllDay: 0, + relativeTrigger: 118800, + }) + + expect(getAmountAndUnitForTimedEvents).toHaveBeenCalledTimes(1) + expect(getAmountAndUnitForTimedEvents).toHaveBeenNthCalledWith(1, 118800) + expect(getAmountHoursMinutesAndUnitForAllDayEvents).toHaveBeenCalledTimes(1) + expect(getAmountHoursMinutesAndUnitForAllDayEvents).toHaveBeenNthCalledWith(1, 118800) + }) + + it('should properly load a relative alarm related to the end of the event', () => { + const alarmComponent = getAlarmComponentFromAsset('alarms/relativeAlarmRelatedEnd') + + getAmountAndUnitForTimedEvents + .mockReturnValueOnce({ + amount: 15, + unit: 'hours', + }) + getAmountHoursMinutesAndUnitForAllDayEvents + .mockReturnValueOnce({ + amount: 1, + unit: 'days', + hours: 9, + minutes: 0 + }) + + const alarmModel = mapAlarmComponentToAlarmObject(alarmComponent) + + expect(alarmModel).toEqual({ + alarmComponent, + type: 'DISPLAY', + isRelative: true, + absoluteDate: null, + relativeIsBefore: true, + relativeIsRelatedToStart: false, + relativeUnitTimed: 'hours', + relativeAmountTimed: 15, + relativeUnitAllDay: 'days', + relativeAmountAllDay: 1, + relativeHoursAllDay: 9, + relativeMinutesAllDay: 0, + relativeTrigger: -54000, + }) + + expect(getAmountAndUnitForTimedEvents).toHaveBeenCalledTimes(1) + expect(getAmountAndUnitForTimedEvents).toHaveBeenNthCalledWith(1, -54000) + expect(getAmountHoursMinutesAndUnitForAllDayEvents).toHaveBeenCalledTimes(1) + expect(getAmountHoursMinutesAndUnitForAllDayEvents).toHaveBeenNthCalledWith(1, -54000) + }) + +}) + diff --git a/tests/javascript/unit/models/attendee.test.js b/tests/javascript/unit/models/attendee.test.js new file mode 100644 index 000000000..ba83c20da --- /dev/null +++ b/tests/javascript/unit/models/attendee.test.js @@ -0,0 +1,159 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import {getDefaultAttendeeObject, mapAttendeePropertyToAttendeeObject} from "../../../../src/models/attendee.js"; + +describe('Test suite: Attendee model (models/attendee.js)', () => { + + it('should return a default attendee object', () => { + expect(getDefaultAttendeeObject()).toEqual({ + attendeeProperty: null, + commonName: null, + calendarUserType: 'INDIVIDUAL', + participationStatus: 'NEEDS-ACTION', + role: 'REQ-PARTICIPANT', + rsvp: false, + uri: null, + }) + }) + + it('should fill up an object with default values', () => { + expect(getDefaultAttendeeObject({ + participationStatus: 'ACCEPTED', + otherProp: 'foo', + })).toEqual({ + attendeeProperty: null, + commonName: null, + calendarUserType: 'INDIVIDUAL', + participationStatus: 'ACCEPTED', + role: 'REQ-PARTICIPANT', + rsvp: false, + uri: null, + otherProp: 'foo', + }) + }) + + it('should properly load an attendee (1/7)', () => { + const attendeeProperty = getAttendeePropertyFromAsset('attendees/attendee1') + const attendeeModel = mapAttendeePropertyToAttendeeObject(attendeeProperty) + + expect(attendeeModel).toEqual({ + attendeeProperty, + commonName: null, + calendarUserType: 'INDIVIDUAL', + participationStatus: 'NEEDS-ACTION', + role: 'REQ-PARTICIPANT', + rsvp: true, + uri: 'mailto:jsmith@example.com', + }) + }) + + it('should properly load an attendee (2/7)', () => { + const attendeeProperty = getAttendeePropertyFromAsset('attendees/attendee2') + const attendeeModel = mapAttendeePropertyToAttendeeObject(attendeeProperty) + + expect(attendeeModel).toEqual({ + attendeeProperty, + commonName: null, + calendarUserType: 'GROUP', + participationStatus: 'NEEDS-ACTION', + role: 'REQ-PARTICIPANT', + rsvp: false, + uri: 'mailto:ietf-calsch@example.org', + }) + }) + + it('should properly load an attendee (3/7)', () => { + const attendeeProperty = getAttendeePropertyFromAsset('attendees/attendee3') + const attendeeModel = mapAttendeePropertyToAttendeeObject(attendeeProperty) + + expect(attendeeModel).toEqual({ + attendeeProperty, + commonName: null, + calendarUserType: 'INDIVIDUAL', + participationStatus: 'DECLINED', + role: 'REQ-PARTICIPANT', + rsvp: false, + uri: 'mailto:jsmith@example.com', + }) + }) + + it('should properly load an attendee (4/7)', () => { + const attendeeProperty = getAttendeePropertyFromAsset('attendees/attendee4') + const attendeeModel = mapAttendeePropertyToAttendeeObject(attendeeProperty) + + expect(attendeeModel).toEqual({ + attendeeProperty, + commonName: null, + calendarUserType: 'INDIVIDUAL', + participationStatus: 'NEEDS-ACTION', + role: 'CHAIR', + rsvp: false, + uri: 'mailto:mrbig@example.com', + }) + }) + + it('should properly load an attendee (5/7)', () => { + const attendeeProperty = getAttendeePropertyFromAsset('attendees/attendee5') + const attendeeModel = mapAttendeePropertyToAttendeeObject(attendeeProperty) + + expect(attendeeModel).toEqual({ + attendeeProperty, + commonName: 'Henry Cabot', + calendarUserType: 'INDIVIDUAL', + participationStatus: 'TENTATIVE', + role: 'REQ-PARTICIPANT', + rsvp: false, + uri: 'mailto:hcabot@example.com', + }) + }) + + it('should properly load an attendee (6/7)', () => { + const attendeeProperty = getAttendeePropertyFromAsset('attendees/attendee6') + const attendeeModel = mapAttendeePropertyToAttendeeObject(attendeeProperty) + + expect(attendeeModel).toEqual({ + attendeeProperty, + commonName: 'The Big Cheese', + calendarUserType: 'INDIVIDUAL', + participationStatus: 'DELEGATED', + role: 'NON-PARTICIPANT', + rsvp: false, + uri: 'mailto:iamboss@example.com', + }) + }) + + it('should properly load an attendee (7/7)', () => { + const attendeeProperty = getAttendeePropertyFromAsset('attendees/attendee7') + const attendeeModel = mapAttendeePropertyToAttendeeObject(attendeeProperty) + + expect(attendeeModel).toEqual({ + attendeeProperty, + commonName: 'Jane Doe', + calendarUserType: 'INDIVIDUAL', + participationStatus: 'ACCEPTED', + role: 'REQ-PARTICIPANT', + rsvp: false, + uri: 'mailto:jdoe@example.com', + }) + }) +}) diff --git a/tests/javascript/unit/models/calendar.test.js b/tests/javascript/unit/models/calendar.test.js index cf7d4e9c8..902617bf6 100644 --- a/tests/javascript/unit/models/calendar.test.js +++ b/tests/javascript/unit/models/calendar.test.js @@ -22,10 +22,15 @@ import { getDefaultCalendarObject, mapDavCollectionToCalendar, - mapDavShareeToSharee } from '../../../../src/models/calendar.js' +import { mapDavShareeToCalendarShareObject } from "../../../../src/models/calendarShare.js"; +jest.mock("../../../../src/models/calendarShare.js") -describe('models/calendar test suite', () => { +describe('Test suite: Calendar model (models/calendar.js)', () => { + + beforeEach(() => { + mapDavShareeToCalendarShareObject.mockClear() + }) it('should provide an empty skeleton for calendar', () => { expect(getDefaultCalendarObject()).toEqual({ @@ -119,8 +124,13 @@ describe('models/calendar test suite', () => { supportsTasks: false, isSharedWithMe: false, timezone: 'BEGIN:VCALENDAR...END:VCALENDAR', - url: '/foo/bar' + url: '/foo/bar', + calendarObjects: [], + fetchedTimeRanges: [], + loading: false, }) + + expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0) }) it('should map a cdav-js calendar object to a calendar model - disabled calendar', () => { @@ -159,8 +169,13 @@ describe('models/calendar test suite', () => { supportsTasks: false, isSharedWithMe: false, timezone: 'BEGIN:VCALENDAR...END:VCALENDAR', - url: '/foo/bar' + url: '/foo/bar', + calendarObjects: [], + fetchedTimeRanges: [], + loading: false, }) + + expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0) }) it('should map a cdav-js calendar object to a calendar model - no enabled - own calendar', () => { @@ -198,8 +213,13 @@ describe('models/calendar test suite', () => { supportsTasks: false, isSharedWithMe: false, timezone: null, - url: '/foo/bar' + url: '/foo/bar', + calendarObjects: [], + fetchedTimeRanges: [], + loading: false, }) + + expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0) }) it('should map a cdav-js calendar object to a calendar model - no enabled - shared with me', () => { @@ -237,8 +257,13 @@ describe('models/calendar test suite', () => { supportsTasks: false, isSharedWithMe: true, timezone: null, - url: '/foo/bar' + url: '/foo/bar', + calendarObjects: [], + fetchedTimeRanges: [], + loading: false, }) + + expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0) }) it('should map a cdav-js calendar object to a calendar model - color without hash', () => { @@ -276,8 +301,13 @@ describe('models/calendar test suite', () => { supportsTasks: false, isSharedWithMe: false, timezone: null, - url: '/foo/bar' + url: '/foo/bar', + calendarObjects: [], + fetchedTimeRanges: [], + loading: false, }) + + expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0) }) it('should map a cdav-js calendar object to a calendar model - rgba color', () => { @@ -315,8 +345,13 @@ describe('models/calendar test suite', () => { supportsTasks: false, isSharedWithMe: false, timezone: null, - url: '/foo/bar' + url: '/foo/bar', + calendarObjects: [], + fetchedTimeRanges: [], + loading: false, }) + + expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0) }) it('should map a cdav-js calendar object to a calendar model - rgba color without hash', () => { @@ -354,8 +389,13 @@ describe('models/calendar test suite', () => { supportsTasks: false, isSharedWithMe: false, timezone: null, - url: '/foo/bar' + url: '/foo/bar', + calendarObjects: [], + fetchedTimeRanges: [], + loading: false, }) + + expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0) }) it('should map a cdav-js calendar object to a calendar model - unknown color', () => { @@ -393,11 +433,24 @@ describe('models/calendar test suite', () => { supportsTasks: false, isSharedWithMe: false, timezone: null, - url: '/foo/bar' + url: '/foo/bar', + calendarObjects: [], + fetchedTimeRanges: [], + loading: false, }) + + expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0) }) it('should properly parse sharees of a calendar', () => { + mapDavShareeToCalendarShareObject + .mockReturnValueOnce({ id: 'share1' }) + .mockReturnValueOnce({ id: 'share2' }) + .mockReturnValueOnce({ id: 'share3' }) + .mockReturnValueOnce({ id: 'share4' }) + .mockReturnValueOnce({ id: 'share5' }) + .mockReturnValue(null) + const cdavObject = { url: '/foo/bar', displayname: 'Displayname of calendar 123', @@ -469,41 +522,55 @@ describe('models/calendar test suite', () => { owner: '/remote.php/dav/principals/users/admin/', publishURL: null, readOnly: false, - shares: [{ - 'displayName': 'Marcus Beehler', - 'id': 'cHJpbmNpcGFsOnByaW5jaXBhbHMvdXNlcnMvdXNlcjQ=', - 'isCircle': false, - 'isGroup': false, - 'uri': 'principal:principals/users/user4', - 'writeable': false, - }, { - 'displayName': 'My personal circle', - 'id': 'cHJpbmNpcGFsOnByaW5jaXBhbHMvY2lyY2xlcy9jNDc5YzE0YmQ4MjQxNQ==', - 'isCircle': true, - 'isGroup': false, - 'uri': 'principal:principals/circles/c479c14bd82415', - 'writeable': false, - }, { - 'displayName': 'Whitney Anders', - 'id': 'cHJpbmNpcGFsOnByaW5jaXBhbHMvdXNlcnMvdXNlcjM=', - 'isCircle': false, - 'isGroup': false, - 'uri': 'principal:principals/users/user3', - 'writeable': true, - }, { - 'displayName': 'admin', - 'id': 'cHJpbmNpcGFsOnByaW5jaXBhbHMvZ3JvdXBzL2FkbWlu', - 'isCircle': false, - 'isGroup': true, - 'uri': 'principal:principals/groups/admin', - 'writeable': false, - }], + shares: [ + { id: 'share1' }, + { id: 'share2' }, + { id: 'share3' }, + { id: 'share4' }, + ], supportsEvents: true, supportsJournals: false, supportsTasks: false, isSharedWithMe: false, timezone: null, - url: '/foo/bar' + url: '/foo/bar', + calendarObjects: [], + fetchedTimeRanges: [], + loading: false, + }) + + expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(4) + expect(mapDavShareeToCalendarShareObject).toHaveBeenNthCalledWith(1, { + 'href': 'principal:principals/users/user4', + 'common-name': 'Marcus Beehler', + 'invite-accepted': true, + 'access': [ + '{http://owncloud.org/ns}read' + ] + }) + expect(mapDavShareeToCalendarShareObject).toHaveBeenNthCalledWith(2, { + 'href': 'principal:principals/circles/c479c14bd82415', + 'common-name': 'My personal circle', + 'invite-accepted': true, + 'access': [ + '{http://owncloud.org/ns}read' + ] + }) + expect(mapDavShareeToCalendarShareObject).toHaveBeenNthCalledWith(3, { + 'href': 'principal:principals/users/user3', + 'common-name': 'Whitney Anders', + 'invite-accepted': true, + 'access': [ + '{http://owncloud.org/ns}read-write' + ] + }) + expect(mapDavShareeToCalendarShareObject).toHaveBeenNthCalledWith(4, { + 'href': 'principal:principals/groups/admin', + 'common-name': '', + 'invite-accepted': true, + 'access': [ + '{http://owncloud.org/ns}read' + ] }) }) @@ -582,26 +649,13 @@ describe('models/calendar test suite', () => { supportsTasks: false, isSharedWithMe: true, timezone: null, - url: '/foo/bar' + url: '/foo/bar', + calendarObjects: [], + fetchedTimeRanges: [], + loading: false, }) - }) - it('should properly parse individual sharees', () => { - expect(mapDavShareeToSharee({ - 'href': 'principal:principals/circles/c479c14bd82415', - 'common-name': 'My personal circle', - 'invite-accepted': true, - 'access': [ - '{http://owncloud.org/ns}read' - ] - })).toEqual({ - 'displayName': 'My personal circle', - 'id': 'cHJpbmNpcGFsOnByaW5jaXBhbHMvY2lyY2xlcy9jNDc5YzE0YmQ4MjQxNQ==', - 'isCircle': true, - 'isGroup': false, - 'uri': 'principal:principals/circles/c479c14bd82415', - 'writeable': false - }) + expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0) }) it('should handle undefined displayname properly', () => { @@ -640,7 +694,10 @@ describe('models/calendar test suite', () => { supportsTasks: false, isSharedWithMe: false, timezone: 'BEGIN:VCALENDAR...END:VCALENDAR', - url: '/remote.php/dav/calendars/admin/personal/' + url: '/remote.php/dav/calendars/admin/personal/', + calendarObjects: [], + fetchedTimeRanges: [], + loading: false, }) }) diff --git a/tests/javascript/unit/models/calendarObject.test.js b/tests/javascript/unit/models/calendarObject.test.js index b75925f4b..1b1f36b8f 100644 --- a/tests/javascript/unit/models/calendarObject.test.js +++ b/tests/javascript/unit/models/calendarObject.test.js @@ -1,5 +1,5 @@ /** - * @copyright Copyright (c) 2019 Georg Ehrke + * @copyright Copyright (c) 2020 Georg Ehrke * * @author Georg Ehrke * @@ -19,11 +19,222 @@ * along with this program. If not, see . * */ +import { + getDefaultCalendarObjectObject, + mapCalendarJsToCalendarObject, + mapCDavObjectToCalendarObject +} from "../../../../src/models/calendarObject.js"; +import CalendarComponent from 'calendar-js/src/components/calendarComponent.js' +import FreeBusyComponent from 'calendar-js/src/components/root/freeBusyComponent.js' +import {getParserManager} from "calendar-js"; -describe('models/calendarObject test suite', () => { +describe('Test suite: Calendar object model (models/calendarObject.js)', () => { - it('should be true', () => { - expect(true).toEqual(true) + it('should return a default calendarObject object', () => { + expect(getDefaultCalendarObjectObject()).toEqual({ + calendarId: null, + dav: null, + calendarComponent: null, + uid: null, + uri: null, + objectType: null, + isEvent: false, + isJournal: false, + isTodo: false, + existsOnServer: false, + id: null, + }) + }) + + it('should fill up an object with default values', () => { + expect(getDefaultCalendarObjectObject({ + calendarId: 'foo', + otherProp: 'bar', + })).toEqual({ + calendarId: 'foo', + dav: null, + calendarComponent: null, + uid: null, + uri: null, + objectType: null, + isEvent: false, + isJournal: false, + isTodo: false, + existsOnServer: false, + otherProp: 'bar', + id: null, + }) + }) + + it('should map a c-dav calendar-object to calendar object - throw error for empty string', () => { + const dav = { + url: 'cdav-url', + data: '', + } + + expect(() => mapCDavObjectToCalendarObject(dav, 'calendar-id-123')) + .toThrowError(/^Empty calendar object$/); + }) + + it('should map a c-dav calendar-object to calendar object - throw error for empty calendar', () => { + const dav = { + url: 'cdav-url', + data: loadICS('vcalendars/vcalendar-empty'), + } + + expect(() => mapCDavObjectToCalendarObject(dav, 'calendar-id-123')) + .toThrowError(/^Empty calendar object$/); + }) + + it('should map a c-dav calendar-object to calendar object - throw error for no vobjects', () => { + const dav = { + url: 'cdav-url', + data: loadICS('vcalendars/vcalendar-without-vobjects'), + } + + expect(() => mapCDavObjectToCalendarObject(dav, 'calendar-id-123')) + .toThrowError(/^Empty calendar object$/); + }) + + it('should map c-dav calendar object to calendar object - vevent', () => { + const dav = { + url: 'cdav-url', + data: loadICS('vcalendars/vcalendar-event-timed'), + } + + expect(mapCDavObjectToCalendarObject(dav, 'calendar-id-123')).toEqual({ + id: 'Y2Rhdi11cmw=', + calendarComponent: expect.any(CalendarComponent), + calendarId: 'calendar-id-123', + dav, + existsOnServer: true, + isEvent: true, + isJournal: false, + isTodo: false, + objectType: 'VEVENT', + uid: '0AD16F58-01B3-463B-A215-FD09FC729A02', + uri: 'cdav-url', + }) + }) + + it('should map c-dav calendar object to calendar object - vjournal', () => { + const dav = { + url: 'cdav-url', + data: loadICS('vcalendars/vcalendar-journal'), + } + + expect(mapCDavObjectToCalendarObject(dav, 'calendar-id-123')).toEqual({ + id: 'Y2Rhdi11cmw=', + calendarComponent: expect.any(CalendarComponent), + calendarId: 'calendar-id-123', + dav, + existsOnServer: true, + isEvent: false, + isJournal: true, + isTodo: false, + objectType: 'VJOURNAL', + uid: '19970901T130000Z-123405@example.com', + uri: 'cdav-url', + }) + }) + + it('should map c-dav calendar object to calendar object - vtodo', () => { + const dav = { + url: 'cdav-url', + data: loadICS('vcalendars/vcalendar-todo'), + } + + expect(mapCDavObjectToCalendarObject(dav, 'calendar-id-123')).toEqual({ + id: 'Y2Rhdi11cmw=', + calendarComponent: expect.any(CalendarComponent), + calendarId: 'calendar-id-123', + dav, + existsOnServer: true, + isEvent: false, + isJournal: false, + isTodo: true, + objectType: 'VTODO', + uid: '20070313T123432Z-456553@example.com', + uri: 'cdav-url', + }) + }) + + it('should map a calendar-js calendar-object to calendar object - empty', () => { + const calendarComponent = CalendarComponent.fromEmpty() + + expect(() => mapCalendarJsToCalendarObject(calendarComponent)) + .toThrowError(/^Calendar object without vobjects$/); + }) + + it('should map a calendar-js calendar-object to calendar object - throw error for no vobjects', () => { + const calendarComponent = CalendarComponent.fromEmpty() + calendarComponent.addComponent(new FreeBusyComponent('VFREEBUSY')) + + expect(() => mapCalendarJsToCalendarObject(calendarComponent)) + .toThrowError(/^Calendar object without vobjects$/); + }) + + it('should map a calendar-js calendar-object to calendar object - vevent', () => { + const ics = loadICS('vcalendars/vcalendar-event-timed') + const parser = getParserManager().getParserForFileType('text/calendar') + parser.parse(ics) + + const calendarComponent = parser.getAllItems()[0] + expect(mapCalendarJsToCalendarObject(calendarComponent)).toEqual({ + id: null, + calendarComponent: expect.any(CalendarComponent), + calendarId: null, + dav: null, + existsOnServer: false, + isEvent: true, + isJournal: false, + isTodo: false, + objectType: 'VEVENT', + uid: '0AD16F58-01B3-463B-A215-FD09FC729A02', + uri: null, + }) + }) + + it('should map a calendar-js calendar-object to calendar object - vjournal', () => { + const ics = loadICS('vcalendars/vcalendar-journal') + const parser = getParserManager().getParserForFileType('text/calendar') + parser.parse(ics) + + const calendarComponent = parser.getAllItems()[0] + expect(mapCalendarJsToCalendarObject(calendarComponent, 'calendar-id-123')).toEqual({ + id: null, + calendarComponent: expect.any(CalendarComponent), + calendarId: 'calendar-id-123', + dav: null, + existsOnServer: false, + isEvent: false, + isJournal: true, + isTodo: false, + objectType: 'VJOURNAL', + uid: '19970901T130000Z-123405@example.com', + uri: null, + }) + }) + + it('should map a calendar-js calendar-object to calendar object - vtodo', () => { + const ics = loadICS('vcalendars/vcalendar-todo') + const parser = getParserManager().getParserForFileType('text/calendar') + parser.parse(ics) + + const calendarComponent = parser.getAllItems()[0] + expect(mapCalendarJsToCalendarObject(calendarComponent)).toEqual({ + id: null, + calendarComponent: expect.any(CalendarComponent), + calendarId: null, + dav: null, + existsOnServer: false, + isEvent: false, + isJournal: false, + isTodo: true, + objectType: 'VTODO', + uid: '20070313T123432Z-456553@example.com', + uri: null, + }) }) }) diff --git a/tests/javascript/unit/models/calendarObjectInstance.test.js b/tests/javascript/unit/models/calendarObjectInstance.test.js deleted file mode 100644 index 4e48307bd..000000000 --- a/tests/javascript/unit/models/calendarObjectInstance.test.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * @copyright Copyright (c) 2019 Georg Ehrke - * - * @author Georg Ehrke - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -describe('models/calendarOje test suite', () => { - - it('should be true', () => { - expect(true).toEqual(true) - }) - -}) diff --git a/tests/javascript/unit/models/calendarShare.test.js b/tests/javascript/unit/models/calendarShare.test.js new file mode 100644 index 000000000..e80ee6312 --- /dev/null +++ b/tests/javascript/unit/models/calendarShare.test.js @@ -0,0 +1,186 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { + getDefaultCalendarShareObject, + mapDavShareeToCalendarShareObject +} from "../../../../src/models/calendarShare.js"; + +describe('Test suite: Calendar share model (models/calendarShare.js)', () => { + + it('should return a default calendar share object', () => { + expect(getDefaultCalendarShareObject()).toEqual({ + id: null, + displayName: null, + writeable: false, + isUser: false, + isGroup: false, + isCircle: false, + uri: null, + }) + }) + + it('should fill up an object with default values', () => { + expect(getDefaultCalendarShareObject({ + id: 'id123', + otherProp: 'foo', + })).toEqual({ + id: 'id123', + displayName: null, + writeable: false, + isUser: false, + isGroup: false, + isCircle: false, + uri: null, + otherProp: 'foo', + }) + }) + + it('should map a dav sharee to a calendar share object - user', () => { + const davSharee = { + 'href': 'principal:principals/users/user4', + 'common-name': 'Marcus Beehler', + 'invite-accepted': true, + 'access': [ + '{http://owncloud.org/ns}read' + ] + } + + expect(mapDavShareeToCalendarShareObject(davSharee)).toEqual({ + id: 'cHJpbmNpcGFsOnByaW5jaXBhbHMvdXNlcnMvdXNlcjQ=', + displayName: 'Marcus Beehler', + writeable: false, + isUser: true, + isGroup: false, + isCircle: false, + uri: 'principal:principals/users/user4', + }) + }) + + it('should map a dav sharee to a calendar share object - user without displayname', () => { + const davSharee = { + 'href': 'principal:principals/users/user4', + 'common-name': '', + 'invite-accepted': true, + 'access': [ + '{http://owncloud.org/ns}read' + ] + } + + expect(mapDavShareeToCalendarShareObject(davSharee)).toEqual({ + id: 'cHJpbmNpcGFsOnByaW5jaXBhbHMvdXNlcnMvdXNlcjQ=', + displayName: 'user4', + writeable: false, + isUser: true, + isGroup: false, + isCircle: false, + uri: 'principal:principals/users/user4', + }) + }) + + it('should map a dav sharee to a calendar share object - group', () => { + const davSharee = { + 'href': 'principal:principals/groups/admin', + 'common-name': '', + 'invite-accepted': true, + 'access': [ + '{http://owncloud.org/ns}read' + ] + } + + expect(mapDavShareeToCalendarShareObject(davSharee)).toEqual({ + id: 'cHJpbmNpcGFsOnByaW5jaXBhbHMvZ3JvdXBzL2FkbWlu', + displayName: 'admin', + writeable: false, + isUser: false, + isGroup: true, + isCircle: false, + uri: 'principal:principals/groups/admin', + }) + }) + + it('should map a dav sharee to a calendar share object - circle', () => { + const davSharee = { + 'href': 'principal:principals/circles/c479c14bd82415', + 'common-name': 'My personal circle', + 'invite-accepted': true, + 'access': [ + '{http://owncloud.org/ns}read-write' + ] + } + + expect(mapDavShareeToCalendarShareObject(davSharee)).toEqual({ + id: 'cHJpbmNpcGFsOnByaW5jaXBhbHMvY2lyY2xlcy9jNDc5YzE0YmQ4MjQxNQ==', + displayName: 'My personal circle', + writeable: true, + isUser: false, + isGroup: false, + isCircle: true, + uri: 'principal:principals/circles/c479c14bd82415', + }) + }) + + it('should map a dav sharee to a calendar share object - circle without displayname', () => { + // This should never be the case. This test should just make sure it doesn't crash and always + // provides a displayname + const davSharee = { + 'href': 'principal:principals/circles/c479c14bd82415', + 'common-name': '', + 'invite-accepted': true, + 'access': [ + '{http://owncloud.org/ns}read-write' + ] + } + + expect(mapDavShareeToCalendarShareObject(davSharee)).toEqual({ + id: 'cHJpbmNpcGFsOnByaW5jaXBhbHMvY2lyY2xlcy9jNDc5YzE0YmQ4MjQxNQ==', + displayName: 'principal:principals/circles/c479c14bd82415', + writeable: true, + isUser: false, + isGroup: false, + isCircle: true, + uri: 'principal:principals/circles/c479c14bd82415', + }) + }) + + it('should properly handle sharee URIs with non-ascii characters', () => { + const davSharee = { + 'href': 'principal:principals/groups/מַזָּל טוֹב', + 'common-name': '', + 'invite-accepted': true, + 'access': [ + '{http://owncloud.org/ns}read' + ] + } + + expect(mapDavShareeToCalendarShareObject(davSharee)).toEqual({ + id: 'cHJpbmNpcGFsOnByaW5jaXBhbHMvZ3JvdXBzLyVENyU5RSVENiVCNyVENyU5NiVENiVCOCVENiVCQyVENyU5QyUyMCVENyU5OCVENyU5NSVENiVCOSVENyU5MQ==', + displayName: 'מַזָּל טוֹב', + writeable: false, + isUser: false, + isGroup: true, + isCircle: false, + uri: 'principal:principals/groups/מַזָּל טוֹב', + }) + }) + +}) diff --git a/tests/javascript/unit/models/contact.test.js b/tests/javascript/unit/models/contact.test.js index 79496ce2f..7a01d2de2 100644 --- a/tests/javascript/unit/models/contact.test.js +++ b/tests/javascript/unit/models/contact.test.js @@ -20,10 +20,44 @@ * */ -describe('models/contact test suite', () => { +import {getDefaultContactsObject} from "../../../../src/models/contact.js"; - it('should be true', () => { - expect(true).toEqual(true) +describe('Test suite: Contact model (models/contact.js)', () => { + + it('should return a default contacts object', () => { + expect(getDefaultContactsObject()).toEqual({ + name: null, + calendarUserType: 'INDIVIDUAL', + isUser: false, + userId: null, + hasPhoto: false, + photoUrl: null, + hasIcon: false, + iconClass: null, + emails: [], + language: null, + timezoneId: null, + }) + }) + + it('should fill up an object with default values', () => { + expect(getDefaultContactsObject({ + name: 'Contact name', + otherProp: 'foo', + })).toEqual({ + name: 'Contact name', + calendarUserType: 'INDIVIDUAL', + isUser: false, + userId: null, + hasPhoto: false, + photoUrl: null, + hasIcon: false, + iconClass: null, + emails: [], + language: null, + timezoneId: null, + otherProp: 'foo', + }) }) }) diff --git a/tests/javascript/unit/models/event.test.js b/tests/javascript/unit/models/event.test.js new file mode 100644 index 000000000..8110022ce --- /dev/null +++ b/tests/javascript/unit/models/event.test.js @@ -0,0 +1,863 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { getDefaultEventObject, mapEventComponentToEventObject } from "../../../../src/models/event.js"; +import { getDateFromDateTimeValue } from '../../../../src/utils/date.js' +import { getHexForColorName } from '../../../../src/utils/color.js' +import { mapAlarmComponentToAlarmObject } from '../../../../src/models/alarm.js' +import { mapAttendeePropertyToAttendeeObject } from '../../../../src/models/attendee.js' +import { getDefaultRecurrenceRuleObject, mapRecurrenceRuleValueToRecurrenceRuleObject } from '../../../../src/models/recurrenceRule.js' +import DateTimeValue from "calendar-js/src/values/dateTimeValue.js"; + +jest.mock('../../../../src/utils/date.js') +jest.mock('../../../../src/utils/color.js') +jest.mock('../../../../src/models/alarm.js') +jest.mock('../../../../src/models/attendee.js') +jest.mock('../../../../src/models/recurrenceRule.js') + +describe('Test suite: Event model (models/event.js)', () => { + + beforeEach(() => { + getDateFromDateTimeValue.mockClear() + getHexForColorName.mockClear() + mapAlarmComponentToAlarmObject.mockClear() + mapAttendeePropertyToAttendeeObject.mockClear() + mapRecurrenceRuleValueToRecurrenceRuleObject.mockClear() + getDefaultRecurrenceRuleObject.mockClear() + }) + + it('should return a default event object', () => { + getDefaultRecurrenceRuleObject + .mockReturnValueOnce({ + defaultRecurrenceObject: true + }) + + expect(getDefaultEventObject()).toEqual({ + eventComponent: null, + title: null, + startDate: null, + startTimezoneId: null, + endDate: null, + endTimezoneId: null, + isAllDay: false, + canModifyAllDay: true, + location: null, + description: null, + accessClass: null, + status: null, + timeTransparency: null, + recurrenceRule: { + defaultRecurrenceObject: true + }, + hasMultipleRRules: false, + isMasterItem: false, + isRecurrenceException: false, + forceThisAndAllFuture: false, + canCreateRecurrenceException: false, + attendees: [], + organizer: null, + alarms: [], + customColor: null, + categories: [], + }) + + expect(getDefaultRecurrenceRuleObject).toHaveBeenCalledTimes(1) + }) + + it('should fill up an object with default values', () => { + getDefaultRecurrenceRuleObject + .mockReturnValueOnce({ + defaultRecurrenceObject: true + }) + + expect(getDefaultEventObject({ + title: '123', + otherProp: 'foo', + })).toEqual({ + eventComponent: null, + title: '123', + startDate: null, + startTimezoneId: null, + endDate: null, + endTimezoneId: null, + isAllDay: false, + canModifyAllDay: true, + location: null, + description: null, + accessClass: null, + status: null, + timeTransparency: null, + recurrenceRule: { + defaultRecurrenceObject: true + }, + hasMultipleRRules: false, + isMasterItem: false, + isRecurrenceException: false, + forceThisAndAllFuture: false, + canCreateRecurrenceException: false, + attendees: [], + organizer: null, + alarms: [], + customColor: null, + categories: [], + otherProp: 'foo', + }) + + expect(getDefaultRecurrenceRuleObject).toHaveBeenCalledTimes(1) + }) + + it('should map an event component to an event object (1/nnn)', () => { + // Simple non-recurring event + const recurrenceId = DateTimeValue.fromJSDate(new Date(Date.UTC(2016, 7, 16, 7, 0, 0)), true) + const eventComponent = getEventComponentFromAsset('vcalendars/vcalendar-event-timed', recurrenceId) + + const mockDate1 = new Date() + const mockDate2 = new Date() + getDateFromDateTimeValue + .mockReturnValueOnce(mockDate1) + .mockReturnValueOnce(mockDate2) + + getDefaultRecurrenceRuleObject + .mockReturnValueOnce({ + defaultRecurrenceObject: true + }) + + expect(mapEventComponentToEventObject(eventComponent)).toEqual({ + eventComponent, + title: 'Test Europe Berlin', + startDate: mockDate1, + startTimezoneId: 'Europe/Berlin', + endDate: mockDate2, + endTimezoneId: 'Europe/Berlin', + isAllDay: false, + canModifyAllDay: true, + location: null, + description: null, + accessClass: 'PUBLIC', + status: null, + timeTransparency: 'OPAQUE', + recurrenceRule: { + defaultRecurrenceObject: true + }, + hasMultipleRRules: false, + isMasterItem: true, + isRecurrenceException: false, + forceThisAndAllFuture: false, + canCreateRecurrenceException: false, + attendees: [], + organizer: null, + alarms: [], + customColor: null, + categories: [], + }) + + expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(2) + expect(getDateFromDateTimeValue).toHaveBeenNthCalledWith(1, eventComponent.startDate) + expect(getDateFromDateTimeValue).toHaveBeenNthCalledWith(2, eventComponent.endDate) + + expect(getDefaultRecurrenceRuleObject).toHaveBeenCalledTimes(1) + }) + + it('should map an event component to an event object (2/nnn)', () => { + // Simple non-recurring event with attendees and organizer + const recurrenceId = DateTimeValue.fromJSDate(new Date(Date.UTC(2016, 7, 16, 7, 0, 0)), true) + const eventComponent = getEventComponentFromAsset('vcalendars/vcalendar-event-attendees', recurrenceId) + + const mockDate1 = new Date() + const mockDate2 = new Date() + getDateFromDateTimeValue + .mockReturnValueOnce(mockDate1) + .mockReturnValueOnce(mockDate2) + + mapAttendeePropertyToAttendeeObject + .mockReturnValueOnce('ATTENDEE1') + .mockReturnValueOnce('ATTENDEE2') + .mockReturnValueOnce('ATTENDEE3') + .mockReturnValueOnce('ATTENDEE4') + + getDefaultRecurrenceRuleObject + .mockReturnValueOnce({ + defaultRecurrenceObject: true + }) + + expect(mapEventComponentToEventObject(eventComponent)).toEqual({ + eventComponent, + title: 'Test Europe Berlin', + startDate: mockDate1, + startTimezoneId: 'Europe/Berlin', + endDate: mockDate2, + endTimezoneId: 'Europe/Berlin', + isAllDay: false, + canModifyAllDay: true, + location: null, + description: null, + accessClass: 'PUBLIC', + status: null, + timeTransparency: 'OPAQUE', + recurrenceRule: { + defaultRecurrenceObject: true + }, + hasMultipleRRules: false, + isMasterItem: true, + isRecurrenceException: false, + forceThisAndAllFuture: false, + canCreateRecurrenceException: false, + attendees: [ + 'ATTENDEE1', + 'ATTENDEE2', + 'ATTENDEE3', + 'ATTENDEE4', + ], + organizer: { + attendeeProperty: eventComponent.getFirstProperty('ORGANIZER'), + commonName: 'John Smith', + uri: 'mailto:jsmith@example.com', + }, + alarms: [], + customColor: null, + categories: [], + }) + + expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(2) + expect(getDateFromDateTimeValue).toHaveBeenNthCalledWith(1, eventComponent.startDate) + expect(getDateFromDateTimeValue).toHaveBeenNthCalledWith(2, eventComponent.endDate) + + const attendees = eventComponent.getAttendeeList() + expect(mapAttendeePropertyToAttendeeObject).toHaveBeenCalledTimes(4) + expect(mapAttendeePropertyToAttendeeObject).toHaveBeenNthCalledWith(1, attendees[0]) + expect(mapAttendeePropertyToAttendeeObject).toHaveBeenNthCalledWith(2, attendees[1]) + expect(mapAttendeePropertyToAttendeeObject).toHaveBeenNthCalledWith(3, attendees[2]) + expect(mapAttendeePropertyToAttendeeObject).toHaveBeenNthCalledWith(4, attendees[3]) + + expect(getDefaultRecurrenceRuleObject).toHaveBeenCalledTimes(1) + }) + + it('should map an event component to an event object (3/nnn)', () => { + // Simple non-recurring event with alarms + const recurrenceId = DateTimeValue.fromJSDate(new Date(Date.UTC(2016, 7, 16, 7, 0, 0)), true) + const eventComponent = getEventComponentFromAsset('vcalendars/vcalendar-event-alarms', recurrenceId) + + const mockDate1 = new Date() + const mockDate2 = new Date() + getDateFromDateTimeValue + .mockReturnValueOnce(mockDate1) + .mockReturnValueOnce(mockDate2) + + mapAlarmComponentToAlarmObject + .mockReturnValueOnce('ALARM1') + .mockReturnValueOnce('ALARM2') + + getDefaultRecurrenceRuleObject + .mockReturnValueOnce({ + defaultRecurrenceObject: true + }) + + expect(mapEventComponentToEventObject(eventComponent)).toEqual({ + eventComponent, + title: 'Test Europe Berlin', + startDate: mockDate1, + startTimezoneId: 'Europe/Berlin', + endDate: mockDate2, + endTimezoneId: 'Europe/Berlin', + isAllDay: false, + canModifyAllDay: true, + location: null, + description: null, + accessClass: 'PUBLIC', + status: null, + timeTransparency: 'OPAQUE', + recurrenceRule: { + defaultRecurrenceObject: true + }, + hasMultipleRRules: false, + isMasterItem: true, + isRecurrenceException: false, + forceThisAndAllFuture: false, + canCreateRecurrenceException: false, + attendees: [], + organizer: null, + alarms: [ + 'ALARM1', + 'ALARM2', + ], + customColor: null, + categories: [], + }) + + const alarms = eventComponent.getAlarmList() + expect(mapAlarmComponentToAlarmObject).toHaveBeenCalledTimes(2) + expect(mapAlarmComponentToAlarmObject).toHaveBeenNthCalledWith(1, alarms[0]) + expect(mapAlarmComponentToAlarmObject).toHaveBeenNthCalledWith(2, alarms[1]) + + expect(getDefaultRecurrenceRuleObject).toHaveBeenCalledTimes(1) + }) + + it('should map an event component to an event object (4/nnn)', () => { + // Simple non-recurring event with categories + const recurrenceId = DateTimeValue.fromJSDate(new Date(Date.UTC(2016, 7, 16, 7, 0, 0)), true) + const eventComponent = getEventComponentFromAsset('vcalendars/vcalendar-event-categories', recurrenceId) + + const mockDate1 = new Date() + const mockDate2 = new Date() + getDateFromDateTimeValue + .mockReturnValueOnce(mockDate1) + .mockReturnValueOnce(mockDate2) + + getDefaultRecurrenceRuleObject + .mockReturnValueOnce({ + defaultRecurrenceObject: true + }) + + expect(mapEventComponentToEventObject(eventComponent)).toEqual({ + eventComponent, + title: 'Test Europe Berlin', + startDate: mockDate1, + startTimezoneId: 'Europe/Berlin', + endDate: mockDate2, + endTimezoneId: 'Europe/Berlin', + isAllDay: false, + canModifyAllDay: true, + location: null, + description: null, + accessClass: 'PUBLIC', + status: null, + timeTransparency: 'OPAQUE', + recurrenceRule: { + defaultRecurrenceObject: true + }, + hasMultipleRRules: false, + isMasterItem: true, + isRecurrenceException: false, + forceThisAndAllFuture: false, + canCreateRecurrenceException: false, + attendees: [], + organizer: null, + alarms: [], + customColor: null, + categories: ['BUSINESS', 'HUMAN RESOURCES'], + }) + + expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(2) + expect(getDateFromDateTimeValue).toHaveBeenNthCalledWith(1, eventComponent.startDate) + expect(getDateFromDateTimeValue).toHaveBeenNthCalledWith(2, eventComponent.endDate) + + expect(getDefaultRecurrenceRuleObject).toHaveBeenCalledTimes(1) + }) + + it('should map an event component to an event object (5/nnn)', () => { + // Simple non-recurring event with custom color + const recurrenceId = DateTimeValue.fromJSDate(new Date(Date.UTC(2016, 7, 16, 7, 0, 0)), true) + const eventComponent = getEventComponentFromAsset('vcalendars/vcalendar-event-custom-color', recurrenceId) + + const mockDate1 = new Date() + const mockDate2 = new Date() + getDateFromDateTimeValue + .mockReturnValueOnce(mockDate1) + .mockReturnValueOnce(mockDate2) + + getHexForColorName + .mockReturnValueOnce('#eeffee') + + getDefaultRecurrenceRuleObject + .mockReturnValueOnce({ + defaultRecurrenceObject: true + }) + + expect(mapEventComponentToEventObject(eventComponent)).toEqual({ + eventComponent, + title: 'Test Europe Berlin', + startDate: mockDate1, + startTimezoneId: 'Europe/Berlin', + endDate: mockDate2, + endTimezoneId: 'Europe/Berlin', + isAllDay: false, + canModifyAllDay: true, + location: null, + description: null, + accessClass: 'PUBLIC', + status: null, + timeTransparency: 'OPAQUE', + recurrenceRule: { + defaultRecurrenceObject: true + }, + hasMultipleRRules: false, + isMasterItem: true, + isRecurrenceException: false, + forceThisAndAllFuture: false, + canCreateRecurrenceException: false, + attendees: [], + organizer: null, + alarms: [], + customColor: '#eeffee', + categories: [], + }) + + expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(2) + expect(getDateFromDateTimeValue).toHaveBeenNthCalledWith(1, eventComponent.startDate) + expect(getDateFromDateTimeValue).toHaveBeenNthCalledWith(2, eventComponent.endDate) + + expect(getHexForColorName).toHaveBeenCalledTimes(1) + expect(getHexForColorName).toHaveBeenNthCalledWith(1, 'turquoise') + + expect(getDefaultRecurrenceRuleObject).toHaveBeenCalledTimes(1) + }) + + it('should map an event component to an event object (6/nnn)', () => { + // Simple non-recurring event with custom color (unknown color) + const recurrenceId = DateTimeValue.fromJSDate(new Date(Date.UTC(2016, 7, 16, 7, 0, 0)), true) + const eventComponent = getEventComponentFromAsset('vcalendars/vcalendar-event-custom-color', recurrenceId) + + const mockDate1 = new Date() + const mockDate2 = new Date() + getDateFromDateTimeValue + .mockReturnValueOnce(mockDate1) + .mockReturnValueOnce(mockDate2) + + getHexForColorName + .mockReturnValueOnce(null) + + getDefaultRecurrenceRuleObject + .mockReturnValueOnce({ + defaultRecurrenceObject: true + }) + + expect(mapEventComponentToEventObject(eventComponent)).toEqual({ + eventComponent, + title: 'Test Europe Berlin', + startDate: mockDate1, + startTimezoneId: 'Europe/Berlin', + endDate: mockDate2, + endTimezoneId: 'Europe/Berlin', + isAllDay: false, + canModifyAllDay: true, + location: null, + description: null, + accessClass: 'PUBLIC', + status: null, + timeTransparency: 'OPAQUE', + recurrenceRule: { + defaultRecurrenceObject: true + }, + hasMultipleRRules: false, + isMasterItem: true, + isRecurrenceException: false, + forceThisAndAllFuture: false, + canCreateRecurrenceException: false, + attendees: [], + organizer: null, + alarms: [], + customColor: null, + categories: [], + }) + + expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(2) + expect(getDateFromDateTimeValue).toHaveBeenNthCalledWith(1, eventComponent.startDate) + expect(getDateFromDateTimeValue).toHaveBeenNthCalledWith(2, eventComponent.endDate) + + expect(getHexForColorName).toHaveBeenCalledTimes(1) + expect(getHexForColorName).toHaveBeenNthCalledWith(1, 'turquoise') + + expect(getDefaultRecurrenceRuleObject).toHaveBeenCalledTimes(1) + }) + + it('should map an event component to an event object (7/nnn)', () => { + // Simple non-recurring event with floating time + const recurrenceId = DateTimeValue.fromJSDate(new Date(Date.UTC(2016, 7, 16, 9, 0, 0)), true) + const eventComponent = getEventComponentFromAsset('vcalendars/vcalendar-event-floating-time', recurrenceId) + + const mockDate1 = new Date() + const mockDate2 = new Date() + getDateFromDateTimeValue + .mockReturnValueOnce(mockDate1) + .mockReturnValueOnce(mockDate2) + + getDefaultRecurrenceRuleObject + .mockReturnValueOnce({ + defaultRecurrenceObject: true + }) + + expect(mapEventComponentToEventObject(eventComponent)).toEqual({ + eventComponent, + title: 'Test Europe Berlin', + startDate: mockDate1, + startTimezoneId: 'floating', + endDate: mockDate2, + endTimezoneId: 'floating', + isAllDay: false, + canModifyAllDay: true, + location: null, + description: null, + accessClass: 'PUBLIC', + status: null, + timeTransparency: 'OPAQUE', + recurrenceRule: { + defaultRecurrenceObject: true + }, + hasMultipleRRules: false, + isMasterItem: true, + isRecurrenceException: false, + forceThisAndAllFuture: false, + canCreateRecurrenceException: false, + attendees: [], + organizer: null, + alarms: [], + customColor: null, + categories: [], + }) + + expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(2) + expect(getDateFromDateTimeValue).toHaveBeenNthCalledWith(1, eventComponent.startDate) + expect(getDateFromDateTimeValue).toHaveBeenNthCalledWith(2, eventComponent.endDate) + + expect(getDefaultRecurrenceRuleObject).toHaveBeenCalledTimes(1) + }) + + it('should map an event component to an event object (8/nnn)', () => { + // Simple non-recurring event with UTC + const recurrenceId = DateTimeValue.fromJSDate(new Date(Date.UTC(2016, 7, 16, 9, 0, 0)), true) + const eventComponent = getEventComponentFromAsset('vcalendars/vcalendar-event-utc-time', recurrenceId) + + const mockDate1 = new Date() + const mockDate2 = new Date() + getDateFromDateTimeValue + .mockReturnValueOnce(mockDate1) + .mockReturnValueOnce(mockDate2) + + getDefaultRecurrenceRuleObject + .mockReturnValueOnce({ + defaultRecurrenceObject: true + }) + + expect(mapEventComponentToEventObject(eventComponent)).toEqual({ + eventComponent, + title: 'Test Europe Berlin', + startDate: mockDate1, + startTimezoneId: 'UTC', + endDate: mockDate2, + endTimezoneId: 'UTC', + isAllDay: false, + canModifyAllDay: true, + location: null, + description: null, + accessClass: 'PUBLIC', + status: null, + timeTransparency: 'OPAQUE', + recurrenceRule: { + defaultRecurrenceObject: true + }, + hasMultipleRRules: false, + isMasterItem: true, + isRecurrenceException: false, + forceThisAndAllFuture: false, + canCreateRecurrenceException: false, + attendees: [], + organizer: null, + alarms: [], + customColor: null, + categories: [], + }) + + expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(2) + expect(getDateFromDateTimeValue).toHaveBeenNthCalledWith(1, eventComponent.startDate) + expect(getDateFromDateTimeValue).toHaveBeenNthCalledWith(2, eventComponent.endDate) + + expect(getDefaultRecurrenceRuleObject).toHaveBeenCalledTimes(1) + }) + + it('should map an event component to an event object (9/nnn)', () => { + // Simple non-recurring event (allDay) + const recurrenceId = DateTimeValue.fromJSDate(new Date(Date.UTC(2016, 9, 5, 0, 0, 0)), true) + const eventComponent = getEventComponentFromAsset('vcalendars/vcalendar-event-allday', recurrenceId) + + const mockDate1 = new Date() + const mockDate2 = new Date() + getDateFromDateTimeValue + .mockReturnValueOnce(mockDate1) + .mockReturnValueOnce(mockDate2) + + getDefaultRecurrenceRuleObject + .mockReturnValueOnce({ + defaultRecurrenceObject: true + }) + + expect(mapEventComponentToEventObject(eventComponent)).toEqual({ + eventComponent, + title: 'allday event', + startDate: mockDate1, + startTimezoneId: 'floating', + endDate: mockDate2, + endTimezoneId: 'floating', + isAllDay: true, + canModifyAllDay: true, + location: null, + description: null, + accessClass: 'PUBLIC', + status: null, + timeTransparency: 'TRANSPARENT', + recurrenceRule: { + defaultRecurrenceObject: true + }, + hasMultipleRRules: false, + isMasterItem: true, + isRecurrenceException: false, + forceThisAndAllFuture: false, + canCreateRecurrenceException: false, + attendees: [], + organizer: null, + alarms: [], + customColor: null, + categories: [], + }) + + expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(2) + expect(getDateFromDateTimeValue).toHaveBeenNthCalledWith(1, eventComponent.startDate) + // verify that DTEND was decremented by one day + expect(getDateFromDateTimeValue.mock.calls[1][0].year).toEqual(2016) + expect(getDateFromDateTimeValue.mock.calls[1][0].month).toEqual(10) + expect(getDateFromDateTimeValue.mock.calls[1][0].day).toEqual(7) + + expect(getDefaultRecurrenceRuleObject).toHaveBeenCalledTimes(1) + }) + + it('should map an event component to an event object (10/nnn)', () => { + // Recurring event (fork) + const recurrenceId = DateTimeValue.fromJSDate(new Date(Date.UTC(2020, 2, 22, 14, 0, 0)), true) + const eventComponent = getEventComponentFromAsset('vcalendars/vcalendar-event-recurring', recurrenceId) + + const mockDate1 = new Date() + const mockDate2 = new Date() + getDateFromDateTimeValue + .mockReturnValueOnce(mockDate1) + .mockReturnValueOnce(mockDate2) + + mapRecurrenceRuleValueToRecurrenceRuleObject + .mockReturnValueOnce('RRULE1') + + getDefaultRecurrenceRuleObject + .mockReturnValueOnce({ + defaultRecurrenceObject: true + }) + + expect(mapEventComponentToEventObject(eventComponent)).toEqual({ + eventComponent, + title: 'TEST', + startDate: mockDate1, + startTimezoneId: 'Europe/Berlin', + endDate: mockDate2, + endTimezoneId: 'Europe/Berlin', + isAllDay: false, + canModifyAllDay: false, + location: null, + description: null, + accessClass: 'PUBLIC', + status: null, + timeTransparency: 'OPAQUE', + recurrenceRule: 'RRULE1', + hasMultipleRRules: false, + isMasterItem: false, + isRecurrenceException: false, + forceThisAndAllFuture: false, + canCreateRecurrenceException: true, + attendees: [], + organizer: null, + alarms: [], + customColor: null, + categories: [], + }) + + expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(2) + expect(getDateFromDateTimeValue).toHaveBeenNthCalledWith(1, eventComponent.startDate) + expect(getDateFromDateTimeValue).toHaveBeenNthCalledWith(2, eventComponent.endDate) + + expect(mapRecurrenceRuleValueToRecurrenceRuleObject).toHaveBeenCalledTimes(1) + expect(mapRecurrenceRuleValueToRecurrenceRuleObject).toHaveBeenNthCalledWith(1, eventComponent.getFirstPropertyFirstValue('RRULE'), eventComponent.startDate) + + expect(getDefaultRecurrenceRuleObject).toHaveBeenCalledTimes(1) + }) + + it('should map an event component to an event object (11/nnn)', () => { + // Recurring event (recurrence-exception) + const recurrenceId = DateTimeValue.fromJSDate(new Date(Date.UTC(2020, 2, 15, 14, 0, 0)), true) + const eventComponent = getEventComponentFromAsset('vcalendars/vcalendar-event-recurring', recurrenceId) + + const mockDate1 = new Date() + const mockDate2 = new Date() + getDateFromDateTimeValue + .mockReturnValueOnce(mockDate1) + .mockReturnValueOnce(mockDate2) + + getDefaultRecurrenceRuleObject + .mockReturnValueOnce({ + defaultRecurrenceObject: true + }) + + expect(mapEventComponentToEventObject(eventComponent)).toEqual({ + eventComponent, + title: 'TEST EX 2', + startDate: mockDate1, + startTimezoneId: 'Europe/Berlin', + endDate: mockDate2, + endTimezoneId: 'Europe/Berlin', + isAllDay: false, + canModifyAllDay: false, + location: null, + description: null, + accessClass: 'PUBLIC', + status: null, + timeTransparency: 'OPAQUE', + recurrenceRule: { + defaultRecurrenceObject: true + }, + hasMultipleRRules: false, + isMasterItem: false, + isRecurrenceException: true, + forceThisAndAllFuture: false, + canCreateRecurrenceException: false, + attendees: [], + organizer: null, + alarms: [], + customColor: null, + categories: [], + }) + + expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(2) + expect(getDateFromDateTimeValue).toHaveBeenNthCalledWith(1, eventComponent.startDate) + expect(getDateFromDateTimeValue).toHaveBeenNthCalledWith(2, eventComponent.endDate) + + expect(getDefaultRecurrenceRuleObject).toHaveBeenCalledTimes(1) + }) + + it('should map an event component to an event object (12/nnn)', () => { + // Multiple Recurrence rules + const recurrenceId = DateTimeValue.fromJSDate(new Date(Date.UTC(2016, 7, 16, 7, 0, 0)), true) + const eventComponent = getEventComponentFromAsset('vcalendars/vcalendar-event-multiple-rrules', recurrenceId) + + const mockDate1 = new Date() + const mockDate2 = new Date() + getDateFromDateTimeValue + .mockReturnValueOnce(mockDate1) + .mockReturnValueOnce(mockDate2) + + mapRecurrenceRuleValueToRecurrenceRuleObject + .mockReturnValueOnce('RRULE1') + + getDefaultRecurrenceRuleObject + .mockReturnValueOnce({ + defaultRecurrenceObject: true + }) + + expect(mapEventComponentToEventObject(eventComponent)).toEqual({ + eventComponent, + title: 'Test Europe Berlin', + startDate: mockDate1, + startTimezoneId: 'Europe/Berlin', + endDate: mockDate2, + endTimezoneId: 'Europe/Berlin', + isAllDay: false, + canModifyAllDay: false, + location: null, + description: null, + accessClass: 'PUBLIC', + status: null, + timeTransparency: 'OPAQUE', + recurrenceRule: 'RRULE1', + hasMultipleRRules: true, + isMasterItem: false, + isRecurrenceException: false, + forceThisAndAllFuture: false, + canCreateRecurrenceException: true, + attendees: [], + organizer: null, + alarms: [], + customColor: null, + categories: [], + }) + + expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(2) + expect(getDateFromDateTimeValue).toHaveBeenNthCalledWith(1, eventComponent.startDate) + expect(getDateFromDateTimeValue).toHaveBeenNthCalledWith(2, eventComponent.endDate) + + expect(mapRecurrenceRuleValueToRecurrenceRuleObject).toHaveBeenCalledTimes(1) + expect(mapRecurrenceRuleValueToRecurrenceRuleObject).toHaveBeenNthCalledWith(1, eventComponent.getFirstPropertyFirstValue('RRULE'), eventComponent.startDate) + + expect(getDefaultRecurrenceRuleObject).toHaveBeenCalledTimes(1) + }) + + it('should map an event component to an event object (12/nnn)', () => { + // recurring daily event + const recurrenceId = DateTimeValue.fromJSDate(new Date(Date.UTC(2020, 3, 15, 0, 0, 0)), true) + const eventComponent = getEventComponentFromAsset('vcalendars/vcalendar-event-recurring-allday', recurrenceId) + + const mockDate1 = new Date() + const mockDate2 = new Date() + getDateFromDateTimeValue + .mockReturnValueOnce(mockDate1) + .mockReturnValueOnce(mockDate2) + + mapRecurrenceRuleValueToRecurrenceRuleObject + .mockReturnValueOnce('RRULE1') + + getDefaultRecurrenceRuleObject + .mockReturnValueOnce({ + defaultRecurrenceObject: true + }) + + expect(mapEventComponentToEventObject(eventComponent)).toEqual({ + eventComponent, + title: 'Weekly test', + startDate: mockDate1, + startTimezoneId: 'floating', + endDate: mockDate2, + endTimezoneId: 'floating', + isAllDay: true, + canModifyAllDay: false, + location: null, + description: null, + accessClass: 'PUBLIC', + status: null, + timeTransparency: 'OPAQUE', + recurrenceRule: 'RRULE1', + hasMultipleRRules: false, + isMasterItem: false, + isRecurrenceException: false, + forceThisAndAllFuture: false, + canCreateRecurrenceException: true, + attendees: [], + organizer: null, + alarms: [], + customColor: null, + categories: [], + }) + + expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(2) + expect(getDateFromDateTimeValue).toHaveBeenNthCalledWith(1, eventComponent.startDate) + // verify that DTEND was decremented by one day + expect(getDateFromDateTimeValue.mock.calls[1][0].year).toEqual(2020) + expect(getDateFromDateTimeValue.mock.calls[1][0].month).toEqual(4) + expect(getDateFromDateTimeValue.mock.calls[1][0].day).toEqual(15) + + expect(mapRecurrenceRuleValueToRecurrenceRuleObject).toHaveBeenCalledTimes(1) + expect(mapRecurrenceRuleValueToRecurrenceRuleObject).toHaveBeenNthCalledWith(1, eventComponent.getFirstPropertyFirstValue('RRULE'), eventComponent.startDate) + + expect(getDefaultRecurrenceRuleObject).toHaveBeenCalledTimes(1) + }) +}) diff --git a/tests/javascript/unit/models/principal.test.js b/tests/javascript/unit/models/principal.test.js index b856bca31..3547014dc 100644 --- a/tests/javascript/unit/models/principal.test.js +++ b/tests/javascript/unit/models/principal.test.js @@ -20,10 +20,253 @@ * */ -describe('models/principal test suite', () => { +import {getDefaultPrincipalObject, mapDavToPrincipal} from "../../../../src/models/principal.js"; - it('should be true', () => { - expect(true).toEqual(true) +describe('Test suite: Principal model (models/principal.js)', () => { + + it('should return a default principal object', () => { + expect(getDefaultPrincipalObject()).toEqual({ + id: null, + calendarUserType: 'INDIVIDUAL', + emailAddress: null, + displayname: null, + principalScheme: null, + userId: null, + url: null, + dav: null, + isCircle: false, + isUser: false, + isGroup: false, + isCalendarResource: false, + isCalendarRoom: false, + principalId: null, + }) }) + it('should fill up an object with default values', () => { + expect(getDefaultPrincipalObject({ + principalId: 'bar', + otherProp: 'foo', + })).toEqual({ + id: null, + calendarUserType: 'INDIVIDUAL', + emailAddress: null, + displayname: null, + principalScheme: null, + userId: null, + url: null, + dav: null, + isCircle: false, + isUser: false, + isGroup: false, + isCalendarResource: false, + isCalendarRoom: false, + principalId: 'bar', + otherProp: 'foo', + }) + }) + + it('should properly map a user-principal to principal-object', () => { + const dav = { + addressBookHomes: undefined, + calendarHomes: ['/remote.php/dav/calendars/jane.doe'], + calendarUserAddressSet: [], + calendarUserType: 'INDIVIDUAL', + displayname: 'Jane Doe', + email: 'jane.doe@example.com', + principalScheme: 'principal:principals/users/jane.doe', + principalUrl: '/remote.php/dav/principals/users/jane.doe/', + scheduleInbox: null, + scheduleOutbox: null, + url: '/remote.php/dav/principals/users/jane.doe/', + userId: 'legacy-jane-doe-uid' + } + + expect(mapDavToPrincipal(dav)).toEqual({ + id: 'L3JlbW90ZS5waHAvZGF2L3ByaW5jaXBhbHMvdXNlcnMvamFuZS5kb2Uv', + dav, + calendarUserType: 'INDIVIDUAL', + principalScheme: 'principal:principals/users/jane.doe', + emailAddress: 'jane.doe@example.com', + displayname: 'Jane Doe', + url: '/remote.php/dav/principals/users/jane.doe/', + isUser: true, + isGroup: false, + isCircle: false, + isCalendarResource: false, + isCalendarRoom: false, + principalId: 'jane.doe', + userId: 'legacy-jane-doe-uid', + }) + }) + + it('should properly map a group-principal to principal-object', () => { + const dav = { + addressBookHomes: undefined, + calendarHomes: [], + calendarUserAddressSet: [], + calendarUserType: 'GROUP', + displayname: 'Jane Doe', + email: null, + principalScheme: 'principal:principals/groups/jane.doe', + principalUrl: '/remote.php/dav/principals/groups/jane.doe/', + scheduleInbox: null, + scheduleOutbox: null, + url: '/remote.php/dav/principals/groups/jane.doe/', + userId: null, + } + + expect(mapDavToPrincipal(dav)).toEqual({ + id: 'L3JlbW90ZS5waHAvZGF2L3ByaW5jaXBhbHMvZ3JvdXBzL2phbmUuZG9lLw==', + dav, + calendarUserType: 'GROUP', + principalScheme: 'principal:principals/groups/jane.doe', + emailAddress: null, + displayname: 'Jane Doe', + url: '/remote.php/dav/principals/groups/jane.doe/', + isUser: false, + isGroup: true, + isCircle: false, + isCalendarResource: false, + isCalendarRoom: false, + principalId: 'jane.doe', + userId: null, + }) + }) + + it('should properly map a circle-principal to principal-object', () => { + const dav = { + addressBookHomes: undefined, + calendarHomes: [], + calendarUserAddressSet: [], + calendarUserType: 'GROUP', + displayname: 'Jane Doe', + email: null, + principalScheme: 'principal:principals/circles/CGAH82BAS285H', + principalUrl: '/remote.php/dav/principals/circles/CGAH82BAS285H/', + scheduleInbox: null, + scheduleOutbox: null, + url: '/remote.php/dav/principals/circles/CGAH82BAS285H/', + userId: null, + } + + expect(mapDavToPrincipal(dav)).toEqual({ + id: 'L3JlbW90ZS5waHAvZGF2L3ByaW5jaXBhbHMvY2lyY2xlcy9DR0FIODJCQVMyODVILw==', + dav, + calendarUserType: 'GROUP', + principalScheme: 'principal:principals/circles/CGAH82BAS285H', + emailAddress: null, + displayname: 'Jane Doe', + url: '/remote.php/dav/principals/circles/CGAH82BAS285H/', + isUser: false, + isGroup: false, + isCircle: true, + isCalendarResource: false, + isCalendarRoom: false, + principalId: 'CGAH82BAS285H', + userId: null, + }) + }) + + it('should properly map a calendar-resource-principal to principal-object', () => { + const dav = { + addressBookHomes: undefined, + calendarHomes: [], + calendarUserAddressSet: [], + calendarUserType: 'RESOURCE', + displayname: 'Projector 123', + email: 'projector-123@example.com', + principalScheme: 'principal:principals/calendar-resources/projector-123', + principalUrl: '/remote.php/dav/principals/calendar-resources/projector-123/', + scheduleInbox: null, + scheduleOutbox: null, + url: '/remote.php/dav/principals/calendar-resources/projector-123/', + userId: null, + } + + expect(mapDavToPrincipal(dav)).toEqual({ + id: 'L3JlbW90ZS5waHAvZGF2L3ByaW5jaXBhbHMvY2FsZW5kYXItcmVzb3VyY2VzL3Byb2plY3Rvci0xMjMv', + dav, + calendarUserType: 'RESOURCE', + principalScheme: 'principal:principals/calendar-resources/projector-123', + emailAddress: 'projector-123@example.com', + displayname: 'Projector 123', + url: '/remote.php/dav/principals/calendar-resources/projector-123/', + isUser: false, + isGroup: false, + isCircle: false, + isCalendarResource: true, + isCalendarRoom: false, + principalId: 'projector-123', + userId: null, + }) + }) + + it('should properly map a calendar-room-principal to principal-object', () => { + const dav = { + addressBookHomes: undefined, + calendarHomes: [], + calendarUserAddressSet: [], + calendarUserType: 'ROOM', + displayname: 'ROOM 123', + email: 'room-123@example.com', + principalScheme: 'principal:principals/calendar-rooms/room-123', + principalUrl: '/remote.php/dav/principals/calendar-rooms/room-123/', + scheduleInbox: null, + scheduleOutbox: null, + url: '/remote.php/dav/principals/calendar-rooms/room-123/', + userId: null, + } + + expect(mapDavToPrincipal(dav)).toEqual({ + id: 'L3JlbW90ZS5waHAvZGF2L3ByaW5jaXBhbHMvY2FsZW5kYXItcm9vbXMvcm9vbS0xMjMv', + dav, + calendarUserType: 'ROOM', + principalScheme: 'principal:principals/calendar-rooms/room-123', + emailAddress: 'room-123@example.com', + displayname: 'ROOM 123', + url: '/remote.php/dav/principals/calendar-rooms/room-123/', + isUser: false, + isGroup: false, + isCircle: false, + isCalendarResource: false, + isCalendarRoom: true, + principalId: 'room-123', + userId: null, + }) + }) + + it('should properly map a principal from an unknown backend to principal-object', () => { + const dav = { + addressBookHomes: undefined, + calendarHomes: ['/remote.php/dav/calendars/jane.doe'], + calendarUserAddressSet: [], + calendarUserType: 'OTHER', + displayname: 'Jane Doe', + email: 'jane.doe@example.com', + principalScheme: 'principal:principals/users-other/jane.doe', + principalUrl: '/remote.php/dav/principals/users-other/jane.doe/', + scheduleInbox: null, + scheduleOutbox: null, + url: '/remote.php/dav/principals/users-other/jane.doe/', + userId: null, + } + + expect(mapDavToPrincipal(dav)).toEqual({ + id: 'L3JlbW90ZS5waHAvZGF2L3ByaW5jaXBhbHMvdXNlcnMtb3RoZXIvamFuZS5kb2Uv', + dav, + calendarUserType: 'OTHER', + principalScheme: 'principal:principals/users-other/jane.doe', + emailAddress: 'jane.doe@example.com', + displayname: 'Jane Doe', + url: '/remote.php/dav/principals/users-other/jane.doe/', + isUser: false, + isGroup: false, + isCircle: false, + isCalendarResource: false, + isCalendarRoom: false, + principalId: null, + userId: null, + }) + }) }) diff --git a/tests/javascript/unit/models/recurrenceRule.test.js b/tests/javascript/unit/models/recurrenceRule.test.js new file mode 100644 index 000000000..085879ee2 --- /dev/null +++ b/tests/javascript/unit/models/recurrenceRule.test.js @@ -0,0 +1,927 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { + getDefaultRecurrenceRuleObject, + mapRecurrenceRuleValueToRecurrenceRuleObject +} from "../../../../src/models/recurrenceRule.js"; +import { getDateFromDateTimeValue } from '../../../../src/utils/date.js' +import DateTimeValue from "calendar-js/src/values/dateTimeValue.js"; + +jest.mock('../../../../src/utils/date.js') + +describe('Test suite: Recurrence Rule model (models/recurrenceRule.js)', () => { + + beforeEach(() => { + getDateFromDateTimeValue.mockClear() + }) + + it('should return a default recurrence rule object', () => { + expect(getDefaultRecurrenceRuleObject()).toEqual({ + recurrenceRuleValue: null, + frequency: 'NONE', + interval: 1, + count: null, + until: null, + byDay: [], + byMonth: [], + byMonthDay: [], + bySetPosition: null, + isUnsupported: false, + }) + }) + + it('should fill up an object with default values', () => { + expect(getDefaultRecurrenceRuleObject({ + frequency: 'DAILY', + interval: 42, + otherProp: 'foo', + })).toEqual({ + recurrenceRuleValue: null, + frequency: 'DAILY', + interval: 42, + count: null, + until: null, + byDay: [], + byMonth: [], + byMonthDay: [], + bySetPosition: null, + isUnsupported: false, + otherProp: 'foo', + }) + }) + + it('should properly load a recurrence-rule (1/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules1') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // unsupported SECONDLY + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'SECONDLY', + interval: 1, + count: null, + until: null, + byDay: [], + byMonth: [], + byMonthDay: [], + bySetPosition: null, + isUnsupported: true, + }) + }) + + it('should properly load a recurrence-rule (2/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules2') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // unsupported MINUTELY + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'MINUTELY', + interval: 1, + count: null, + until: null, + byDay: [], + byMonth: [], + byMonthDay: [], + bySetPosition: null, + isUnsupported: true, + }) + }) + + it('should properly load a recurrence-rule (3/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules3') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // unsupported HOURLY + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'HOURLY', + interval: 1, + count: null, + until: null, + byDay: [], + byMonth: [], + byMonthDay: [], + bySetPosition: null, + isUnsupported: true, + }) + }) + + it('should properly load a recurrence-rule (4/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules4') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // plain DAILY with INTERVAL + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'DAILY', + interval: 5, + count: null, + until: null, + byDay: [], + byMonth: [], + byMonthDay: [], + bySetPosition: null, + isUnsupported: false, + }) + }) + + it('should properly load a recurrence-rule (5/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules5') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // DAILY with unsupported BYMONTH + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'DAILY', + interval: 42, + count: null, + until: null, + byDay: [], + byMonth: [], + byMonthDay: [], + bySetPosition: null, + isUnsupported: true, + }) + }) + + it('should properly load a recurrence-rule (6/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules6') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // plain WEEKLY + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'WEEKLY', + interval: 1, + count: null, + until: null, + byDay: ['SU'], + byMonth: [], + byMonthDay: [], + bySetPosition: null, + isUnsupported: false, + }) + }) + + it('should properly load a recurrence-rule (7/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules7') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // WEEKLY with BYDAY + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'WEEKLY', + interval: 1, + count: null, + until: null, + byDay: ['MO', 'TU', 'WE'], + byMonth: [], + byMonthDay: [], + bySetPosition: null, + isUnsupported: false, + }) + }) + + it('should properly load a recurrence-rule (8/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules8') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // WEEKLY with unsupported BYDAY + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'WEEKLY', + interval: 1, + count: null, + until: null, + byDay: ['MO'], + byMonth: [], + byMonthDay: [], + bySetPosition: null, + isUnsupported: true, + }) + }) + + it('should properly load a recurrence-rule (9/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules9') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // WEEKLY with BYDAY and unsupported BYMONYH + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'WEEKLY', + interval: 1, + count: null, + until: null, + byDay: ['MO', 'TU', 'WE'], + byMonth: [], + byMonthDay: [], + bySetPosition: null, + isUnsupported: true, + }) + }) + + it('should properly load a recurrence-rule (10/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules10') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // plain MONTHLY + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'MONTHLY', + interval: 1, + count: null, + until: null, + byDay: [], + byMonth: [], + byMonthDay: ['15'], + bySetPosition: null, + isUnsupported: false, + }) + }) + + it('should properly load a recurrence-rule (11/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules11') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // MONTHLY with BYMONTHDAY + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'MONTHLY', + interval: 1, + count: null, + until: null, + byDay: [], + byMonth: [], + byMonthDay: ['1', '2', '3', '30', '31'], + bySetPosition: null, + isUnsupported: false, + }) + }) + + it('should properly load a recurrence-rule (12/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules12') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // MONTHLY with invalid BYMONTHDAY + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'MONTHLY', + interval: 1, + count: null, + until: null, + byDay: [], + byMonth: [], + byMonthDay: ['2', '30', '31'], + bySetPosition: null, + isUnsupported: true, + }) + }) + + it('should properly load a recurrence-rule (13/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules13') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // MONTHLY with BYMONTHDAY, BYDAY, BYSETPOS + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'MONTHLY', + interval: 1, + count: null, + until: null, + byDay: [], + byMonth: [], + byMonthDay: ['1', '2', '3', '30', '31'], + bySetPosition: null, + isUnsupported: true, + }) + }) + + it('should properly load a recurrence-rule (14/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules14') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // MONTHLY with BYDAY and BYSETPOS + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'MONTHLY', + interval: 1, + count: null, + until: null, + byDay: ['MO'], + byMonth: [], + byMonthDay: [], + bySetPosition: 3, + isUnsupported: false, + }) + }) + + it('should properly load a recurrence-rule (15/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules15') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // MONTHLY with BYDAY and BYSETPOS, unsupported BYDAY + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'MONTHLY', + interval: 1, + count: null, + until: null, + byDay: ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'], + byMonth: [], + byMonthDay: [], + bySetPosition: 3, + isUnsupported: true, + }) + }) + + it('should properly load a recurrence-rule (16/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules16') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // MONTHLY with BYDAY and BYSETPOS, unsupported BYSETPOS + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'MONTHLY', + interval: 1, + count: null, + until: null, + byDay: ['MO'], + byMonth: [], + byMonthDay: [], + bySetPosition: 1, + isUnsupported: true, + }) + }) + + it('should properly load a recurrence-rule (17/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules17') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // MONTHLY with BYDAY and BYSETPOS, unsupported multiple BYSETPOS + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'MONTHLY', + interval: 1, + count: null, + until: null, + byDay: ['MO'], + byMonth: [], + byMonthDay: [], + bySetPosition: 1, + isUnsupported: true, + }) + }) + + it('should properly load a recurrence-rule (18/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules18') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // plain YEARLY + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'YEARLY', + interval: 1, + count: null, + until: null, + byDay: [], + byMonth: ['3'], + byMonthDay: [], + bySetPosition: null, + isUnsupported: false, + }) + }) + + it('should properly load a recurrence-rule (19/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules19') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // YEARLY with BYMONTH + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'YEARLY', + interval: 1, + count: null, + until: null, + byDay: [], + byMonth: ['1', '2', '3'], + byMonthDay: [], + bySetPosition: null, + isUnsupported: false, + }) + }) + + it('should properly load a recurrence-rule (20/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules20') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // YEARLY with invalid BYMONTH + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'YEARLY', + interval: 1, + count: null, + until: null, + byDay: [], + byMonth: ['1', '2', '3'], + byMonthDay: [], + bySetPosition: null, + isUnsupported: true, + }) + }) + + it('should properly load a recurrence-rule (21/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules21') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // YEARLY with BYDAY and BYSETPOS + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'YEARLY', + interval: 1, + count: null, + until: null, + byDay: ['MO'], + byMonth: ['3'], + byMonthDay: [], + bySetPosition: 3, + isUnsupported: false, + }) + }) + + it('should properly load a recurrence-rule (22/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules22') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // YEARLY with BYDAY and BYSETPOS, unsupported BYDAY + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'YEARLY', + interval: 1, + count: null, + until: null, + byDay: ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'], + byMonth: ['3'], + byMonthDay: [], + bySetPosition: 3, + isUnsupported: true, + }) + }) + + it('should properly load a recurrence-rule (23/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules23') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // YEARLY with BYDAY and BYSETPOS, unsupported BYSETPOS + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'YEARLY', + interval: 1, + count: null, + until: null, + byDay: ['MO'], + byMonth: ['3'], + byMonthDay: [], + bySetPosition: 1, + isUnsupported: true, + }) + }) + + it('should properly load a recurrence-rule (24/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules24') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // YEARLY with BYDAY and BYSETPOS, unsupported multiple BYSETPOS + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'YEARLY', + interval: 1, + count: null, + until: null, + byDay: ['MO'], + byMonth: ['3'], + byMonthDay: [], + bySetPosition: 1, + isUnsupported: true, + }) + }) + + it('should properly load a recurrence-rule (25/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules25') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // MONTHLY with multiple BYDAY + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'MONTHLY', + interval: 1, + count: null, + until: null, + byDay: [], + byMonth: [], + byMonthDay: ['15'], + bySetPosition: null, + isUnsupported: true, + }) + }) + + it('should properly load a recurrence-rule (26/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules26') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // MONTHLY with BYDAY that includes a position + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'MONTHLY', + interval: 1, + count: null, + until: null, + byDay: ['MO'], + byMonth: [], + byMonthDay: [], + bySetPosition: 3, + isUnsupported: false, + }) + }) + + it('should properly load a recurrence-rule (27/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules27') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // MONTHLY with BYDAY that includes a negative position + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'MONTHLY', + interval: 1, + count: null, + until: null, + byDay: ['MO'], + byMonth: [], + byMonthDay: [], + bySetPosition: 1, + isUnsupported: true, + }) + }) + + it('should properly load a recurrence-rule (28/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules28') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // MONTHLY with BYDAY (only weekday) + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'MONTHLY', + interval: 1, + count: null, + until: null, + byDay: [], + byMonth: [], + byMonthDay: ['15'], + bySetPosition: null, + isUnsupported: true, + }) + }) + + it('should properly load a recurrence-rule (29/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules29') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // YEARLY with multiple BYDAY + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'YEARLY', + interval: 1, + count: null, + until: null, + byDay: [], + byMonth: ['3'], + byMonthDay: [], + bySetPosition: null, + isUnsupported: true, + }) + }) + + it('should properly load a recurrence-rule (30/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules30') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // YEARLY with BYDAY that includes a position + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'YEARLY', + interval: 1, + count: null, + until: null, + byDay: ['MO'], + byMonth: ['3'], + byMonthDay: [], + bySetPosition: 3, + isUnsupported: false, + }) + }) + + it('should properly load a recurrence-rule (31/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules31') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // YEARLY with BYDAY that includes a negative position + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'YEARLY', + interval: 1, + count: null, + until: null, + byDay: ['MO'], + byMonth: ['3'], + byMonthDay: [], + bySetPosition: 1, + isUnsupported: true, + }) + }) + + it('should properly load a recurrence-rule (32/24)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrules32') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // YEARLY with BYDAY (only weekday) + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'YEARLY', + interval: 1, + count: null, + until: null, + byDay: [], + byMonth: ['3'], + byMonthDay: [], + bySetPosition: null, + isUnsupported: true, + }) + }) + + it('should properly load recurrence-rules with count-limit', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrule-count') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + // plain MONTHLY + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'MONTHLY', + interval: 1, + count: 42, + until: null, + byDay: [], + byMonth: [], + byMonthDay: ['15'], + bySetPosition: null, + isUnsupported: false, + }) + }) + + it('should properly load recurrence-rules with until-limit', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrule-until') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + const mockDate = new Date() + getDateFromDateTimeValue + .mockReturnValueOnce(mockDate) + + // plain MONTHLY + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'MONTHLY', + interval: 1, + count: null, + until: mockDate, + byDay: [], + byMonth: [], + byMonthDay: ['15'], + bySetPosition: null, + isUnsupported: false, + }) + + expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(1) + expect(getDateFromDateTimeValue).toHaveBeenNthCalledWith(1, recurrenceRuleValue.until) + }) + + it('should properly load recurrence-rules with both count and until-limit (mark as unsupported)', () => { + const recurrenceRuleValue = getRecurValueFromAsset('rrules/rrule-count-and-until') + const baseDate = DateTimeValue.fromData({ + year: 2020, + month: 3, + day: 15, + isDate: true, + }) + + const mockDate = new Date() + getDateFromDateTimeValue + .mockReturnValueOnce(mockDate) + + + // plain MONTHLY + expect(mapRecurrenceRuleValueToRecurrenceRuleObject(recurrenceRuleValue, baseDate)).toEqual({ + recurrenceRuleValue, + frequency: 'MONTHLY', + interval: 1, + count: 5, + until: mockDate, + byDay: [], + byMonth: [], + byMonthDay: ['15'], + bySetPosition: null, + isUnsupported: true, + }) + + expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(1) + expect(getDateFromDateTimeValue).toHaveBeenNthCalledWith(1, recurrenceRuleValue.until) + }) +}) diff --git a/tests/javascript/unit/models/rfcProps.test.js b/tests/javascript/unit/models/rfcProps.test.js index 4b5ee9833..b9a26ed58 100644 --- a/tests/javascript/unit/models/rfcProps.test.js +++ b/tests/javascript/unit/models/rfcProps.test.js @@ -25,7 +25,7 @@ import { getDefaultCategories } from '../../../../src/defaults/defaultCategories jest.mock('@nextcloud/l10n') jest.mock('../../../../src/defaults/defaultCategories.js') -describe('models/rfcProps test suite', () => { +describe('Test suite: RFC properties (models/rfcProps.js)', () => { beforeEach(() => { translate.mockClear() diff --git a/tests/javascript/unit/models/schedulingObject.test.js b/tests/javascript/unit/models/schedulingObject.test.js new file mode 100644 index 000000000..60cd451fb --- /dev/null +++ b/tests/javascript/unit/models/schedulingObject.test.js @@ -0,0 +1,681 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { + getDefaultSchedulingObject, + mapCalendarJsToSchedulingObject, + mapCDavObjectToSchedulingObject +} from "../../../../src/models/schedulingObject.js"; +import CalendarComponent from "calendar-js/src/components/calendarComponent.js"; +import {getParserManager} from "calendar-js"; + +describe('Test suite: Scheduling Object model (models/schedulingObject.js)', () => { + + it('should return a default scheduling object object', () => { + expect(getDefaultSchedulingObject()).toEqual({ + id: null, + dav: null, + calendarComponent: null, + uid: null, + recurrenceId: null, + uri: null, + method: null, + isPublish: false, + isRequest: false, + isReply: false, + isAdd: false, + isCancel: false, + isRefresh: false, + isCounter: false, + isDeclineCounter: false, + existsOnServer: false, + }) + }) + + it('should fill up an object with default values', () => { + expect(getDefaultSchedulingObject({ + uid: '123', + otherProp: 'foo', + })).toEqual({ + id: null, + dav: null, + calendarComponent: null, + uid: '123', + recurrenceId: null, + uri: null, + method: null, + isPublish: false, + isRequest: false, + isReply: false, + isAdd: false, + isCancel: false, + isRefresh: false, + isCounter: false, + isDeclineCounter: false, + existsOnServer: false, + otherProp: 'foo', + }) + }) + + it('should map a calendar-js calendar-object to scheduling object - throw error for empty string', () => { + const dav = { + url: 'cdav-url', + data: '', + } + + expect(() => mapCDavObjectToSchedulingObject(dav)) + .toThrowError(/^Empty scheduling object$/); + }) + + it('should map a calendar-js calendar-object to scheduling object - empty', () => { + const dav = { + url: 'cdav-url', + data: loadICS('vcalendars/vcalendar-empty'), + } + + expect(() => mapCDavObjectToSchedulingObject(dav)) + .toThrowError(/^Empty scheduling object$/); + }) + + it('should map a calendar-js calendar-object to scheduling object - no method', () => { + const dav = { + url: 'cdav-url', + data: loadICS('vcalendars/vcalendar-event-timed'), + } + + expect(() => mapCDavObjectToSchedulingObject(dav)) + .toThrowError(/^Scheduling-object does not have method$/); + }) + + it('should map a calendar-js calendar-object to scheduling object - no vobjects nor freebusy', () => { + const dav = { + url: 'cdav-url', + data: loadICS('vcalendars/vcalendar-without-vobjects'), + } + + expect(() => mapCDavObjectToSchedulingObject(dav)) + .toThrowError(/^Empty scheduling object$/); + }) + + it('should map a calendar-js calendar-object to scheduling object - add', () => { + const dav = { + url: 'cdav-url', + data: loadICS('vcalendars-scheduling/add'), + } + + expect(mapCDavObjectToSchedulingObject(dav)).toEqual({ + id: 'Y2Rhdi11cmw=', + dav, + calendarComponent: expect.any(CalendarComponent), + uid: '123456789@example.com', + recurrenceId: null, + uri: 'cdav-url', + method: 'ADD', + isPublish: false, + isRequest: false, + isReply: false, + isAdd: true, + isCancel: false, + isRefresh: false, + isCounter: false, + isDeclineCounter: false, + existsOnServer: true, + }) + }) + + it('should map a calendar-js calendar-object to scheduling object - cancel', () => { + const dav = { + url: 'cdav-url', + data: loadICS('vcalendars-scheduling/cancel'), + } + + expect(mapCDavObjectToSchedulingObject(dav)).toEqual({ + id: 'Y2Rhdi11cmw=', + dav, + calendarComponent: expect.any(CalendarComponent), + uid: 'guid-1@example.com', + recurrenceId: null, + uri: 'cdav-url', + method: 'CANCEL', + isPublish: false, + isRequest: false, + isReply: false, + isAdd: false, + isCancel: true, + isRefresh: false, + isCounter: false, + isDeclineCounter: false, + existsOnServer: true, + }) + }) + + it('should map a calendar-js calendar-object to scheduling object - counter', () => { + const dav = { + url: 'cdav-url', + data: loadICS('vcalendars-scheduling/counter'), + } + + expect(mapCDavObjectToSchedulingObject(dav)).toEqual({ + id: 'Y2Rhdi11cmw=', + dav, + calendarComponent: expect.any(CalendarComponent), + uid: 'guid-1@example.com', + recurrenceId: expect.any(Date), + uri: 'cdav-url', + method: 'COUNTER', + isPublish: false, + isRequest: false, + isReply: false, + isAdd: false, + isCancel: false, + isRefresh: false, + isCounter: true, + isDeclineCounter: false, + existsOnServer: true, + }) + }) + + it('should map a calendar-js calendar-object to scheduling object - declinecounter', () => { + const dav = { + url: 'cdav-url', + data: loadICS('vcalendars-scheduling/declinecounter'), + } + + expect(mapCDavObjectToSchedulingObject(dav)).toEqual({ + id: 'Y2Rhdi11cmw=', + dav, + calendarComponent: expect.any(CalendarComponent), + uid: 'calsrv.example.com-873970198738777@example.com', + recurrenceId: null, + uri: 'cdav-url', + method: 'DECLINECOUNTER', + isPublish: false, + isRequest: false, + isReply: false, + isAdd: false, + isCancel: false, + isRefresh: false, + isCounter: false, + isDeclineCounter: true, + existsOnServer: true, + }) + }) + + it('should map a calendar-js calendar-object to scheduling object - freebusy-reply', () => { + const dav = { + url: 'cdav-url', + data: loadICS('vcalendars-scheduling/freebusy-reply'), + } + + expect(mapCDavObjectToSchedulingObject(dav)).toEqual({ + id: 'Y2Rhdi11cmw=', + dav, + calendarComponent: expect.any(CalendarComponent), + uid: 'calsrv.example.com-873970198738777@example.com', + recurrenceId: null, + uri: 'cdav-url', + method: 'REPLY', + isPublish: false, + isRequest: false, + isReply: true, + isAdd: false, + isCancel: false, + isRefresh: false, + isCounter: false, + isDeclineCounter: false, + existsOnServer: true, + }) + }) + + it('should map a calendar-js calendar-object to scheduling object - freebusy-request', () => { + const dav = { + url: 'cdav-url', + data: loadICS('vcalendars-scheduling/freebusy-request'), + } + + expect(mapCDavObjectToSchedulingObject(dav)).toEqual({ + id: 'Y2Rhdi11cmw=', + dav, + calendarComponent: expect.any(CalendarComponent), + uid: 'calsrv.example.com-873970198738777@example.com', + recurrenceId: null, + uri: 'cdav-url', + method: 'REQUEST', + isPublish: false, + isRequest: true, + isReply: false, + isAdd: false, + isCancel: false, + isRefresh: false, + isCounter: false, + isDeclineCounter: false, + existsOnServer: true, + }) + }) + + it('should map a calendar-js calendar-object to scheduling object - publish', () => { + const dav = { + url: 'cdav-url', + data: loadICS('vcalendars-scheduling/publish'), + } + + expect(mapCDavObjectToSchedulingObject(dav)).toEqual({ + id: 'Y2Rhdi11cmw=', + dav, + calendarComponent: expect.any(CalendarComponent), + uid: null, + recurrenceId: null, + uri: 'cdav-url', + method: 'PUBLISH', + isPublish: true, + isRequest: false, + isReply: false, + isAdd: false, + isCancel: false, + isRefresh: false, + isCounter: false, + isDeclineCounter: false, + existsOnServer: true, + }) + }) + + it('should map a calendar-js calendar-object to scheduling object - refresh', () => { + const dav = { + url: 'cdav-url', + data: loadICS('vcalendars-scheduling/refresh'), + } + + expect(mapCDavObjectToSchedulingObject(dav)).toEqual({ + id: 'Y2Rhdi11cmw=', + dav, + calendarComponent: expect.any(CalendarComponent), + uid: 'guid-1-12345@example.com', + recurrenceId: null, + uri: 'cdav-url', + method: 'REFRESH', + isPublish: false, + isRequest: false, + isReply: false, + isAdd: false, + isCancel: false, + isRefresh: true, + isCounter: false, + isDeclineCounter: false, + existsOnServer: true, + }) + }) + + it('should map a calendar-js calendar-object to scheduling object - reply', () => { + const dav = { + url: 'cdav-url', + data: loadICS('vcalendars-scheduling/reply'), + } + + expect(mapCDavObjectToSchedulingObject(dav)).toEqual({ + id: 'Y2Rhdi11cmw=', + dav, + calendarComponent: expect.any(CalendarComponent), + uid: 'calsrv.example.com-873970198738777@example.com', + recurrenceId: null, + uri: 'cdav-url', + method: 'REPLY', + isPublish: false, + isRequest: false, + isReply: true, + isAdd: false, + isCancel: false, + isRefresh: false, + isCounter: false, + isDeclineCounter: false, + existsOnServer: true, + }) + }) + + it('should map a calendar-js calendar-object to scheduling object - request', () => { + const dav = { + url: 'cdav-url', + data: loadICS('vcalendars-scheduling/request'), + } + + expect(mapCDavObjectToSchedulingObject(dav)).toEqual({ + id: 'Y2Rhdi11cmw=', + dav, + calendarComponent: expect.any(CalendarComponent), + uid: '123456789@example.com', + recurrenceId: expect.any(Date), + uri: 'cdav-url', + method: 'REQUEST', + isPublish: false, + isRequest: true, + isReply: false, + isAdd: false, + isCancel: false, + isRefresh: false, + isCounter: false, + isDeclineCounter: false, + existsOnServer: true, + }) + }) + + it('should map a calendar-js calendar-object to scheduling-object - no method', () => { + const ics = loadICS('vcalendars/vcalendar-event-timed') + const parser = getParserManager().getParserForFileType('text/calendar', { + preserveMethod: true, + processFreeBusy: true, + }) + parser.parse(ics) + + const calendarComponent = parser.getAllItems()[0] + expect(() => mapCalendarJsToSchedulingObject(calendarComponent)) + .toThrowError(/^Scheduling-object does not have method$/); + }) + + it('should map a calendar-js calendar-object to scheduling-object - add', () => { + const ics = loadICS('vcalendars-scheduling/add') + const parser = getParserManager().getParserForFileType('text/calendar', { + preserveMethod: true, + processFreeBusy: true, + }) + parser.parse(ics) + + const calendarComponent = parser.getAllItems()[0] + expect(mapCalendarJsToSchedulingObject(calendarComponent)).toEqual({ + id: null, + dav: null, + calendarComponent, + uri: null, + existsOnServer: false, + method: 'ADD', + isPublish: false, + isRequest: false, + isReply: false, + isAdd: true, + isCancel: false, + isRefresh: false, + isCounter: false, + isDeclineCounter: false, + uid: '123456789@example.com', + recurrenceId: null, + }) + }) + + it('should map a calendar-js calendar-object to scheduling-object - cancel', () => { + const ics = loadICS('vcalendars-scheduling/cancel') + const parser = getParserManager().getParserForFileType('text/calendar', { + preserveMethod: true, + processFreeBusy: true, + }) + parser.parse(ics) + + const calendarComponent = parser.getAllItems()[0] + expect(mapCalendarJsToSchedulingObject(calendarComponent)).toEqual({ + id: null, + dav: null, + calendarComponent, + uri: null, + existsOnServer: false, + method: 'CANCEL', + isPublish: false, + isRequest: false, + isReply: false, + isAdd: false, + isCancel: true, + isRefresh: false, + isCounter: false, + isDeclineCounter: false, + uid: 'guid-1@example.com', + recurrenceId: null, + }) + }) + + it('should map a calendar-js calendar-object to scheduling-object - counter', () => { + const ics = loadICS('vcalendars-scheduling/counter') + const parser = getParserManager().getParserForFileType('text/calendar', { + preserveMethod: true, + processFreeBusy: true, + }) + parser.parse(ics) + + const calendarComponent = parser.getAllItems()[0] + expect(mapCalendarJsToSchedulingObject(calendarComponent)).toEqual({ + id: null, + dav: null, + calendarComponent, + uri: null, + existsOnServer: false, + method: 'COUNTER', + isPublish: false, + isRequest: false, + isReply: false, + isAdd: false, + isCancel: false, + isRefresh: false, + isCounter: true, + isDeclineCounter: false, + uid: 'guid-1@example.com', + recurrenceId: expect.any(Date), + }) + }) + + it('should map a calendar-js calendar-object to scheduling-object - declinecounter', () => { + const ics = loadICS('vcalendars-scheduling/declinecounter') + const parser = getParserManager().getParserForFileType('text/calendar', { + preserveMethod: true, + processFreeBusy: true, + }) + parser.parse(ics) + + const calendarComponent = parser.getAllItems()[0] + expect(mapCalendarJsToSchedulingObject(calendarComponent)).toEqual({ + id: null, + dav: null, + calendarComponent, + uri: null, + existsOnServer: false, + method: 'DECLINECOUNTER', + isPublish: false, + isRequest: false, + isReply: false, + isAdd: false, + isCancel: false, + isRefresh: false, + isCounter: false, + isDeclineCounter: true, + uid: 'calsrv.example.com-873970198738777@example.com', + recurrenceId: null, + }) + }) + + it('should map a calendar-js calendar-object to scheduling-object - freebusy-reply', () => { + const ics = loadICS('vcalendars-scheduling/freebusy-reply') + const parser = getParserManager().getParserForFileType('text/calendar', { + preserveMethod: true, + processFreeBusy: true, + }) + parser.parse(ics) + + const calendarComponent = parser.getAllItems()[0] + expect(mapCalendarJsToSchedulingObject(calendarComponent)).toEqual({ + id: null, + dav: null, + calendarComponent, + uri: null, + existsOnServer: false, + method: 'REPLY', + isPublish: false, + isRequest: false, + isReply: true, + isAdd: false, + isCancel: false, + isRefresh: false, + isCounter: false, + isDeclineCounter: false, + uid: 'calsrv.example.com-873970198738777@example.com', + recurrenceId: null, + }) + }) + + it('should map a calendar-js calendar-object to scheduling-object - freebusy-request', () => { + const ics = loadICS('vcalendars-scheduling/freebusy-request') + const parser = getParserManager().getParserForFileType('text/calendar', { + preserveMethod: true, + processFreeBusy: true, + }) + parser.parse(ics) + + const calendarComponent = parser.getAllItems()[0] + expect(mapCalendarJsToSchedulingObject(calendarComponent)).toEqual({ + id: null, + dav: null, + calendarComponent, + uri: null, + existsOnServer: false, + method: 'REQUEST', + isPublish: false, + isRequest: true, + isReply: false, + isAdd: false, + isCancel: false, + isRefresh: false, + isCounter: false, + isDeclineCounter: false, + uid: 'calsrv.example.com-873970198738777@example.com', + recurrenceId: null, + }) + }) + + it('should map a calendar-js calendar-object to scheduling-object - publish', () => { + const ics = loadICS('vcalendars-scheduling/publish') + const parser = getParserManager().getParserForFileType('text/calendar', { + preserveMethod: true, + processFreeBusy: true, + }) + parser.parse(ics) + + const calendarComponent = parser.getAllItems()[0] + expect(mapCalendarJsToSchedulingObject(calendarComponent)).toEqual({ + id: null, + dav: null, + calendarComponent, + uri: null, + existsOnServer: false, + method: 'PUBLISH', + isPublish: true, + isRequest: false, + isReply: false, + isAdd: false, + isCancel: false, + isRefresh: false, + isCounter: false, + isDeclineCounter: false, + uid: null, + recurrenceId: null, + }) + }) + + it('should map a calendar-js calendar-object to scheduling-object - refresh', () => { + const ics = loadICS('vcalendars-scheduling/refresh') + const parser = getParserManager().getParserForFileType('text/calendar', { + preserveMethod: true, + processFreeBusy: true, + }) + parser.parse(ics) + + const calendarComponent = parser.getAllItems()[0] + expect(mapCalendarJsToSchedulingObject(calendarComponent)).toEqual({ + id: null, + dav: null, + calendarComponent, + uri: null, + existsOnServer: false, + method: 'REFRESH', + isPublish: false, + isRequest: false, + isReply: false, + isAdd: false, + isCancel: false, + isRefresh: true, + isCounter: false, + isDeclineCounter: false, + uid: 'guid-1-12345@example.com', + recurrenceId: null, + }) + }) + + it('should map a calendar-js calendar-object to scheduling-object - reply', () => { + const ics = loadICS('vcalendars-scheduling/reply') + const parser = getParserManager().getParserForFileType('text/calendar', { + preserveMethod: true, + processFreeBusy: true, + }) + parser.parse(ics) + + const calendarComponent = parser.getAllItems()[0] + expect(mapCalendarJsToSchedulingObject(calendarComponent)).toEqual({ + id: null, + dav: null, + calendarComponent, + uri: null, + existsOnServer: false, + method: 'REPLY', + isPublish: false, + isRequest: false, + isReply: true, + isAdd: false, + isCancel: false, + isRefresh: false, + isCounter: false, + isDeclineCounter: false, + uid: 'calsrv.example.com-873970198738777@example.com', + recurrenceId: null, + }) + }) + + it('should map a calendar-js calendar-object to scheduling-object - request', () => { + const ics = loadICS('vcalendars-scheduling/request') + const parser = getParserManager().getParserForFileType('text/calendar', { + preserveMethod: true, + processFreeBusy: true, + }) + parser.parse(ics) + + const calendarComponent = parser.getAllItems()[0] + expect(mapCalendarJsToSchedulingObject(calendarComponent)).toEqual({ + id: null, + dav: null, + calendarComponent, + uri: null, + existsOnServer: false, + method: 'REQUEST', + isPublish: false, + isRequest: true, + isReply: false, + isAdd: false, + isCancel: false, + isRefresh: false, + isCounter: false, + isDeclineCounter: false, + uid: '123456789@example.com', + recurrenceId: expect.any(Date), + }) + }) +}) diff --git a/tests/javascript/unit/utils/date.test.js b/tests/javascript/unit/utils/date.test.js index 61370f4ac..1fc535abc 100644 --- a/tests/javascript/unit/utils/date.test.js +++ b/tests/javascript/unit/utils/date.test.js @@ -58,8 +58,6 @@ describe('utils/alarms test suite', () => { const date1 = getDateFromFirstdayParam('2019-01-01') const date2 = getDateFromFirstdayParam('2019-12-31') - const expectedTimezoneOffset = new Date().getTimezoneOffset() - expect(date1.getFullYear()).toEqual(2019) expect(date1.getMonth()).toEqual(0) expect(date1.getDate()).toEqual(1) @@ -67,7 +65,7 @@ describe('utils/alarms test suite', () => { expect(date1.getMinutes()).toEqual(0) expect(date1.getSeconds()).toEqual(0) expect(date1.getMilliseconds()).toEqual(0) - expect(date1.getTimezoneOffset()).toEqual(expectedTimezoneOffset) + expect(date1.getTimezoneOffset()).toEqual(new Date(2019, 0, 1).getTimezoneOffset()) expect(date2.getFullYear()).toEqual(2019) expect(date2.getMonth()).toEqual(11) @@ -76,7 +74,7 @@ describe('utils/alarms test suite', () => { expect(date2.getMinutes()).toEqual(0) expect(date2.getSeconds()).toEqual(0) expect(date2.getMilliseconds()).toEqual(0) - expect(date2.getTimezoneOffset()).toEqual(expectedTimezoneOffset) + expect(date2.getTimezoneOffset()).toEqual(new Date(2019, 11, 31).getTimezoneOffset()) }) it('should log an error when providing a non-numerical first-day-parameter', () => {