import config from '../lib/config' import WebDAV from '../lib/WebDAV' import TestUtility from '../lib/utilities' import ICAL from 'ical.js' describe('create, read, modify, delete tasks for regular user', function() { const webdav = new WebDAV(config.username, config.password) const webdav_su = new WebDAV(config.superuser, config.superuser_password) const webdavAttendee1 = new WebDAV(config.attendee1, config.attendee1_password) const webdavAttendee1Delegate = new WebDAV(config.attendee1_delegate_username, config.attendee1_delegate_password) const utility = new TestUtility(webdav) const userCalendar = `/SOGo/dav/${config.username}/Calendar/personal/` const attendee1Calendar = `/SOGo/dav/${config.attendee1}/Calendar/personal/` const attendee1DelegateCalendar = `/SOGo/dav/${config.attendee1_delegate}/Calendar/personal/` const resourceNoOverbookCalendar = `/SOGo/dav/${config.resource_no_overbook}/Calendar/personal/` const resourceCanOverbookCalendar = `/SOGo/dav/${config.resource_can_overbook}/Calendar/personal/` let user let attendee1 let attendee1Delegate let resourceNoOverbook let resourceCanOverbook let icsList = [] const _getEvent = async function(client, calendarName, filename, expectedCode = 200) { const [{ status, raw }] = await client.getObject(calendarName, filename) expect(status).toBe(expectedCode) if (status <= 300) return new ICAL.Component(ICAL.parse(raw)) } const _getAllEvents = async function(client, calendarName, expectedCode = 207) { const results = await client.propfindCollection(calendarName) const hrefs = results.filter(r => r.href).map(r => r.href) return hrefs } const _putEvent = async function(client, calendarName, filename, event, expectedCode = 201) { const response = await client.createCalendarObject(calendarName, filename, event.toString()) expect(response.status) .withContext(`Event creation returns code ${expectedCode}`) .toBe(expectedCode) return response } const _postEvent = async function(client, outbox, vcalendar, originator, recipients, expectedCode = 200) { const response = await client.postCaldav(outbox, vcalendar, originator, recipients) expect(response.status) .withContext(`Event post returns code ${expectedCode}`) .toBe(expectedCode) return response } const _deleteEvent = async function(client, url, expectedCode) { const response = await client.deleteObject(url) if (expectedCode) expect(response.status).toBe(expectedCode) return response } const _deleteAllEvents = async function(client, calendarName, expectedCode = 204) { const hrefs = await _getAllEvents(client, calendarName) for (const href of hrefs) { await _deleteEvent(client, href) // ignore returned code } return hrefs } const _compareAttendees = function(vcalendar1, vcalendar2) { const vevent1 = vcalendar1.getFirstSubcomponent('vevent') const vevent2 = vcalendar2.getFirstSubcomponent('vevent') const attendeeToString = function(a) { const email = a.getFirstValue() const partstat = a.getParameter('partstat') const delegatedto = a.getParameter('delegated-to') || '(none)' const delegatedfrom = a.getParameter('delegated-from') || '(none)' return `${email}/${partstat}/${delegatedto}/${delegatedfrom}` } const attendees1 = vevent1.getAllProperties('attendee').map(attendeeToString) const attendees2 = vevent2.getAllProperties('attendee').map(attendeeToString) expect(attendees1.length) .withContext(`'vcalendar1' has attendees`) .toBeGreaterThan(0) expect(attendees2.length) .withContext(`'vcalendar2' has attendees`) .toBeGreaterThan(0) expect(attendees1.length) .withContext(`'vcalendar1' and 'vcalendar2' have the same number of attendees`) .toBe(attendees2.length) for (let attendee of attendees1) { expect(attendees2.indexOf(attendee)) .withContext(`${attendee} from 'vcalendar1' is found in 'vcalendar2`) .toBeGreaterThanOrEqual(0) } } beforeAll(async function() { user = await utility.fetchUserInfo(config.username) attendee1 = await utility.fetchUserInfo(config.attendee1) attendee1Delegate = await utility.fetchUserInfo(config.attendee1_delegate) resourceNoOverbook = await utility.fetchUserInfo(config.resource_no_overbook) resourceCanOverbook = await utility.fetchUserInfo(config.resource_can_overbook) // fetch non existing event to let sogo create the calendars in the db await _getEvent(webdav, userCalendar, 'nonexistent', 404) await _getEvent(webdavAttendee1, attendee1Calendar, 'nonexistent', 404) await _getEvent(webdavAttendee1Delegate, attendee1DelegateCalendar, 'nonexistent', 404) }) afterEach(async function() { for (const ics of icsList) { await _deleteEvent(webdav_su, userCalendar + ics) await _deleteEvent(webdav_su, attendee1Calendar + ics) await _deleteEvent(webdav_su, attendee1DelegateCalendar + ics) await _deleteEvent(webdav_su, resourceCanOverbookCalendar + ics) await _deleteEvent(webdav_su, resourceNoOverbookCalendar + ics) } }) // CalDAVSchedulingTest it('add attendee after event creation', async function() { const icsName = 'test-add-attendee.ics' icsList.push(icsName) let vcalendar, vcalendarAttendee let vevent, veventAttendee, organizer, attendee // make sure the event doesn't exist await _deleteEvent(webdav, userCalendar + icsName) await _deleteEvent(webdavAttendee1, attendee1Calendar + icsName) // 1. create an event in the organizer's calendar vcalendar = utility.createCalendar('Test add attendee', 'Test add attendee') vevent = vcalendar.getFirstSubcomponent('vevent') organizer = new ICAL.Property('organizer') organizer.setParameter('cn', user.displayname) organizer.setValue(user.email) vevent.addProperty(organizer) await _putEvent(webdav, userCalendar, icsName, vcalendar) // 2. add an attendee vcalendar.addPropertyWithValue('method', 'REQUEST') attendee = new ICAL.Property('attendee') attendee.setParameter('cn', attendee1.displayname) attendee.setParameter('rsvp', 'TRUE') attendee.setParameter('partstat', 'NEEDS-ACTION') attendee.setValue(attendee1.email) vevent.addProperty(attendee) await _putEvent(webdav, userCalendar, icsName, vcalendar, 204) // 3. verify that the attendee has the event vcalendarAttendee = await _getEvent(webdavAttendee1, attendee1Calendar, icsName) // 4. make sure the received event match the original one veventAttendee = vcalendarAttendee.getFirstSubcomponent('vevent') expect(veventAttendee.getFirstProperty('uid').getFirstValue()) .withContext(`UID in organizer's calendar and attendees's calendar are identical`) .toBe(vevent.getFirstProperty('uid').getFirstValue()) }) it('Remove attendee after event creation', async function() { const icsName = 'test-remove-attendee.ics' icsList.push(icsName) let vcalendar, vcalendarNoAttendee, vcalendarAttendee let vevent, veventAttendee, organizer, attendee // make sure the event doesn't exist await _deleteEvent(webdav, userCalendar + icsName) await _deleteEvent(webdavAttendee1, attendee1Calendar + icsName) // 1. create an event in the organizer's calendar vcalendar = utility.createCalendar('Test uninvite attendee', 'Test uninvite attendee') vevent = vcalendar.getFirstSubcomponent('vevent') organizer = new ICAL.Property('organizer') organizer.setParameter('cn', user.displayname) organizer.setValue(user.email) vevent.addProperty(organizer) await _putEvent(webdav, userCalendar, icsName, vcalendar) // keep a copy around for updates without other attributes vcalendarNoAttendee = ICAL.Component.fromString(vcalendar.toString()) // 2. add an attendee vcalendar.addPropertyWithValue('method', 'REQUEST') attendee = new ICAL.Property('attendee') attendee.setParameter('cn', attendee1.displayname) attendee.setParameter('rsvp', 'TRUE') attendee.setParameter('partstat', 'NEEDS-ACTION') attendee.setValue(attendee1.email) vevent.addProperty(attendee) await _putEvent(webdav, userCalendar, icsName, vcalendar, 204) // 3. verify that the attendee has the event vcalendarAttendee = await _getEvent(webdavAttendee1, attendee1Calendar, icsName) // 4. make sure the received event match the original one veventAttendee = vcalendarAttendee.getFirstSubcomponent('vevent') expect(veventAttendee.getFirstProperty('uid').getFirstValue()) .toBe(vevent.getFirstProperty('uid').getFirstValue()) // 5. uninvite the attendee - put the event back without the attendee vevent = vcalendarNoAttendee.getFirstSubcomponent('vevent') vevent.addProperty(utility.createDateTimeProperty('last-modified')) await _putEvent(webdav, userCalendar, icsName, vcalendarNoAttendee, 204) // 6. verify that the attendee doesn't have the event anymore await _getEvent(webdavAttendee1, attendee1Calendar, icsName, 404) }) it('try to overbook a resource', async function() { const icsName = 'test-no-overbook.ics' const obIcsName = 'test-no-overbook-overlap.ics' icsList.push(icsName, obIcsName) let vcalendar, vevent, organizer, attendee // make sure there are no events in the resource calendar await _deleteAllEvents(webdav_su, resourceNoOverbookCalendar) // make sure the events don't exist await _deleteEvent(webdav, userCalendar + icsName) await _deleteEvent(webdav, userCalendar + obIcsName) // 1. create an event in the organizer's calendar vcalendar = utility.createCalendar('Test no overbook', 'Test no overbook') vevent = vcalendar.getFirstSubcomponent('vevent') organizer = new ICAL.Property('organizer') organizer.setParameter('cn', user.displayname) organizer.setValue(user.email) vevent.addProperty(organizer) attendee = new ICAL.Property('attendee') attendee.setParameter('cn', resourceNoOverbook.displayname) attendee.setParameter('rsvp', 'TRUE') attendee.setParameter('partstat', 'NEEDS-ACTION') attendee.setValue(resourceNoOverbook.email) vevent.addProperty(attendee) await _putEvent(webdav, userCalendar, icsName, vcalendar) // 2. create a second event overlapping the first one vcalendar = utility.createCalendar('Test no overbook - overlap', 'Test no overbook - overlap') vevent = vcalendar.getFirstSubcomponent('vevent') organizer = new ICAL.Property('organizer') organizer.setParameter('cn', user.displayname) organizer.setValue(user.email) vevent.addProperty(organizer) attendee = new ICAL.Property('attendee') attendee.setParameter('cn', resourceNoOverbook.displayname) attendee.setParameter('rsvp', 'TRUE') attendee.setParameter('partstat', 'NEEDS-ACTION') attendee.setValue(resourceNoOverbook.email) vevent.addProperty(attendee) // put the event - should trigger a 409 await _putEvent(webdav, userCalendar, obIcsName, vcalendar, 409) }) it('try to overbook a resource - multiplebookings=0', async function() { const icsName = 'test-can-overbook.ics' const obIcsName = 'test-can-overbook-overlap.ics' icsList.push(icsName, obIcsName) let vcalendar, vevent, organizer, attendee // make sure there are no events in the resource calendar await _deleteAllEvents(webdav_su, resourceCanOverbookCalendar) // make sure the events don't exist await _deleteEvent(webdav, userCalendar + icsName) await _deleteEvent(webdav, userCalendar + obIcsName) // 1. create an event in the organizer's calendar vcalendar = utility.createCalendar('Test can overbook', 'Test can overbook') vevent = vcalendar.getFirstSubcomponent('vevent') organizer = new ICAL.Property('organizer') organizer.setParameter('cn', user.displayname) organizer.setValue(user.email) vevent.addProperty(organizer) attendee = new ICAL.Property('attendee') attendee.setParameter('cn', resourceCanOverbook.displayname) attendee.setParameter('rsvp', 'TRUE') attendee.setParameter('partstat', 'NEEDS-ACTION') attendee.setValue(resourceCanOverbook.email) vevent.addProperty(attendee) await _putEvent(webdav, userCalendar, icsName, vcalendar) // 2. create a second event overlapping the first one vcalendar = utility.createCalendar('Test can overbook - overlap', 'Test can overbook - overlap') vevent = vcalendar.getFirstSubcomponent('vevent') organizer = new ICAL.Property('organizer') organizer.setParameter('cn', user.displayname) organizer.setValue(user.email) vevent.addProperty(organizer) attendee = new ICAL.Property('attendee') attendee.setParameter('cn', resourceCanOverbook.displayname) attendee.setParameter('rsvp', 'TRUE') attendee.setParameter('partstat', 'NEEDS-ACTION') attendee.setValue(resourceCanOverbook.email) vevent.addProperty(attendee) // put the event - should be fine since we can overbook this one await _putEvent(webdav, userCalendar, obIcsName, vcalendar) }) it('Resource booking overlap detection - bug #1837', async function() { // There used to be some problems with recurring events and resources booking // This test implements these edge cases // 1. Create recurring event (with resource) // 2. Create single event overlaping one instance for the previous event // (should fail) // 3. Create recurring event which _doesn't_ overlap the first event // (should be OK, used to fail pre1.3.17) // 4. Create recurring event overlapping the previous recurring event // (should fail) const icsName = 'test-res-overlap-detection.ics' const overlapIcsName = 'test-res-overlap-detection-overlap.ics' icsList.push(icsName, overlapIcsName) let vcalendar, vcalendarNoOverlap let vevent, organizer, attendee, rrule, recur, nstartdate, nenddate // make sure there are no events in the resource calendar await _deleteAllEvents(webdav_su, resourceNoOverbookCalendar) // make sure the event doesn't exist await _deleteEvent(webdav, userCalendar + icsName) await _deleteEvent(webdav, attendee1Calendar + overlapIcsName) const noOverlapRecurringIcsName = 'test-res-overlap-detection-nooverlap.ics' icsList.push(noOverlapRecurringIcsName) await _deleteEvent(webdav, userCalendar + noOverlapRecurringIcsName) const overlapRecurringIcsName = 'test-res-overlap-detection-overlap-recurring.ics' icsList.push(overlapRecurringIcsName) await _deleteEvent(webdav, userCalendar + overlapRecurringIcsName) // 1. create recurring event with resource vcalendar = utility.createCalendar('Recurring event with resource', 'Recurring event with resource') vevent = vcalendar.getFirstSubcomponent('vevent') rrule = new ICAL.Property('rrule') recur = new ICAL.Recur({ freq: 'DAILY', count: 5 }) rrule.setValue(recur) vevent.addProperty(rrule) organizer = new ICAL.Property('organizer') organizer.setParameter('cn', user.displayname) organizer.setValue(user.email) vevent.addProperty(organizer) attendee = new ICAL.Property('attendee') attendee.setParameter('cn', resourceNoOverbook.displayname) attendee.setParameter('rsvp', 'TRUE') attendee.setParameter('partstat', 'NEEDS-ACTION') attendee.setValue(resourceNoOverbook.email) vevent.addProperty(attendee) // keep a copy around for #3 vcalendarNoOverlap = ICAL.Component.fromString(vcalendar.toString()) await _putEvent(webdav, userCalendar, icsName, vcalendar) // 2. Create single event overlaping one instance for the previous event vcalendar = utility.createCalendar('Recurring event with resource - overlap', 'Recurring event with resource - overlap') vevent = vcalendar.getFirstSubcomponent('vevent') organizer = new ICAL.Property('organizer') organizer.setParameter('cn', attendee1.displayname) organizer.setValue(attendee1.email) vevent.addProperty(organizer) attendee = new ICAL.Property('attendee') attendee.setParameter('cn', resourceNoOverbook.displayname) attendee.setParameter('rsvp', 'TRUE') attendee.setParameter('partstat', 'NEEDS-ACTION') attendee.setValue(resourceNoOverbook.email) vevent.addProperty(attendee) // should fail await _putEvent(webdavAttendee1, attendee1Calendar, overlapIcsName, vcalendar, 409) // 3. Create recurring event which _doesn't_ overlap the first event // (should be OK, used to fail pre1.3.17) // shift the start date to one hour after the original event end time vevent = vcalendarNoOverlap.getFirstSubcomponent('vevent') nstartdate = vevent.getFirstProperty('dtend').getFirstValue().toJSDate() nstartdate = new Date(nstartdate.getTime() + 1000*60*60) nenddate = new Date(nstartdate.getTime() + 1000*60*60) vevent.removeProperty('dtstart') vevent.removeProperty('dtend') vevent.addProperty(utility.createDateTimeProperty('dtstart', nstartdate)) vevent.addProperty(utility.createDateTimeProperty('dtend', nenddate)) vevent.updatePropertyWithValue('uid', 'recurring - nooverlap') await _putEvent(webdav, userCalendar, noOverlapRecurringIcsName, vcalendarNoOverlap) // 4. Create recurring event overlapping the previous recurring event // should fail with a 409 nstartdate = vevent.getFirstProperty('dtstart').getFirstValue().toJSDate() nstartdate = new Date(nstartdate.getTime() + 1000*60*5) nenddate = new Date(nstartdate.getTime() + 1000*60*60) vevent.removeProperty('dtstart') vevent.removeProperty('dtend') vevent.addProperty(utility.createDateTimeProperty('dtstart', nstartdate)) vevent.addProperty(utility.createDateTimeProperty('dtend', nenddate)) vevent.updatePropertyWithValue('uid', 'recurring - nooverlap') await _putEvent(webdav, userCalendar, overlapRecurringIcsName, vcalendarNoOverlap, 409) }) it('RRULE exception invitation dance', async function() { // This workflow is based on what lightning 1.2.1 does // create a reccurring event // add an exception // invite bob to the exception: // bob is declined in the master event // bob needs-action in the exception // bob accepts // bob is declined in the master event // bob is accepted in the exception // the organizer 'uninvites' bob // the event disappears from bob's calendar // bob isn't in the master+exception event const icsName = 'test-rrule-exception-invitation-dance.ics' icsList.push(icsName) let vcalendar, vcalendarOrganizer, vcalendarAttendee, vevents, vevent, veventMaster, veventException let recurrenceId, summary, uid, organizer, attendees, attendee, rrule, recur, originalStartDate await _deleteEvent(webdav, userCalendar + icsName) await _deleteEvent(webdav, attendee1Calendar + icsName) // 1. create a recurring event in the organizer's calendar summary = 'Test reccuring exception invite cancel' uid = 'Test-recurring-exception-invite-cancel' vcalendar = utility.createCalendar(summary, uid) vevent = vcalendar.getFirstSubcomponent('vevent') rrule = new ICAL.Property('rrule') recur = new ICAL.Recur({ freq: 'DAILY', count: 5 }) rrule.setValue(recur) vevent.addProperty(rrule) await _putEvent(webdav, userCalendar, icsName, vcalendar) // read the event back from the server vcalendarOrganizer = await _getEvent(webdav, userCalendar, icsName) // 2. Add an exception to the master event and invite attendee1 to it vevent = vcalendarOrganizer.getFirstSubcomponent('vevent') vevent.removeProperty('last-modified') vevent.addProperty(utility.createDateTimeProperty('last-modified')) originalStartDate = vevent.getFirstPropertyValue('dtstart') veventException = new ICAL.Component('vevent') veventException.addProperty(utility.createDateTimeProperty('created')) veventException.addProperty(utility.createDateTimeProperty('last-modified')) veventException.addProperty(utility.createDateTimeProperty('dtstamp')) veventException.addPropertyWithValue('uid', uid) veventException.addPropertyWithValue('summary', summary) veventException.addPropertyWithValue('transp', 'OPAQUE') veventException.addPropertyWithValue('description', 'Exception') veventException.addPropertyWithValue('sequence', '1') veventException.addProperty(vevent.getFirstProperty('dtstart')) veventException.addProperty(vevent.getFirstProperty('dtend')) // out of laziness, add the exception for the first occurence of the event recurrenceId = new ICAL.Property('recurrence-id') recurrenceId.setParameter('tzid', originalStartDate.timezone) recurrenceId.setValue(originalStartDate) veventException.addProperty(recurrenceId) // 2.1 Add attendee1 and organizer to the exception organizer = new ICAL.Property('organizer') organizer.setParameter('cn', user.displayname) organizer.setParameter('partstat', 'ACCEPTED') organizer.setValue(user.email) veventException.addProperty(organizer) attendee = new ICAL.Property('attendee') attendee.setParameter('cn', attendee1.displayname) attendee.setParameter('rsvp', 'TRUE') attendee.setParameter('role', 'REQ-PARTICIPANT') attendee.setParameter('partstat', 'NEEDS-ACTION') attendee.setValue(attendee1.email) veventException.addProperty(attendee) vcalendarOrganizer.addSubcomponent(veventException) await _putEvent(webdav, userCalendar, icsName, vcalendarOrganizer, 204) // 3. Make sure the attendee got the event vcalendarAttendee = await _getEvent(webdavAttendee1, attendee1Calendar, icsName) vevents = vcalendarAttendee.getAllSubcomponents('vevent') expect(vevents.length) .withContext('vEvents count in the calendar of the attendee') .toBe(1) vevent = vevents[0] expect(vevent.getFirstPropertyValue('recurrence-id')) .withContext('The vEvent of the attendee has a RECURRENCE-ID') .toBeTruthy() attendees = vevent.getAllProperties('attendee') expect(attendees.length) .withContext('Attendees count in the calendar of the attendee') .toBe(1) attendee = attendees[0] expect(attendee.getParameter('partstat')) .withContext('Partstat of attendee in calendar of the attendee') .toBe('NEEDS-ACTION') // 4. attendee accepts invitation attendee.setParameter('partstat', 'ACCEPTED') await _putEvent(webdavAttendee1, attendee1Calendar, icsName, vcalendarAttendee, 204) // fetch the organizer's event vcalendarOrganizer = await _getEvent(webdav, userCalendar, icsName) vevents = vcalendarOrganizer.getAllSubcomponents('vevent') expect(vevents.length) .withContext('vEvents count in the calendar of the organizer') .toBe(2) for (vevent of vevents) { if (vevent.getFirstPropertyValue('recurrence-id')) { veventException = vevent } else { veventMaster = vevent } } // make sure sogo doesn't duplicate attendees expect(veventMaster.getAllProperties('attendee').length).toBe(0) expect(veventException.getAllProperties('attendee').length).toBe(1) // 5. Make sure organizer got the accept for the exception and // that the attendee is still declined in the master attendee = veventException.getAllProperties('attendee')[0] expect(attendee.getParameter('partstat')) .withContext('Partstat of attendee in the calendar of the organizer') .toBe('ACCEPTED') // 6. delete the attendee from the organizer event (uninvite) // The event should be deleted from the attendee's calendar vcalendarOrganizer.removeSubcomponent(veventException) await _putEvent(webdav, userCalendar, icsName, vcalendarOrganizer, 204) await _getEvent(webdavAttendee1, attendee1Calendar, icsName, 404) }) it ('RRULE invitation delete exdate dance', async function() { // Workflow: // Create an recurring event and invite Bob // Add an exdate to the master event // Verify that the exdate has propagated to Bob's calendar // Add an exdate to bob's version of the event // Verify that an exception has been created in the org's calendar and that bob is 'declined' const icsName = 'test-rrule-invitation-deleted-exdate-dance.ics' icsList.push(icsName) let summary, uid, rrule, recur, organizer, attendees, attendee, nstartdate, exdate, offset let vcalendar, vcalendarOrganizer, vcalendarAttendee, vevent, vevents, veventMaster, veventException await _deleteEvent(webdav, userCalendar + icsName) await _deleteEvent(webdavAttendee1, attendee1Calendar + icsName) // 1. create a recurring event in the organizer's calendar summary = 'Test rrule invitation deleted exdate dance' uid = 'Test-rrule-invitation-deleted-exdate-dance' vcalendar = utility.createCalendar(summary, uid) vevent = vcalendar.getFirstSubcomponent('vevent') rrule = new ICAL.Property('rrule') recur = new ICAL.Recur({ freq: 'DAILY', count: 5 }) rrule.setValue(recur) vevent.addProperty(rrule) organizer = new ICAL.Property('organizer') organizer.setParameter('cn', user.displayname) organizer.setParameter('partstat', 'ACCEPTED') organizer.setValue(user.email) vevent.addProperty(organizer) attendee = new ICAL.Property('attendee') attendee.setParameter('cn', attendee1.displayname) attendee.setParameter('rsvp', 'TRUE') attendee.setParameter('role', 'REQ-PARTICIPANT') attendee.setParameter('partstat', 'NEEDS-ACTION') attendee.setValue(attendee1.email) vevent.addProperty(attendee) await _putEvent(webdav, userCalendar, icsName, vcalendar) // 2. Make sure the attendee got it await _getEvent(webdavAttendee1, attendee1Calendar, icsName) // 3. Add exdate to master event vcalendarOrganizer = await _getEvent(webdav, userCalendar, icsName) vevent = vcalendarOrganizer.getFirstSubcomponent('vevent') nstartdate = vevent.getFirstProperty('dtstart').getFirstValue().toJSDate() offset = nstartdate.getTimezoneOffset() exdate = new Date(nstartdate.getTime() - offset*60*1000) exdate = ICAL.Time.fromJSDate(exdate) exdate = exdate.convertToZone(ICAL.Timezone.utcTimezone) vevent.addPropertyWithValue('exdate', exdate) await _putEvent(webdav, userCalendar, icsName, vcalendarOrganizer, 204) // 4. make sure the attendee has the exdate vcalendarAttendee = await _getEvent(webdavAttendee1, attendee1Calendar, icsName) vevent = vcalendarAttendee.getFirstSubcomponent('vevent') expect(vevent.getFirstProperty('exdate').getFirstValue().toICALString()) .withContext(`Exdate is in attendee's calendar`) .toEqual(exdate.toICALString()) // 5. Create an exdate in the attendee's calendar exdate = new Date(nstartdate.getTime() + offset*60*1000 + 1000*60*60*24*2) exdate = ICAL.Time.fromJSDate(exdate) exdate = exdate.convertToZone(ICAL.Timezone.utcTimezone) vevent.addPropertyWithValue('exdate', exdate) vevent.removeProperty('last-modified') vevent.addProperty(utility.createDateTimeProperty('last-modified')) await _putEvent(webdavAttendee1, attendee1Calendar, icsName, vcalendarAttendee, 204) // 6. Make sure the attendee is: // needs-action in master event // declined in the new exception created by the exdate above vcalendarOrganizer = await _getEvent(webdav, userCalendar, icsName) vevents = vcalendarOrganizer.getAllSubcomponents('vevent') for (vevent of vevents) { if (vevent.getFirstPropertyValue('recurrence-id')) veventException = vevent else veventMaster = vevent } attendees = veventMaster.getAllProperties('attendee') expect(attendees.length) .withContext('Attendees count in the calendar of the master event') .toBe(1) attendee = attendees[0] expect(attendee.getParameter('partstat')) .withContext('Partstat of attendee is need-actions for the master event') .toBe('NEEDS-ACTION') expect(veventException).toBeTruthy() attendees = veventException.getAllProperties('attendee') expect(attendees.length) .withContext('Attendees count in the calendar of the exception event') .toBe(1) attendee = attendees[0] expect(attendee.getParameter('partstat')) .withContext('Partstat of attendee is declined for the exception') .toBe('DECLINED') }) it('iCal organizer is attendee - bug #1839', async function() { const icsName = 'test-organizer-is-attendee.ics' icsList.push(icsName) let summary, uid let vcalendar, vcalendarOrganizer, vevent, organizer, attendee, attendees await _deleteEvent(webdav, userCalendar + icsName) await _deleteEvent(webdavAttendee1, attendee1Calendar + icsName) // 1. create a recurring event in the organizer's calendar summary = 'Test organizer is attendee' uid = 'Test-organizer-is-attendee' vcalendar = utility.createCalendar(summary, uid) vevent = vcalendar.getFirstSubcomponent('vevent') organizer = new ICAL.Property('organizer') organizer.setParameter('cn', user.displayname) organizer.setParameter('partstat', 'ACCEPTED') organizer.setValue(user.email) vevent.addProperty(organizer) attendee = new ICAL.Property('attendee') attendee.setParameter('cn', attendee1.displayname) attendee.setParameter('rsvp', 'TRUE') attendee.setParameter('role', 'REQ-PARTICIPANT') attendee.setParameter('partstat', 'NEEDS-ACTION') attendee.setValue(attendee1.email) vevent.addProperty(attendee) // 1.1 add the organizer as an attendee attendee = new ICAL.Property('attendee') attendee.setParameter('cn', user.displayname) attendee.setParameter('rsvp', 'TRUE') attendee.setParameter('role', 'REQ-PARTICIPANT') attendee.setParameter('partstat', 'ACCEPTED') attendee.setValue(user.email) vevent.addProperty(attendee) // console.debug(`Test organizer is attendee =\n${vcalendar.toString()}`) await _putEvent(webdav, userCalendar, icsName, vcalendar) // 2. Fetch the event and make sure the organizer is not in the attendee list anymore vcalendarOrganizer = await _getEvent(webdav, userCalendar, icsName) vevent = vcalendarOrganizer.getFirstSubcomponent('vevent') attendees = vevent.getAllProperties('attendee') for (attendee of attendees) { expect(attendee.getFirstValue()) .withContext(`Organizer is not an attendee`) .not.toBe(user.email) } }) it('PUT 2 events with the same UID - bug #1853', async function () { const icsName = 'test-same-uid.ics' const conflictIcsName = 'test-same-uid-conflict.ics' icsList.push(icsName, conflictIcsName) await _deleteEvent(webdav, userCalendar + icsName) await _deleteEvent(webdav, userCalendar + conflictIcsName) let summary, uid let vcalendar // 1. create simple event summary = 'Test same uid' uid = 'Test-same-uid' vcalendar = utility.createCalendar(summary, uid) await _putEvent(webdav, userCalendar, icsName, vcalendar) // PUT the same event with a new filename - should trigger a 409 await _putEvent(webdav, userCalendar, conflictIcsName, vcalendar, 409) }) it('invitation delegation', async function () { const icsName = 'test-delegation.ics' icsList.push(icsName) let vcalendarInvitation, vcalendarInvitationAttendee, vcalendarInvitationDelegate, vcalendarInvitationOrganizer, vcalendarCancellation let vevent, organizer, attendee, attendees, delegate // the invitation must not exist await _deleteEvent(webdav, userCalendar + icsName) await _deleteEvent(webdavAttendee1, attendee1Calendar + icsName) await _deleteEvent(webdavAttendee1Delegate, attendee1DelegateCalendar + icsName) // 1. org -> attendee => org: 1, attendee: 1 (pst=N-A), delegate: 0 vcalendarInvitation = utility.createCalendar() vcalendarInvitation.addPropertyWithValue('method', 'REQUEST') vevent = vcalendarInvitation.getFirstSubcomponent('vevent') organizer = new ICAL.Property('organizer') organizer.setParameter('cn', user.displayname) organizer.setValue(user.email) vevent.addProperty(organizer) attendee = new ICAL.Property('attendee') attendee.setParameter('cn', attendee1.displayname) attendee.setParameter('rsvp', 'TRUE') attendee.setParameter('partstat', 'NEEDS-ACTION') attendee.setValue(attendee1.email) vevent.addProperty(attendee) await _postEvent(webdav, userCalendar, vcalendarInvitation, user.email, [attendee1.email]) vcalendarInvitation.removeProperty('method') await _putEvent(webdav, userCalendar, icsName, vcalendarInvitation) vcalendarInvitationAttendee = await _getEvent(webdavAttendee1, attendee1Calendar, icsName) _compareAttendees(vcalendarInvitationAttendee, vcalendarInvitation) // 2. attendee delegates to delegate // => org: 1 (updated), attendee: 1 (updated,pst=D), // delegate: 1 (new,pst=N-A) vcalendarInvitation.addPropertyWithValue('method', 'REQUEST') attendee.setParameter('partstat', 'DELEGATED') attendee.setParameter('delegated-to', attendee1Delegate.email) delegate = new ICAL.Property('attendee') delegate.setParameter('delegated-from', attendee1.email) delegate.setParameter('cn', attendee1Delegate.displayname) delegate.setParameter('rsvp', 'TRUE') delegate.setParameter('partstat', 'NEEDS-ACTION') delegate.setValue(attendee1Delegate.email) vevent.addProperty(delegate) await _postEvent(webdavAttendee1, attendee1Calendar, vcalendarInvitation, attendee1.email, [attendee1Delegate.email]) vcalendarInvitation.updatePropertyWithValue('method', 'REPLY') await _postEvent(webdavAttendee1, attendee1Calendar, vcalendarInvitation, attendee1.email, [user.email]) vcalendarInvitation.removeProperty('method') await _putEvent(webdavAttendee1, attendee1Calendar, icsName, vcalendarInvitation, 204) vcalendarInvitationDelegate = await _getEvent(webdavAttendee1Delegate, attendee1DelegateCalendar, icsName) _compareAttendees(vcalendarInvitationDelegate, vcalendarInvitation) // 3. delegate accepts // => org: 1 (updated), attendee: 1 (updated,pst=D), // delegate: 1 (accepted,pst=A) vcalendarInvitation.updatePropertyWithValue('method', 'REQUEST') delegate.setParameter('partstat', 'ACCEPTED') await _postEvent(webdavAttendee1Delegate, attendee1DelegateCalendar, vcalendarInvitation, attendee1Delegate.email, [user.email, attendee1.email]) vcalendarInvitation.removeProperty('method') await _putEvent(webdavAttendee1Delegate, attendee1DelegateCalendar, icsName, vcalendarInvitation, 204) vcalendarInvitationOrganizer = await _getEvent(webdav, userCalendar, icsName) _compareAttendees(vcalendarInvitationOrganizer, vcalendarInvitation) // 4. attendee accepts // => org: 1 (updated), attendee: 1 (updated,pst=A), // delegate: 0 (cancelled, deleted) vcalendarCancellation = utility.createCalendar() vcalendarCancellation.addPropertyWithValue('method', 'CANCEL') attendees = vevent.getAllProperties('attendee') vevent = vcalendarCancellation.getFirstSubcomponent('vevent') vevent.updatePropertyWithValue('sequence', '1') vevent.addProperty(ICAL.Property.fromString(organizer.toICALString())) for (attendee of attendees) { vevent.addProperty(ICAL.Property.fromString(attendee.toICALString())) } await _postEvent(webdavAttendee1, attendee1Calendar, vcalendarCancellation, attendee1.email, [attendee1Delegate.email]) vevent = vcalendarInvitation.getFirstSubcomponent('vevent') for (attendee of attendees) { if (attendee.getParameter('delegated-to')) { // console.debug(`delegated-to = ${attendee.toICALString()}`) attendee.removeParameter('delegated-to') attendee.setParameter('partstat', 'ACCEPTED') } else { // Remove delegate attendee vevent.removeProperty(attendee) } } vcalendarInvitation.addPropertyWithValue('method', 'REPLY') await _postEvent(webdavAttendee1, attendee1Calendar, vcalendarInvitation, attendee1.email, [user.email]) vcalendarInvitation.removeProperty('method') await _putEvent(webdavAttendee1, attendee1Calendar, icsName, vcalendarInvitation, 204) vcalendarInvitationOrganizer = await _getEvent(webdav, userCalendar, icsName) _compareAttendees(vcalendarInvitationOrganizer, vcalendarInvitation) // vcalendarInvitationDelegate = await _getEvent(webdavAttendee1Delegate, attendee1DelegateCalendar, icsName, 404) // 5. org updates inv. // => org: 1 (updated), attendee: 1 (updated), delegate: 0 vcalendarInvitation.updatePropertyWithValue('method', 'REQUEST') vevent.updatePropertyWithValue('sequence', '1') vevent.updatePropertyWithValue('last-modified', utility.createDateTimeProperty('last-modified').getFirstValue()) vevent.updatePropertyWithValue('dtstamp', utility.createDateTimeProperty('dtstamp').getFirstValue()) attendee = vevent.getFirstProperty('attendee') attendee.setParameter('partstat', 'NEEDS-ACTION') await _postEvent(webdav, userCalendar, vcalendarInvitation, user.email, [attendee1.email]) vcalendarInvitation.removeProperty('method') await _putEvent(webdav, userCalendar, icsName, vcalendarInvitation, 204) vcalendarInvitationAttendee = await _getEvent(webdavAttendee1, attendee1Calendar, icsName) _compareAttendees(vcalendarInvitationAttendee, vcalendarInvitation) // 6. attendee delegates to delegate // => org: 1 (updated), attendee: 1 (updated), delegate: 1 (new) vcalendarInvitation.updatePropertyWithValue('method', 'REQUEST') attendee.setParameter('partstat', 'DELEGATED') attendee.setParameter('delegated-to', attendee1Delegate.email) delegate = new ICAL.Property('attendee') delegate.setParameter('delegated-from', attendee1.email) delegate.setParameter('cn', attendee1Delegate.displayname) delegate.setParameter('rsvp', 'TRUE') delegate.setParameter('partstat', 'NEEDS-ACTION') delegate.setValue(attendee1Delegate.email) vevent.addProperty(delegate) await _postEvent(webdavAttendee1, attendee1Calendar, vcalendarInvitation, attendee1.email, [attendee1Delegate.email]) vcalendarInvitation.updatePropertyWithValue('method', 'REPLY') await _postEvent(webdavAttendee1, attendee1Calendar, vcalendarInvitation, attendee1.email, [user.email]) vcalendarInvitation.removeProperty('method') await _putEvent(webdavAttendee1, attendee1Calendar, icsName, vcalendarInvitation, 204) vcalendarInvitationOrganizer = await _getEvent(webdav, userCalendar, icsName) _compareAttendees(vcalendarInvitationOrganizer, vcalendarInvitation) vcalendarInvitationDelegate = await _getEvent(webdavAttendee1Delegate, attendee1DelegateCalendar, icsName) _compareAttendees(vcalendarInvitationDelegate, vcalendarInvitation) // 7. delegate accepts // => org: 1 (updated), attendee: 1 (updated), delegate: 1 (accepted) vcalendarInvitation.updatePropertyWithValue('method', 'REPLY') delegate.setParameter('partstat', 'ACCEPTED') await _postEvent(webdavAttendee1Delegate, attendee1DelegateCalendar, vcalendarInvitation, attendee1Delegate.email, [user.email, attendee1.email]) vcalendarInvitation.removeProperty('method') await _putEvent(webdavAttendee1Delegate, attendee1DelegateCalendar, icsName, vcalendarInvitation, 204) vcalendarInvitationOrganizer = await _getEvent(webdav, userCalendar, icsName) _compareAttendees(vcalendarInvitationOrganizer, vcalendarInvitation) vcalendarInvitationAttendee = await _getEvent(webdavAttendee1, attendee1Calendar, icsName) _compareAttendees(vcalendarInvitationAttendee, vcalendarInvitation) // 8. org updates inv. // => org: 1 (updated), attendee: 1 (updated,partstat unchanged), // delegate: 1 (updated,partstat reset) vcalendarInvitation.updatePropertyWithValue('method', 'REQUEST') vevent.updatePropertyWithValue('sequence', '2') vevent.updatePropertyWithValue('last-modified', utility.createDateTimeProperty('last-modified').getFirstValue()) vevent.updatePropertyWithValue('dtstamp', utility.createDateTimeProperty('dtstamp').getFirstValue()) delegate.setParameter('partstat', 'NEEDS-ACTION') await _postEvent(webdav, userCalendar, vcalendarInvitation, user.email, [attendee1.email, attendee1DelegateCalendar.email]) vcalendarInvitation.removeProperty('method') await _putEvent(webdav, userCalendar, icsName, vcalendarInvitation, 204) vcalendarInvitationAttendee = await _getEvent(webdavAttendee1, attendee1Calendar, icsName) _compareAttendees(vcalendarInvitationAttendee, vcalendarInvitation) vcalendarInvitationDelegate = await _getEvent(webdavAttendee1Delegate, attendee1DelegateCalendar, icsName) _compareAttendees(vcalendarInvitationDelegate, vcalendarInvitation) // 9. org cancels invitation // => org: 1 (updated), attendee: 0 (cancelled, deleted), // delegate: 0 (cancelled, deleted) vcalendarInvitation.updatePropertyWithValue('method', 'CANCEL') vevent.updatePropertyWithValue('sequence', '3') vevent.updatePropertyWithValue('last-modified', utility.createDateTimeProperty('last-modified').getFirstValue()) vevent.updatePropertyWithValue('dtstamp', utility.createDateTimeProperty('dtstamp').getFirstValue()) await _postEvent(webdav, userCalendar, vcalendarInvitation, user.email, [attendee1.email, attendee1DelegateCalendar.email]) vcalendarInvitation.removeProperty('method') vevent.removeProperty(attendee) vevent.removeProperty(delegate) await _putEvent(webdav, userCalendar, icsName, vcalendarInvitation, 204) vcalendarInvitationAttendee = await _getEvent(webdavAttendee1, attendee1Calendar, icsName, 404) vcalendarInvitationDelegate = await _getEvent(webdavAttendee1Delegate, attendee1DelegateCalendar, icsName, 404) }) })