From 54dff236829a2b61842d9223fb8ef29d6e115225 Mon Sep 17 00:00:00 2001 From: Francis Lachapelle Date: Fri, 16 Jul 2021 14:42:15 -0400 Subject: [PATCH] test: migration from Python to JavaScript --- Tests/lib/WebDAV.js | 304 +++++++++++++ Tests/lib/config.js | 44 ++ Tests/package.json | 18 + Tests/spec/CalDAVPropertiesSpec.js | 61 +++ Tests/spec/CalDAVSchedulingSpec.js | 532 ++++++++++++++++++++++ Tests/spec/DAVAddressBookAclSpec.js | 242 ++++++++++ Tests/spec/DAVCalendarAclSpec.js | 442 ++++++++++++++++++ Tests/spec/DAVCalendarPublicAclSpec.js | 181 ++++++++ Tests/spec/DAVCalendarSuperUserAclSpec.js | 116 +++++ Tests/spec/DAVPublicAccessSpec.js | 39 ++ Tests/spec/WebDAVSpec.js | 110 +++++ Tests/spec/WebDavSyncSpec.js | 48 ++ 12 files changed, 2137 insertions(+) create mode 100644 Tests/lib/WebDAV.js create mode 100644 Tests/lib/config.js create mode 100644 Tests/package.json create mode 100644 Tests/spec/CalDAVPropertiesSpec.js create mode 100644 Tests/spec/CalDAVSchedulingSpec.js create mode 100644 Tests/spec/DAVAddressBookAclSpec.js create mode 100644 Tests/spec/DAVCalendarAclSpec.js create mode 100644 Tests/spec/DAVCalendarPublicAclSpec.js create mode 100644 Tests/spec/DAVCalendarSuperUserAclSpec.js create mode 100644 Tests/spec/DAVPublicAccessSpec.js create mode 100644 Tests/spec/WebDAVSpec.js create mode 100644 Tests/spec/WebDavSyncSpec.js diff --git a/Tests/lib/WebDAV.js b/Tests/lib/WebDAV.js new file mode 100644 index 000000000..0fc86e83c --- /dev/null +++ b/Tests/lib/WebDAV.js @@ -0,0 +1,304 @@ +import { + DAVNamespace, + DAVNamespaceShorthandMap, + + davRequest, + deleteObject, + getBasicAuthHeaders, + propfind, + syncCollection, + + calendarMultiGet, + createCalendarObject, + makeCalendar, + + createVCard +} from 'tsdav' +import { formatProps, getDAVAttribute } from 'tsdav/dist/util/requestHelpers'; +import { makeCollection } from 'tsdav/dist/collection'; +import config from './config' + +class WebDAV { + constructor(un, pw) { + this.serverUrl = `http://${config.hostname}:${config.port}` + if (un && pw) { + this.headers = getBasicAuthHeaders({ + username: un, + password: pw + }) + } + else { + this.headers = {} + } + } + + deleteObject(resource) { + return deleteObject({ + url: this.serverUrl + resource, + headers: this.headers + }) + } + + makeCalendar(resource) { + return makeCalendar({ + url: this.serverUrl + resource, + headers: this.headers + }) + } + + createCalendarObject(resource, filename, calendar) { + return createCalendarObject({ + headers: this.headers, + calendar: { url: this.serverUrl + resource }, // DAVCalendar + filename: filename, + iCalString: calendar + }) + } + + getEvent(resource, filename) { + return davRequest({ + url: this.serverUrl + resource + filename, + init: { + method: 'GET', + headers: this.headers, + body: null + }, + convertIncoming: false + }) + } + + propfindEvent(resource) { + return propfind({ + url: this.serverUrl + resource, + headers: this.headers, + depth: '1', + props: [ + { name: 'calendar-data', namespace: DAVNamespace.CALDAV } + ] + }) + } + + calendarMultiGet(resource, filename) { + return calendarMultiGet({ + url: this.serverUrl + resource, + headers: this.headers, + props: [ + { name: 'calendar-data', namespace: DAVNamespace.CALDAV }, + ], + objectUrls: [ this.serverUrl + resource + filename ] + }) + } + + principalCollectionSet(resource = '/SOGo/dav') { + return propfind({ + url: this.serverUrl + resource, + depth: '0', + props: [{ name: 'principal-collection-set', namespace: DAVNamespace.DAV }], + headers: this.headers + }) + } + + propfindURL(resource = '/SOGo/dav') { + return propfind({ + url: this.serverUrl + resource, + depth: '1', + props: [ + { name: 'displayname', namespace: DAVNamespace.DAV }, + { name: 'resourcetype', namespace: DAVNamespace.DAV } + ], + headers: this.headers + }) + } + + propfindCollection(resource) { + return propfind({ + url: this.serverUrl + resource, + headers: this.headers + }) + } + + principalPropertySearch(resource) { + return davRequest({ + url: `${this.serverUrl}/SOGo/dav`, + init: { + method: 'REPORT', + namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV], + headers: this.headers, + body: { + 'principal-property-search': { + _attributes: getDAVAttribute([ + DAVNamespace.CALDAV, + DAVNamespace.DAV, + ]), + 'property-search': [ + { + [`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:prop`]: formatProps([{ name: 'calendar-home-set', namespace: DAVNamespace.CALDAV }]), + 'match': resource + } + ], + [`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:prop`]: formatProps([{ name: 'displayname', namespace: DAVNamespace.DAV }]) + } + } + }, + }) + } + + // http://tools.ietf.org/html/rfc3253.html#section-3.8 + expendProperty(resource, properties) { + return davRequest({ + url: `${this.serverUrl}/${resource}`, + init: { + method: 'REPORT', + namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV], + headers: this.headers, + body: { + 'expand-property': { + _attributes: getDAVAttribute([ + DAVNamespace.DAV, + ]), + [`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:property`]: properties + } + } + }, + }) + } + + syncColletion(resource) { + return davRequest({ + url: `${this.serverUrl}/${resource}`, + init: { + method: 'REPORT', + namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV], + headers: this.headers, + body: { + 'sync-collection': { + _attributes: getDAVAttribute([ + DAVNamespace.CALDAV, + DAVNamespace.DAV + ]), + [`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:prop`]: formatProps([{ name: 'calendar-data', namespace: DAVNamespace.CALDAV }]), + } + } + }, + }) + } + + syncQuery(resource, token = '', properties) { + const formattedProperties = properties.map(p => { return { name: p, namespace: DAVNamespace.DAV } }) + return syncCollection({ + url: `${this.serverUrl}/${resource}`, + props: formattedProperties, + syncLevel: 1, + syncToken: token, + headers: this.headers + }) + } + + propfindCaldav(resource, properties, depth = 0, parseOutgoing = true) { + const formattedProperties = properties.map(p => { return { name: p, namespace: DAVNamespace.CALDAV } }) + return davRequest({ + url: this.serverUrl + resource, + init: { + method: 'PROPFIND', + headers: { ...this.headers, depth: new String(depth) }, + namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV], + body: { + propfind: { + _attributes: getDAVAttribute([ + DAVNamespace.CALDAV, + DAVNamespace.CALDAV_APPLE, + DAVNamespace.CALENDAR_SERVER, + DAVNamespace.CARDDAV, + DAVNamespace.DAV + ]), + prop: formattedProperties.length ? formatProps(formattedProperties) : null, + } + } + }, + parseOutgoing + }) + } + + proppatchCaldav(resource, properties) { + const formattedProperties = Object.keys(properties).map(p => { + return { name: p, namespace: DAVNamespace.CALDAV, value: properties[p] } + }) + return davRequest({ + url: this.serverUrl + resource, + init: { + method: 'PROPPATCH', + headers: this.headers, + namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV], + body: { + propertyupdate: { + _attributes: getDAVAttribute([ + DAVNamespace.CALDAV, + DAVNamespace.CALDAV_APPLE, + DAVNamespace.CALENDAR_SERVER, + DAVNamespace.CARDDAV, + DAVNamespace.DAV + ]), + set: { + prop: formatProps(formattedProperties) + } + } + } + } + // parseOutgoing + }) + } + + currentUserPrivilegeSet(resource) { + return propfind({ + url: this.serverUrl + resource, + depth: '0', + props: [ + { name: 'current-user-privilege-set', namespace: DAVNamespace.DAV } + ], + headers: this.headers + }) + } + + makeAddressBook(resource) { + return makeCollection({ + url: this.serverUrl + resource, + headers: this.headers + }); + } + + getCard(resource, filename) { + return davRequest({ + url: this.serverUrl + resource + filename, + init: { + method: 'GET', + headers: this.headers, + body: null + }, + convertIncoming: false + }) + } + + createVCard(resource, filename, card) { + return createVCard({ + headers: this.headers, + addressBook: { url: this.serverUrl + resource }, // DAVAddressBook + filename, + vCardString: card + }) + } + + options(resource) { + return davRequest({ + url: this.serverUrl + resource, + init: { + method: 'OPTIONS', + headers: this.headers, + body: null + }, + convertIncoming: false + }) + } + +} + +export default WebDAV \ No newline at end of file diff --git a/Tests/lib/config.js b/Tests/lib/config.js new file mode 100644 index 000000000..9ba337a5e --- /dev/null +++ b/Tests/lib/config.js @@ -0,0 +1,44 @@ +export default { + // setup: 4 user are needed: username, superuser, attendee1, attendee1_delegate + // superuser must be a sogo superuser... + + hostname: "localhost", + port: "80", + username: "myuser", + password: "mypass", + + superuser: "super", + superuser_password: "pass", + + // 'subscriber_username' and 'attendee1' must be the same user + subscriber_username: "otheruser", + subscriber_password: "otherpass", + + attendee1: "user@domain.com", + attendee1_username: "user", + attendee1_password: "pass", + + attendee1_delegate: "user2@domain.com", + attendee1_delegate_username: "sogo2", + attendee1_delegate_password: "sogo", + + resource_no_overbook: "res", + resource_can_overbook: "res-nolimit", + + // must match attendee1 + white_listed_attendee: { + "sogo1": "John Doe " + }, + + mailserver: "imaphost", + + testput_nbrdays: 30, + + sieve_server: "localhost", + sieve_port: 4190, + + sogo_user: "sogo", + sogo_tool_path: "/usr/local/sbin/sogo-tool", + + webCalendarURL: "http://inverse.ca/sogo-integration-tests/CanadaHolidays.ics" +} \ No newline at end of file diff --git a/Tests/package.json b/Tests/package.json new file mode 100644 index 000000000..d89c6a609 --- /dev/null +++ b/Tests/package.json @@ -0,0 +1,18 @@ +{ + "name": "tests", + "description": "This directory holds automated tests for SOGo.", + "type": "module", + "devDependencies": {}, + "scripts": { + "test": "jasmine --require=esm" + }, + "dependencies": { + "@babel/core": "^7.14.6", + "@babel/preset-env": "^7.14.7", + "babel-cli": "^6.26.0", + "esm": "^3.2.25", + "ical.js": "^1.4.0", + "jasmine": "^3.8.0", + "tsdav": "^1.0.2" + } +} diff --git a/Tests/spec/CalDAVPropertiesSpec.js b/Tests/spec/CalDAVPropertiesSpec.js new file mode 100644 index 000000000..22ab3cfda --- /dev/null +++ b/Tests/spec/CalDAVPropertiesSpec.js @@ -0,0 +1,61 @@ +import config from '../lib/config' +import WebDAV from '../lib/WebDAV' + +describe('read and set calendar properties', function() { + const webdav = new WebDAV(config.username, config.password) + const resource = `/SOGo/dav/${config.username}/Calendar/test-dav-properties/` + + beforeEach(async function() { + await webdav.makeCalendar(resource) + }) + + afterEach(async function() { + await webdav.deleteObject(resource) + }) + + // CalDAVPropertiesTest + + it("propfind", async function() { + const [result] = await webdav.propfindCaldav(resource, ['schedule-calendar-transp']) + const { raw: { multistatus: { response: { propstat: { status, prop }}}}} = result + expect(status) + .withContext('schedule-calendar-transp profind is successful') + .toBe('HTTP/1.1 200 OK') + expect(Object.keys(prop).length) + .withContext('schedule-calendar-transp has one element only') + .toBe(1) + expect(Object.keys(prop.scheduleCalendarTransp).includes('opaque')) + .withContext('schedule-calendar-transp is "opaque" on new') + .toBeTrue() + }) + + it("proppatch", async function() { + let newValueNode + let results + + newValueNode = { 'thisvaluedoesnotexist': {} } + results = await webdav.proppatchCaldav(resource, {'schedule-calendar-transp': newValueNode}) + expect(results.length) + .toBe(1) + expect(results[0].status) + .withContext('Setting an invalid transparency is refused') + .toBe(400) + + newValueNode = { 'transparent': {} } + results = await webdav.proppatchCaldav(resource, {'schedule-calendar-transp': newValueNode}) + expect(results.length) + .toBe(1) + expect(results[0].status) + .withContext(`Setting transparency to ${newValueNode} is successful`) + .toBe(207) + + newValueNode = { 'opaque': {} } + results = await webdav.proppatchCaldav(resource, {'schedule-calendar-transp': newValueNode}) + expect(results.length) + .toBe(1) + expect(results[0].status) + .withContext(`Setting transparency to ${newValueNode} is successful`) + .toBe(207) + + }) +}) \ No newline at end of file diff --git a/Tests/spec/CalDAVSchedulingSpec.js b/Tests/spec/CalDAVSchedulingSpec.js new file mode 100644 index 000000000..aa92593fc --- /dev/null +++ b/Tests/spec/CalDAVSchedulingSpec.js @@ -0,0 +1,532 @@ +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, calendar, filename, expectedCode = 200) { + const [{ status, raw }] = await client.getEvent(calendar, filename) + expect(status).toBe(expectedCode) + if (status <= 300) + return new ICAL.Component(ICAL.parse(raw)) + } + + const _getAllEvents = async function(client, calendar, expectedCode = 207) { + const results = await client.propfindCollection(calendar) + const hrefs = results.filter(r => r.href).map(r => r.href) + + return hrefs + } + + const _newDateTimeProperty = function(propertyName, dateObject = new Date()) { + let property = new ICAL.Property(propertyName) + property.setParameter('tzid', 'America/Montreal') + property.setValue(ICAL.Time.fromJSDate(dateObject)) + + return property + } + + const _newEvent = function(summary = 'test event', uid = 'test', transp = 'OPAQUE') { + const vcalendar = new ICAL.Component('vcalendar') + const vevent = new ICAL.Component('vevent') + const now = new Date() + const later = new Date(now.getTime() + 1000*60*60) + + vcalendar.addSubcomponent(vevent) + vevent.addPropertyWithValue('uid', uid) + vevent.addPropertyWithValue('summary', summary) + vevent.addPropertyWithValue('transp', transp) + vevent.addProperty(_newDateTimeProperty('dtstart', now)) + vevent.addProperty(_newDateTimeProperty('dtend', later)) + vevent.addProperty(_newDateTimeProperty('dtstamp', now)) + vevent.addProperty(_newDateTimeProperty('last-modified', now)) + vevent.addProperty(_newDateTimeProperty('created', now)) + vevent.addPropertyWithValue('class', 'PUBLIC') + vevent.addPropertyWithValue('sequence', '0') + + return vcalendar + } + + const _putEvent = async function(client, calendar, filename, event, expectedCode = 201) { + const response = await client.createCalendarObject(calendar, filename, event.toString()) + expect(response.status) + .withContext(`Event creation 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, calendar, expectedCode = 204) { + const hrefs = await _getAllEvents(client, calendar) + for (const href of hrefs) { + await _deleteEvent(client, href) // ignore returned code + } + return hrefs + } + + 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() { + // make sure the event doesn't exist + const icsName = 'test-add-attendee.ics' + icsList.push(icsName) + await _deleteEvent(webdav, userCalendar + icsName) + await _deleteEvent(webdavAttendee1, attendee1Calendar + icsName) + + // 1. create an event in the organiser's calendar + let calendar = _newEvent('Test add attendee', 'Test add attendee') + let event = calendar.getFirstSubcomponent('vevent') + let organizer = new ICAL.Property('organizer') + organizer.setParameter('cn', user.displayname) + organizer.setValue(user.email) + event.addProperty(organizer) + await _putEvent(webdav, userCalendar, icsName, calendar) + + // 2. add an attendee + calendar.addPropertyWithValue('method', 'REQUEST') + let attendee = new ICAL.Property('attendee') + attendee.setParameter('cn', attendee1.displayname) + attendee.setParameter('rsvp', 'TRUE') + attendee.setParameter('partstat', 'NEEDS-ACTION') + attendee.setValue(attendee1.email) + event.addProperty(attendee) + await _putEvent(webdav, userCalendar, icsName, calendar, 204) + + // 3. verify that the attendee has the event + let attendeeCalendar = await _getEvent(webdavAttendee1, attendee1Calendar, icsName) + + // 4. make sure the received event match the original one + let attendeeEvent = attendeeCalendar.getFirstSubcomponent('vevent') + expect(attendeeEvent.getFirstProperty('uid').getFirstValue()) + .toBe(event.getFirstProperty('uid').getFirstValue()) + }) + + it('Remove attendee after event creation', async function() { + const icsName = 'test-remove-attendee.ics' + icsList.push(icsName) + + // make sure the event doesn't exist + await _deleteEvent(webdav, userCalendar + icsName) + await _deleteEvent(webdavAttendee1, attendee1Calendar + icsName) + + // 1. create an event in the organiser's calendar + let calendar = _newEvent('Test uninvite attendee', 'Test uninvite attendee') + let event = calendar.getFirstSubcomponent('vevent') + let organizer = new ICAL.Property('organizer') + organizer.setParameter('cn', user.displayname) + organizer.setValue(user.email) + event.addProperty(organizer) + await _putEvent(webdav, userCalendar, icsName, calendar) + + // keep a copy around for updates without other attributes + let noAttendeeEvent = ICAL.Component.fromString(calendar.toString()) + + // 2. add an attendee + calendar.addPropertyWithValue('method', 'REQUEST') + let attendee = new ICAL.Property('attendee') + attendee.setParameter('cn', attendee1.displayname) + attendee.setParameter('rsvp', 'TRUE') + attendee.setParameter('partstat', 'NEEDS-ACTION') + attendee.setValue(attendee1.email) + event.addProperty(attendee) + await _putEvent(webdav, userCalendar, icsName, calendar, 204) + + // 3. verify that the attendee has the event + let attendeeCalendar = await _getEvent(webdavAttendee1, attendee1Calendar, icsName) + + // 4. make sure the received event match the original one + let attendeeEvent = attendeeCalendar.getFirstSubcomponent('vevent') + expect(attendeeEvent.getFirstProperty('uid').getFirstValue()) + .toBe(event.getFirstProperty('uid').getFirstValue()) + + // 5. uninvite the attendee - put the event back without the attendee + event = noAttendeeEvent.getFirstSubcomponent('vevent') + event.addProperty(_newDateTimeProperty('last-modified')) + await _putEvent(webdav, userCalendar, icsName, noAttendeeEvent, 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() { + let calendar, event, organizer, attendee + + // make sure there are no events in the resource calendar + await _deleteAllEvents(webdav_su, resourceNoOverbookCalendar) + + // make sure the events don't exist + const icsName = 'test-no-overbook.ics' + icsList.push(icsName) + await _deleteEvent(webdav, userCalendar + icsName) + + const obIcsName = 'test-no-overbook-overlap.ics' + icsList.push(obIcsName) + await _deleteEvent(webdav, userCalendar + obIcsName) + + // 1. create an event in the organiser's calendar + calendar = _newEvent('Test no overbook', 'Test no overbook') + event = calendar.getFirstSubcomponent('vevent') + organizer = new ICAL.Property('organizer') + organizer.setParameter('cn', user.displayname) + organizer.setValue(user.email) + event.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) + event.addProperty(attendee) + await _putEvent(webdav, userCalendar, icsName, calendar) + + // 2. create a second event overlapping the first one + calendar = _newEvent('Test no overbook - overlap', 'Test no overbook - overlap') + event = calendar.getFirstSubcomponent('vevent') + organizer = new ICAL.Property('organizer') + organizer.setParameter('cn', user.displayname) + organizer.setValue(user.email) + event.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) + event.addProperty(attendee) + + // put the event - should trigger a 409 + await _putEvent(webdav, userCalendar, obIcsName, calendar, 409) + }) + + it('try to overbook a resource - multiplebookings=0', async function() { + let calendar, event, organizer, attendee + + // make sure there are no events in the resource calendar + await _deleteAllEvents(webdav_su, resourceCanOverbookCalendar) + + // make sure the events don't exist + const icsName = 'test-can-overbook.ics' + icsList.push(icsName) + await _deleteEvent(webdav, userCalendar + icsName) + + const obIcsName = 'test-can-overbook-overlap.ics' + icsList.push(obIcsName) + await _deleteEvent(webdav, userCalendar + obIcsName) + + // 1. create an event in the organiser's calendar + calendar = _newEvent('Test can overbook', 'Test can overbook') + event = calendar.getFirstSubcomponent('vevent') + organizer = new ICAL.Property('organizer') + organizer.setParameter('cn', user.displayname) + organizer.setValue(user.email) + event.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) + event.addProperty(attendee) + await _putEvent(webdav, userCalendar, icsName, calendar) + + // 2. create a second event overlapping the first one + calendar = _newEvent('Test can overbook - overlap', 'Test can overbook - overlap') + event = calendar.getFirstSubcomponent('vevent') + organizer = new ICAL.Property('organizer') + organizer.setParameter('cn', user.displayname) + organizer.setValue(user.email) + event.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) + event.addProperty(attendee) + + // put the event - should be fine since we can overbook this one + await _putEvent(webdav, userCalendar, obIcsName, calendar) + }) + + 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) + + let calendar, event, organizer, attendee, rrule, recur + let noOverlapCalendar, nstartdate, nenddate + + // make sure there are no events in the resource calendar + await _deleteAllEvents(webdav_su, resourceNoOverbookCalendar) + + // make sure the event doesn't exist + const icsName = 'test-res-overlap-detection.ics' + icsList.push(icsName) + await _deleteEvent(webdav, userCalendar + icsName) + + const overlapIcsName = 'test-res-overlap-detection-overlap.ics' + icsList.push(overlapIcsName) + await _deleteEvent(webdav, attendee1Calendar + overlapIcsName) // TODO: validate calendar + + 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 + calendar = _newEvent('Recurring event with resource', 'Recurring event with resource') + event = calendar.getFirstSubcomponent('vevent') + rrule = new ICAL.Property('rrule') + recur = new ICAL.Recur({ freq: 'DAILY', count: 5 }) + rrule.setValue(recur) + event.addProperty(rrule) + organizer = new ICAL.Property('organizer') + organizer.setParameter('cn', user.displayname) + organizer.setValue(user.email) + event.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) + event.addProperty(attendee) + + // keep a copy around for #3 + noOverlapCalendar = ICAL.Component.fromString(calendar.toString()) + + await _putEvent(webdav, userCalendar, icsName, calendar) + + // 2. Create single event overlaping one instance for the previous event + calendar = _newEvent('Recurring event with resource - overlap', 'Recurring event with resource - overlap') + event = calendar.getFirstSubcomponent('vevent') + organizer = new ICAL.Property('organizer') + organizer.setParameter('cn', attendee1.displayname) + organizer.setValue(attendee1.email) + event.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) + event.addProperty(attendee) + + // should fail + await _putEvent(webdavAttendee1, attendee1Calendar, overlapIcsName, calendar, 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 + event = noOverlapCalendar.getFirstSubcomponent('vevent') + nstartdate = event.getFirstProperty('dtend').getFirstValue().toJSDate() + nstartdate = new Date(nstartdate.getTime() + 1000*60*60) + nenddate = new Date(nstartdate.getTime() + 1000*60*60) + event.removeProperty('dtstart') + event.removeProperty('dtend') + event.addProperty(_newDateTimeProperty('dtstart', nstartdate)) + event.addProperty(_newDateTimeProperty('dtend', nenddate)) + event.updatePropertyWithValue('uid', 'recurring - nooverlap') + await _putEvent(webdav, userCalendar, noOverlapRecurringIcsName, noOverlapCalendar) + + // 4. Create recurring event overlapping the previous recurring event + // should fail with a 409 + nstartdate = event.getFirstProperty('dtstart').getFirstValue().toJSDate() + nstartdate = new Date(nstartdate.getTime() + 1000*60*5) + nenddate = new Date(nstartdate.getTime() + 1000*60*60) + event.removeProperty('dtstart') + event.removeProperty('dtend') + event.addProperty(_newDateTimeProperty('dtstart', nstartdate)) + event.addProperty(_newDateTimeProperty('dtend', nenddate)) + event.updatePropertyWithValue('uid', 'recurring - nooverlap') + await _putEvent(webdav, userCalendar, overlapRecurringIcsName, noOverlapCalendar, 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 + + let vcalendar, vevent, summary, uid, rrule, recur + let originalStartDate, originalEndDate + + const icsName = 'test-rrule-exception-invitation-dance.ics' + icsList.push(icsName) + + await _deleteEvent(webdav, userCalendar + icsName) + await _deleteEvent(webdav, attendee1Calendar + icsName) + + summary = 'Test reccuring exception invite cancel' + uid = 'Test-recurring-exception-invite-cancel' + vcalendar = _newEvent(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 + let 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(_newDateTimeProperty('last-modified')) + originalStartDate = vevent.getFirstPropertyValue('dtstart') + originalEndDate = vevent.getFirstPropertyValue('dtend') + + let veventEx = new ICAL.Component('vevent') + veventEx.addProperty(_newDateTimeProperty('created')) + veventEx.addProperty(_newDateTimeProperty('last-modified')) + veventEx.addProperty(_newDateTimeProperty('dtstamp')) + veventEx.addPropertyWithValue('uid', uid) + veventEx.addPropertyWithValue('summary', summary) + veventEx.addPropertyWithValue('transp', 'OPAQUE') + veventEx.addPropertyWithValue('description', 'Exception') + veventEx.addPropertyWithValue('sequence', '1') + veventEx.addProperty(vevent.getFirstProperty('dtstart')) + veventEx.addProperty(vevent.getFirstProperty('dtend')) + // out of laziness, add the exception for the first occurence of the event + let recurrenceId = new ICAL.Property('recurrence-id') + recurrenceId.setParameter('tzid', originalStartDate.timezone) + recurrenceId.setValue(originalStartDate) + veventEx.addProperty(recurrenceId) + + // 2.1 Add attendee1 and organizer to the exception + let organizer = new ICAL.Property('organizer') + organizer.setParameter('cn', user.displayname) + organizer.setParameter('partstat', 'ACCEPTED') + organizer.setValue(user.email) + veventEx.addProperty(organizer) + let 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) + veventEx.addProperty(attendee) + vcalendarOrganizer.addSubcomponent(veventEx) + + await _putEvent(webdav, userCalendar, icsName, vcalendarOrganizer, 204) + + // 3. Make sure the attendee got the event + let vcalendarAttendee = await _getEvent(webdavAttendee1, attendee1Calendar, icsName) + let 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() + let 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) + let veventMaster, veventException + 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) + }) +}) \ No newline at end of file diff --git a/Tests/spec/DAVAddressBookAclSpec.js b/Tests/spec/DAVAddressBookAclSpec.js new file mode 100644 index 000000000..51e91d828 --- /dev/null +++ b/Tests/spec/DAVAddressBookAclSpec.js @@ -0,0 +1,242 @@ +import config from '../lib/config' +import WebDAV from '../lib/WebDAV' +import TestUtility from '../lib/utilities' + +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 webdav_subscriber = new WebDAV(config.subscriber_username, config.subscriber_password) + const utility = new TestUtility(webdav) + + const cards = { + 'new.vcf': `BEGIN:VCARD +VERSION:3.0 +PRODID:-//Inverse//Card Generator//EN +UID:NEWTESTCARD +N:New;Carte +FN:Carte 'new' +ORG:societe;service +NICKNAME:surnom +ADR;TYPE=work:adr2 societe;;adr societe;ville societe;etat soc;code soc;pays soc +ADR;TYPE=home:rue perso 2;;rue perso;ville perso;etat perso;code post perso;pays perso +TEL;TYPE=work:+1 514 123-3372 +TEL;TYPE=home:tel dom +TEL;TYPE=cell:portable +TEL;TYPE=fax:fax +TEL;TYPE=pager:pager +X-MOZILLA-HTML:FALSE +EMAIL;TYPE=work:address.email@domaine.ca +EMAIL;TYPE=home:address.email@domaine2.com +URL;TYPE=home:web perso +TITLE:fonction +URL;TYPE=work:page soc +CUSTOM1:divers1 +CUSTOM2:divers2 +CUSTOM3:divers3 +CUSTOM4:divers4 +NOTE:Remarque +X-AIM:pseudo aim +END:VCARD`, + 'old.vcf': `BEGIN:VCARD +VERSION:3.0 +PRODID:-//Inverse//Card Generator//EN +UID:NEWTESTCARD +N:Old;Carte +FN:Carte 'old' +ORG:societe;service +NICKNAME:surnom +ADR;TYPE=work:adr2 societe;;adr societe;ville societe;etat soc;code soc;pays soc +ADR;TYPE=home:rue perso 2;;rue perso;ville perso;etat perso;code post perso;pays perso +TEL;TYPE=work:+1 514 123-3372 +TEL;TYPE=home:tel dom +TEL;TYPE=cell:portable +TEL;TYPE=fax:fax +TEL;TYPE=pager:pager +X-MOZILLA-HTML:FALSE +EMAIL;TYPE=work:address.email@domaine.ca +EMAIL;TYPE=home:address.email@domaine2.com +URL;TYPE=home:web perso +TITLE:fonction +URL;TYPE=work:page soc +CUSTOM1:divers1 +CUSTOM2:divers2 +CUSTOM3:divers3 +CUSTOM4:divers4 +NOTE:Remarque +X-AIM:pseudo aim +END:VCARD`, + 'new-modified.vcf': `BEGIN:VCARD +VERSION:3.0 +PRODID:-//Inverse//Card Generator//EN +UID:NEWTESTCARD +N:New;Carte modifiee +FN:Carte modifiee 'new' +ORG:societe;service +NICKNAME:surnom +ADR;TYPE=work:adr2 societe;;adr societe;ville societe;etat soc;code soc;pays soc +ADR;TYPE=home:rue perso 2;;rue perso;ville perso;etat perso;code post perso;pays perso +TEL;TYPE=work:+1 514 123-3372 +TEL;TYPE=home:tel dom +TEL;TYPE=cell:portable +TEL;TYPE=fax:fax +TEL;TYPE=pager:pager +X-MOZILLA-HTML:FALSE +EMAIL;TYPE=work:address.email@domaine.ca +EMAIL;TYPE=home:address.email@domaine2.com +URL;TYPE=home:web perso +TITLE:fonction +URL;TYPE=work:page soc +CUSTOM1:divers1 +CUSTOM2:divers2 +CUSTOM3:divers3 +CUSTOM4:divers4 +NOTE:Remarque +X-AIM:pseudo aim +END:VCARD`, + 'old-modified.vcf': `BEGIN:VCARD +VERSION:3.0 +PRODID:-//Inverse//Card Generator//EN +UID:NEWTESTCARD +N:Old;Carte modifiee +FN:Carte modifiee 'old' +ORG:societe;service +NICKNAME:surnom +ADR;TYPE=work:adr2 societe;;adr societe;ville societe;etat soc;code soc;pays soc +ADR;TYPE=home:rue perso 2;;rue perso;ville perso;etat perso;code post perso;pays perso +TEL;TYPE=work:+1 514 123-3372 +TEL;TYPE=home:tel dom +TEL;TYPE=cell:portable +TEL;TYPE=fax:fax +TEL;TYPE=pager:pager +X-MOZILLA-HTML:FALSE +EMAIL;TYPE=work:address.email@domaine.ca +EMAIL;TYPE=home:address.email@domaine2.com +URL;TYPE=home:web perso +TITLE:fonction +URL;TYPE=work:page soc +CUSTOM1:divers1 +CUSTOM2:divers2 +CUSTOM3:divers3 +CUSTOM4:divers4 +NOTE:Remarque +X-AIM:pseudo aim +END:VCARD` + } + + const sogoRights = { + c: 'ObjectCreator', + d: 'ObjectEraser', + v: 'ObjectViewer', + e: 'ObjectEditor' + } + + const resource = `/SOGo/dav/${config.username}/Contacts/test-dav-acl/` + + const _putCard = async function(client, filename, expectedCode, realCard) { + const card = cards[realCard || filename] + if (!card) + throw new Error(`Card ${realCard || filename} is unknown`) + const response = await client.createVCard(resource, filename, card) + expect(response.status).toBe(expectedCode) + } + + const _getCard = async function(client, filename, expectedCode) { + const [{ status }] = await client.getCard(resource, filename) + expect(status).toBe(expectedCode) + } + + const _deleteCard = async function(client, filename, expectedCode = 204) { + const response = await client.deleteObject(resource + filename) + expect(response.status) + .withContext('HTTP status code when deleting a card') + .toBe(expectedCode) + } + + const _testView = async function(rights) { + let expectedCode = 403 + if (rights && (rights.v || rights.e)) { + expectedCode = 200 + } + await _getCard(webdav_subscriber, 'old.vcf', expectedCode) + } + + const _testCreate = async function(rights) { + let expectedCode + if (rights && rights.c) + expectedCode = 201 + else + expectedCode = 403 + await _putCard(webdav_subscriber, 'new.vcf', expectedCode) + } + + const _testModify = async function(rights) { + let expectedCode + if (rights && rights.e) + expectedCode = 204 + else + expectedCode = 403 + await _putCard(webdav_subscriber, 'old.vcf', expectedCode, 'old-modified.vcf') + } + + const _testDelete = async function(rights) { + let expectedCode = 403 + if (rights && rights.d) { + expectedCode = 204 + } + await _deleteCard(webdav_subscriber, 'old.vcf', expectedCode) + } + + const _testRights = async function(rights) { + const results = await utility.setupAddressBookRights(resource, config.subscriber_username, rights) + expect(results.length).toBe(1) + expect(results[0].status).toBe(204) + await _testCreate(rights) + await _testView(rights) + await _testModify(rights) + await _testDelete(rights) + } + + beforeEach(async function() { + await webdav.deleteObject(resource) + await webdav.makeAddressBook(resource) + await _putCard(webdav, 'old.vcf', 201) + }) + + afterEach(async function() { + await webdav_su.deleteObject(resource) + }) + + // DAVAddressBookAclTest + + it("'view' only", async function() { + await _testRights({ v: true }) + }) + + it("'edit' only", async function() { + await _testRights({ e: true }) + }) + + it("'create' only", async function() { + await _testRights({ c: true }) + }) + + it("'delete' only", async function() { + await _testRights({ d: true }) + }) + + it("'create', 'delete'", async function() { + await _testRights({ c: true, d: true }) + }) + + it("'view', 'delete'", async function() { + await _testRights({ v: true, d: true }) + }) + + it("'edit', 'create'", async function() { + await _testRights({ c: true, e: true }) + }) + + it("'edit', 'delete'", async function() { + await _testRights({ d: true, e: true }) + }) +}) \ No newline at end of file diff --git a/Tests/spec/DAVCalendarAclSpec.js b/Tests/spec/DAVCalendarAclSpec.js new file mode 100644 index 000000000..2feea588f --- /dev/null +++ b/Tests/spec/DAVCalendarAclSpec.js @@ -0,0 +1,442 @@ +import config from '../lib/config' +import WebDAV from '../lib/WebDAV' +import TestUtility from '../lib/utilities' + +describe('create, read, modify, delete events for regular user', function() { + const webdav = new WebDAV(config.username, config.password) + const webdav_su = new WebDAV(config.superuser, config.superuser_password) + const webdav_subscriber = new WebDAV(config.subscriber_username, config.subscriber_password) + const utility = new TestUtility(webdav) + + const event_template = `BEGIN:VCALENDAR +PRODID:-//Inverse//Event Generator//EN +VERSION:2.0 +BEGIN:VEVENT +SEQUENCE:0 +TRANSP:OPAQUE +UID:12345-%(class)-%(filename) +SUMMARY:%(class) event (orig. title) +DTSTART:20090805T100000Z +DTEND:20090805T140000Z +CLASS:%(class) +DESCRIPTION:%(class) description +LOCATION:location +%(organizer_line)%(attendee_line)CREATED:20090805T100000Z +DTSTAMP:20090805T100000Z +END:VEVENT +END:VCALENDAR` + + const task_template = `BEGIN:VCALENDAR +PRODID:-//Inverse//Event Generator//EN +VERSION:2.0 +BEGIN:VTODO +CREATED:20100122T201440Z +LAST-MODIFIED:20100201T175246Z +DTSTAMP:20100201T175246Z +UID:12345-%(class)-%(filename) +SUMMARY:%(class) event (orig. title) +CLASS:%(class) +DESCRIPTION:%(class) description +STATUS:IN-PROCESS +PERCENT-COMPLETE:0 +END:VTODO +END:VCALENDAR` + + const resource = `/SOGo/dav/${config.username}/Calendar/test-dav-acl/` + const classToICSClass = { + 'pu': 'PUBLIC', + 'pr': 'PRIVATE', + 'co': 'CONFIDENTIAL' + } + + let user + + const _checkViewEventRight = function(operation, event, eventClass, right) { + if (right) { + expect(event).toBeTruthy() + if (['v', 'r', 'm'].includes(right)) { + const iscClass = classToICSClass[eventClass] + const expectedEvent = utility.formatTemplate(event_template, { + 'class': iscClass, + 'filename': `${iscClass.toLowerCase()}-event.ics` + }) + expect(event).toBe(expectedEvent) + } + else if (right == 'd') { + _testEventIsSecureVersion(eventClass, event) + } + else { + throw new Error(`Right '${right} is not supported`) + } + } + else { + expect(event).toBeFalsy() + } + } + + const _currentUserPrivilegeSet = async function(resource, expectedCode = 207) { + const results = await webdav_subscriber.currentUserPrivilegeSet(resource) + expect(results.length).toBe(1) + const response = results[0] + expect(response.status).toBe(expectedCode) + let privileges = [] + if (expectedCode < 300) { + privileges = response.props.currentUserPrivilegeSet.privilege.map(o => { + return Object.keys(o)[0] + }) + } + return privileges + } + + const _deleteEvent = async function(client, filename, expectedCode = 204) { + const response = await client.deleteObject(resource + filename) + expect(response.status).toBe(expectedCode) + } + + const _getEvent = async function(eventClass, isInvitation = false) { + const iscClass = classToICSClass[eventClass].toLowerCase() + const filename = (isInvitation ? `invitation-${iscClass}` : iscClass) + '-event.ics' + const [{ status, raw = '' }] = await webdav_subscriber.getEvent(resource, filename) + + if (status == 200) + return raw.replace(/\r\n/g,'\n') + else + return undefined + } + + const _multigetEvent = async function(eventClass) { + const iscClass = classToICSClass[eventClass].toLowerCase() + const filename = `${iscClass}-event.ics` + let event = undefined + const results = await webdav_subscriber.calendarMultiGet(resource, filename) + if (results.status !== 404) { + results.find(o => { + if (o.href == resource + filename) { + const { props: { calendarData = '' } } = o + event = calendarData.replace(/\r\n/g,'\n') + return true + } + return false + }) + } + return event + } + + const _propfindEvent = async function(eventClass) { + const iscClass = classToICSClass[eventClass].toLowerCase() + const filename = `${iscClass}-event.ics` + const results = await webdav_subscriber.propfindEvent(resource + filename) + let event = undefined + if (results.status !== 404) { + results.find(o => { + if (o.href == resource + filename) { + const { props: { calendarData = '' } } = o + event = calendarData.replace(/\r\n/g,'\n') + return true + } + return false + }) + } + return event + } + + const _putEvent = async function(client, filename, eventClass = 'PUBLIC', expectedCode = 201, organizer, attendee, partstat = 'NEEDS-ACTION') { + const organizer_line = organizer ? `ORGANIZER:${organizer}\n` : '' + const attendee_line = attendee ? `ATTENDEE;PARTSTAT=${partstat}:${attendee}\n` : '' + const event = utility.formatTemplate(event_template, { + 'class': eventClass, + 'filename': filename, + organizer_line, + attendee_line + }) + const response = await client.createCalendarObject(resource, filename, event) + expect(response.status).toBe(expectedCode) + } + + const _webdavSyncEvent = async function(eventClass) { + const iscClass = classToICSClass[eventClass].toLowerCase() + const filename = `${iscClass}-event.ics` + let event = undefined + const results = await webdav_subscriber.syncColletion(resource) + if (results.status !== 404) { + results.find(o => { + if (o.href == resource + filename) { + const { props: { calendarData = '' } } = o + event = calendarData.length ? calendarData.replace(/\r\n/g,'\n') : undefined + return true + } + return false + }) + } + return event + } + + const _testCreate = async function(rights) { + let expectedCode + if (rights.c) + expectedCode = 201 + else if (Object.keys(rights).length === 0) + expectedCode = 404 + else + expectedCode = 403 + return _putEvent(webdav_subscriber, 'creation-test.ics', 'PUBLIC', expectedCode) + } + + const _testCollectionDAVAcl = async function(rights) { + let expectedPrivileges = [] + if (Object.keys(rights).length > 0) { + expectedPrivileges.push('read', 'readCurrentUserPrivilegeSet', 'readFreeBusy') + } + if (rights.c) { + expectedPrivileges.push( + 'bind', + 'writeContent', + 'schedule', + 'schedulePost', + 'schedulePostVevent', + 'schedulePostVtodo', + 'schedulePostVjournal', + 'schedulePostVfreebusy', + 'scheduleDeliver', + 'scheduleDeliverVevent', + 'scheduleDeliverVtodo', + 'scheduleDeliverVjournal', + 'scheduleDeliverVfreebusy', + 'scheduleRespond', + 'scheduleRespondVevent', + 'scheduleRespondVtodo' + ) + } + if (rights.d) { + expectedPrivileges.push('unbind') + } + const expectedCode = (expectedPrivileges.length == 0) ? 404 : 207 + const privileges = await _currentUserPrivilegeSet(resource, expectedCode) + + // When comparing privileges on DAV collection, we remove all 'default' + // privileges on the collection. + for (const c of ['Public', 'Private', 'Confidential']) { + for (const r of ['viewdant', 'viewwhole', 'modify', 'respondto']) { + const i = privileges.indexOf(`${r}${c}Records`) + if (i >= 0) { + privileges.splice(i, 1) + } + } + } + // for (const privilege of ['read', 'readCurrentUserPrivilegeSet', 'readFreeBusy']) { + for (const expectedPrivilege of expectedPrivileges) { + expect(privileges).toContain(expectedPrivilege) + } + } + + const _testEventIsSecureVersion = function(eventClass, event) { + const iscClass = classToICSClass[eventClass].toLowerCase().replace(/^\w/, c => c.toUpperCase()) + const expectedDict = { + version: 'VERSION:2.0', + prodid: 'PRODID:-//Inverse//Event Generator//EN', + summary: `SUMMARY:(${iscClass} event)`, + dtstart: 'DTSTART:20090805T100000Z', + dtend: 'DTEND:20090805T140000Z', + dtstamp: 'DTSTAMP:20090805T100000Z', + 'x-sogo-secure': 'X-SOGO-SECURE:YES' + } + const eventDict = utility.versitDict(event) + // Ignore UID + for (const key of Object.keys(eventDict).filter(k => k !== 'uid')) { + expect(expectedDict[key]) + .withContext(`Key ${key} of secure event is expected`) + .toBeTruthy() + if (expectedDict[key]) + expect(expectedDict[key]) + .withContext(`Value of key ${key} of secure event is valid`) + .toBe(eventDict[key]) + } + for (const key of Object.keys(expectedDict)) { + expect(eventDict[key]) + .withContext(`Key ${key} of secure event is present`) + .toBeTruthy() + } + } + + const _testModify = async function(eventClass, right, errorCode) { + const iscClass = classToICSClass[eventClass] + const filename = `${iscClass.toLowerCase()}-event.ics` + let expectedCode = errorCode + if (['r', 'm'].includes(right)) + expectedCode = 204 + return _putEvent(webdav_subscriber, filename, iscClass, expectedCode) + } + + const _testRespondTo = async function(eventClass, right, errorCode) { + const iscClass = classToICSClass[eventClass] + const filename = `invitation-${iscClass.toLowerCase()}-event.ics` + let expectedCode = errorCode + if (['r', 'm'].includes(right)) + expectedCode = 204 + + await _putEvent(webdav, filename, iscClass, 201, 'mailto:nobody@somewhere.com', user.email, 'NEEDS-ACTION') + + // here we only do 'passive' validation: if a user has a "respond to" + // right, only the attendee entry will me modified. The change of + // organizer must thus be silently ignored below. + await _putEvent(webdav_subscriber, filename, iscClass, expectedCode, 'mailto:someone@nowhere.com', user.email, 'ACCEPTED') + + if (expectedCode == 204) { + const attendee_line = `ATTENDEE;PARTSTAT=ACCEPTED:${user.email}\n` + let expectedEvent + if (right == 'r') { + expectedEvent = utility.formatTemplate(event_template, { + 'class': iscClass, + 'filename': filename, + organizer_line: 'ORGANIZER;CN=nobody@somewhere.com:mailto:nobody@somewhere.com\n', + attendee_line + }) + } + else { + expectedEvent = utility.formatTemplate(event_template, { + 'class': iscClass, + 'filename': filename, + organizer_line: 'ORGANIZER;CN=someone@nowhere.com:mailto:someone@nowhere.com\n', + attendee_line + }) + } + const event = await _getEvent(eventClass, true) + expect(utility.calendarsAreEqual(expectedEvent, event)) + .withContext('Calendars of organizer and attendee are identical') + .toBe(true) + } + } + + const _testEventDAVAcl = async function(eventClass, right, errorCode) { + const iscClass = classToICSClass[eventClass].toLowerCase() + for (const suffix of ['event', 'task']) { + const filename = `${iscClass}-${suffix}.ics` + let expectedCode = errorCode + let expectedPrivileges = [] + if (right) { + expectedCode = 207 + expectedPrivileges.push('readCurrentUserPrivilegeSet', 'viewDateAndTime', 'read') + if (right != 'd') { + expectedPrivileges.push('viewWholeComponent') + if (right != 'v') { + expectedPrivileges.push('respondToComponent', 'writeContent') + if (right != 'r') { + expectedPrivileges.push('writeProperties', 'write') + } + } + } + } + const privileges = await _currentUserPrivilegeSet(resource + filename, expectedCode) + if (errorCode != expectedCode) { + for (const expectedPrivilege of expectedPrivileges) { + expect(privileges).toContain(expectedPrivilege) + } + } + } + } + + const _testEventRight = async function(eventClass, rights) { + const right = Object.keys(rights).includes(eventClass) ? rights[eventClass] : undefined + + let event = await _getEvent(eventClass) + _checkViewEventRight('GET', event, eventClass, right) + + event = await _propfindEvent(eventClass) + _checkViewEventRight('PROPFIND', event, eventClass, right) + + event = await _multigetEvent(eventClass) + _checkViewEventRight('multiget', event, eventClass, right) + + event = await _webdavSyncEvent(eventClass) + _checkViewEventRight('webdav-sync', event, eventClass, right) + + const errorCode = (Object.keys(rights).length > 0) ? 403 : 404 + await _testModify(eventClass, right, errorCode) + await _testRespondTo(eventClass, right, errorCode) + await _testEventDAVAcl(eventClass, right, errorCode) + } + + const _testDelete = async function(rights) { + let expectedCode = 403 + if (rights && rights.d) { + expectedCode = 204 + } + else if (Object.keys(rights) == 0) { + expectedCode = 404 + } + for (const eventClass of Object.values(classToICSClass)) { + await _deleteEvent(webdav_subscriber, `${eventClass.toLocaleLowerCase()}-event.ics`, expectedCode) + } + } + + const _testRights = async function(rights) { + const results = await utility.setupCalendarRights(resource, config.subscriber_username, rights) + expect(results.length).toBe(1) + expect(results[0].status).toBe(204) + await _testCreate(rights) + await _testCollectionDAVAcl(rights) + await _testEventRight('pu', rights) + await _testEventRight('pr', rights) + await _testEventRight('co', rights) + await _testDelete(rights) + } + + beforeEach(async function() { + user = await utility.fetchUserInfo(config.username) + await webdav.deleteObject(resource) + await webdav.makeCalendar(resource) + for (const c of Object.values(classToICSClass)) { + // Create event for each class + const eventFilename = `${c.toLowerCase()}-event.ics` + const event = utility.formatTemplate(event_template, { + 'class': c, + 'filename': eventFilename + }) + let response = await webdav.createCalendarObject(resource, eventFilename, event) + expect(response.status).toBe(201) + // Create task for each class + const taskFilename = `${c.toLowerCase()}-task.ics` + const task = utility.formatTemplate(task_template, { + 'class': c, + 'filename': taskFilename + }) + response = await webdav.createCalendarObject(resource, taskFilename, task) + expect(response.status).toBe(201) + } + }) + + afterEach(async function() { + await webdav_su.deleteObject(resource) + }) + + // DAVCalendarAclTest + + it("'view all' on a specific class (PUBLIC)", async function() { + await _testRights({ pu: 'v' }) + }) + + it("'modify' PUBLIC, 'view all' PRIVATE, 'view d&t' confidential", async function() { + await _testRights({ pu: 'm', pr: 'v', co: 'd' }) + }) + + it("'create' only", async function() { + await _testRights({ c: true }) + }) + + it("'delete' only", async function() { + await _testRights({ d: true }) + }) + + it("'create', 'delete', 'view d&t' PUBLIC, 'modify' PRIVATE", async function() { + await _testRights({ c: true, d: true, pu: 'd', pr: 'm' }) + }) + + it("'create', 'respond to' PUBLIC", async function() { + await _testRights({ c: true, pu: 'r' }) + }) + + it("no right given", async function() { + await _testRights({}) + }) + +}) \ No newline at end of file diff --git a/Tests/spec/DAVCalendarPublicAclSpec.js b/Tests/spec/DAVCalendarPublicAclSpec.js new file mode 100644 index 000000000..54607eaee --- /dev/null +++ b/Tests/spec/DAVCalendarPublicAclSpec.js @@ -0,0 +1,181 @@ +import config from '../lib/config' +import WebDAV from '../lib/WebDAV' +import TestUtility from '../lib/utilities' + +describe('public access', function() { + const webdav = new WebDAV(config.username, config.password) + const webdav_anon = new WebDAV() + const webdav_su = new WebDAV(config.superuser, config.superuser_password) + const webdav_subscriber = new WebDAV(config.subscriber_username, config.subscriber_password) + const utility = new TestUtility(webdav) + const utility_subscriber = new TestUtility(webdav_subscriber) + let createdRsrc + + // DAVCalendarPublicAclTest + + afterEach(async function() { + if (createdRsrc) { + await webdav_su.deleteObject(createdRsrc) + } + }) + + it("normal user access to (non-)shared resource from su", async function() { + const parentColl = `/SOGo/dav/${config.username}/Calendar/` + let results + let href + + // 1. all rights removed + createdRsrc = `${parentColl}test-dav-acl/` + for (const rsrc of ['personal', 'test-dav-acl']) { + const resource = `${parentColl}${rsrc}/` + await webdav.makeCalendar(resource) + await utility.setupRights(resource, 'anonymous', {}) + await utility.setupRights(resource, config.subscriber_username, {}) + await utility.setupRights(resource, '', {}) + } + + results = await webdav_subscriber.propfindURL(parentColl) + expect(results.length) + .withContext(`Profind returns 1 href when subscriber user ${config.subscriber_username} has no right`) + .toBe(1) + href = results[0].href + expect(href) + .withContext(`Unique href must be the Calendar parent collection ${parentColl}`) + .toBe(parentColl) + + // 2. creation right added + await utility.setupCalendarRights(createdRsrc, config.subscriber_username, { c: true }) + + results = await webdav_subscriber.propfindURL(parentColl) + expect(results.length) + .withContext(`Profind returns 4 href when subscriber user ${config.subscriber_username} has creation right`) + .toBe(4) + href = results[0].href + expect(href) + .withContext(`First href must be the Calendar parent collection ${parentColl}`) + .toBe(parentColl) + + let resourceHrefs = { + [createdRsrc]: false, + [`${createdRsrc.slice(0, -1)}.xml`]: false, + [`${createdRsrc.slice(0, -1)}.ics`]: false + } + for (href of results.map(r => r.href).slice(1)) { + expect(Object.keys(resourceHrefs).includes(href)) + .withContext(`Propfind href ${href} is returned`) + .toBeTrue() + expect(resourceHrefs[href]) + .not.toBeTrue() + resourceHrefs[href] = true + } + + await utility.setupRights(createdRsrc, config.subscriber_username) // remove rights + + // 3. creation right added for "default user" + // subscriber_username expected to have access, but not "anonymous" + await utility.setupCalendarRights(createdRsrc, '', { c: true }) + + results = await webdav_subscriber.propfindURL(parentColl) + expect(results.length) + .withContext('Profind returns 4 href when user has creation right') + .toBe(4) + href = results[0].href + expect(href) + .withContext('First href must be the Calendar parent collection') + .toBe(parentColl) + + resourceHrefs = { + [createdRsrc]: false, + [`${createdRsrc.slice(0, -1)}.xml`]: false, + [`${createdRsrc.slice(0, -1)}.ics`]: false + } + for (href of results.map(r => r.href).slice(1)) { + expect(Object.keys(resourceHrefs).includes(href)) + .withContext(`Propfind href ${href} is returned`) + .toBeTrue() + expect(resourceHrefs[href]) + .withContext(`Propfind href ${href} is returned only once`) + .not.toBeTrue() + resourceHrefs[href] = true + } + + const anonParentColl = `/SOGo/dav/public/${config.username}/Calendar/` + results = await webdav_anon.propfindURL(anonParentColl) + expect(results.length) + .withContext('Profind returns 1 href for anonymous user') + .toBe(1) + href = results[0].href + expect(href) + .withContext('Unique href must be the Calendar parent collection') + .toBe(anonParentColl) + + await utility.setupRights(createdRsrc, '', {}) + + // 4. creation right added for "anonymous" + // "anonymous" expected to have access, but not subscriber_username + + await utility.setupCalendarRights(createdRsrc, 'anonymous', { c: true }) + + results = await webdav_anon.propfindURL(anonParentColl) + expect(results.length) + .withContext('Profind returns 4 href when anonymous user has creation right') + .toBe(4) + href = results[0].href + expect(href) + .withContext('First href must be the Calendar parent collection') + .toBe(anonParentColl) + + const anonRsrc = `${anonParentColl}test-dav-acl/` + resourceHrefs = { + [anonRsrc]: false, + [`${anonRsrc.slice(0, -1)}.xml`]: false, + [`${anonRsrc.slice(0, -1)}.ics`]: false + } + for (href of results.map(r => r.href).slice(1)) { + expect(Object.keys(resourceHrefs).includes(href)) + .withContext(`Propfind href ${href} is returned`) + .toBeTrue() + expect(resourceHrefs[href]) + .withContext(`Propfind href ${href} is returned only once`) + .not.toBeTrue() + resourceHrefs[href] = true + } + + results = await webdav_subscriber.propfindURL(parentColl) + expect(results.length) + .withContext('Profind returns 1 href when user has no right') + .toBe(1) + href = results[0].href + expect(href) + .withContext('First href must be the Calendar parent collection') + .toBe(parentColl) + + }) + + it("user accessing (non-)shared Calendars", async function() { + const parentColl = `/SOGo/dav/${config.subscriber_username}/Calendar/` + let results + + createdRsrc = `${parentColl}test-dav-acl/` + for (const rsrc of ['personal', 'test-dav-acl']) { + const resource = `${parentColl}${rsrc}/` + await webdav_su.makeCalendar(resource) + await utility_subscriber.setupRights(resource, config.username, {}) + } + + results = await webdav_subscriber.propfindURL(parentColl) + const hrefs = results.map(r => r.href).filter(h => { + return h == `${parentColl}` || + h.indexOf(`${parentColl}personal`) == 0 || + h.indexOf(`${parentColl}test-dav-acl`) == 0 + }) + expect(hrefs.length) + .withContext(`Profind returns at least 3 hrefs when user ${config.subscriber_username} is the owner`) + .toBeGreaterThan(2) + const [href] = hrefs + expect(href) + .withContext('Unique href must be the Calendar parent collection') + .toBe(parentColl) + }) + +}) \ No newline at end of file diff --git a/Tests/spec/DAVCalendarSuperUserAclSpec.js b/Tests/spec/DAVCalendarSuperUserAclSpec.js new file mode 100644 index 000000000..4672e79de --- /dev/null +++ b/Tests/spec/DAVCalendarSuperUserAclSpec.js @@ -0,0 +1,116 @@ +import config from '../lib/config' +import WebDAV from '../lib/WebDAV' +import TestUtility from '../lib/utilities' + +describe('DAVCalendarSuperUserAcl', function() { + const webdav = new WebDAV(config.username, config.password) + const webdav_su = new WebDAV(config.superuser, config.superuser_password) + const utility = new TestUtility(webdav) + + const event_template = `BEGIN:VCALENDAR +PRODID:-//Inverse//Event Generator//EN +VERSION:2.0 +BEGIN:VEVENT +SEQUENCE:0 +TRANSP:OPAQUE +UID:12345-%(class)-%(filename) +SUMMARY:%(class) event (orig. title) +DTSTART:20090805T100000Z +DTEND:20090805T140000Z +CLASS:%(class) +DESCRIPTION:%(class) description +LOCATION:location +%(organizer_line)%(attendee_line)CREATED:20090805T100000Z +DTSTAMP:20090805T100000Z +END:VEVENT +END:VCALENDAR` + + const resource = `/SOGo/dav/${config.subscriber_username}/Calendar/test-dav-superuser-acl/` + const filename = 'suevent.ics' + + const event = utility.formatTemplate(event_template, { + 'class': 'PUBLIC', + 'filename': filename + }) + + beforeAll(async function() { + await webdav_su.deleteObject(resource) + await webdav_su.makeCalendar(resource) + }) + + afterAll(async function() { + await webdav_su.deleteObject(resource) + }) + + // DAVCalendarSuperUserAclTest.testSUAccess + it("create, read, modify, delete for superuser", async function() { + let result, results + + // 1. Create + + result = await webdav_su.createCalendarObject(resource, filename, event) + expect(result.status) + .withContext('Event creation returns status code 201') + .toBe(201) + + // 2. Read - GET + + results = await webdav_su.getEvent(resource, filename) + expect(results.length).toBe(1) + expect(results[0].raw.replace(/\r\n/g,'\n')).toBe(event) + + // 2. Read - PROPFIND calendar-data + + results = await webdav_su.propfindEvent(resource + filename) + expect(results.length).toBe(2) // suevent.ics + suevent.ics/master + expect(results.find(o => { + if (o.href == resource + filename) { + expect(o.props.calendarData.replace(/\r\n/g,'\n')).toBe(event) + return true + } + return false + })).toBeTruthy() + + // 2. Read - REPORT calendar-multiget + + results = await webdav_su.calendarMultiGet(resource, filename) + expect(results.length).toBe(1) + expect(results.find(o => { + if (o.href == resource + filename) { + expect(o.props.calendarData.replace(/\r\n/g,'\n')).toBe(event) + return true + } + return false + })).toBeTruthy() + + // 2. Read - webdav-sync + + results = await webdav_su.syncColletion(resource) + expect(results.length).toBe(1) + expect(results.find(o => { + expect(o.status).toBe(201) + if (o.href == resource + filename) { + expect(o.props.calendarData.replace(/\r\n/g,'\n')).toBe(event) + return true + } + return false + })).toBeTruthy() + + // 3. Modify + + const classes = ['CONFIDENTIAL', 'PRIVATE', 'PUBLIC'] + for (const c of classes) { + const event = utility.formatTemplate(event_template, { + 'class': c, + 'filename': filename + }) + const response = await webdav_su.createCalendarObject(resource, filename, event) + expect(response.status).toBe(204) + } + + // 4. Delete + const response = await webdav_su.deleteObject(resource) + expect(response.status).toBe(204) + }) + +}) \ No newline at end of file diff --git a/Tests/spec/DAVPublicAccessSpec.js b/Tests/spec/DAVPublicAccessSpec.js new file mode 100644 index 000000000..d837dd067 --- /dev/null +++ b/Tests/spec/DAVPublicAccessSpec.js @@ -0,0 +1,39 @@ +import config from '../lib/config' +import WebDAV from '../lib/WebDAV' + +describe('public access', function() { + const webdav_anon = new WebDAV() + + // DAVPublicAccessTest + + it("access to /SOGo/so/public", async function() { + const [{ status }] = await webdav_anon.options('/SOGo/so/public') + expect(status) + .withContext('/SOGo/so/public must not be accessible') + .toBe(404) + }) + + it("access to /SOGo/public", async function() { + const [{ status }] = await webdav_anon.options('/SOGo/public') + expect(status) + .withContext('/SOGo/public must not be accessible') + .toBe(404) + }) + + it("access to non-public resource", async function() { + const [{ status }] = await webdav_anon.options(`/SOGo/dav/${config.username}`) + expect(status) + .withContext('DAV non-public resources should request authentication') + .toBe(401) + }) + + it("access to public resource", async function() { + const [{ status }] = await webdav_anon.options('/SOGo/dav/public') + expect(status) + .withContext('DAV public resources must not request authentication') + .not.toBe(401) + expect(status) + .withContext('DAV public resources must be accessible') + .toBe(200) + }) +}) \ No newline at end of file diff --git a/Tests/spec/WebDAVSpec.js b/Tests/spec/WebDAVSpec.js new file mode 100644 index 000000000..6242d1dc5 --- /dev/null +++ b/Tests/spec/WebDAVSpec.js @@ -0,0 +1,110 @@ +import config from '../lib/config' +import WebDAV from '../lib/WebDAV' +import TestUtility from '../lib/utilities' + +describe('WebDAV', function() { + var webdav + var utility + + beforeEach(function() { + webdav = new WebDAV(config.username, config.password) + utility = new TestUtility(webdav) + }) + + it("property: 'principal-collection-set' on collection object", async function() { + const resource = `/SOGo/dav/${config.username}/` + const results = await webdav.principalCollectionSet(resource) + expect(results.length).toBe(1) + results.forEach(o => { + expect(o.ok).toBe(true) + expect(o.status).toBe(207) + expect(resource).toBe(o.href) + }) + }) + + it("property: 'principal-collection-set' on non-collection object", function() { + const resource = `/SOGo/dav/${config.username}/freebusy.ifb` + return webdav.principalCollectionSet(resource).then(function(results) { + expect(results.length).toBe(1) + results.forEach(o => { + expect(o.ok).toBe(true) + expect(o.status).toBe(207) + }) + }) + }) + + it("propfind: ensure various NSURL work-arounds", async function() { + const resultsNoSlash = await webdav.propfindURL(`/SOGo/dav/${config.username}`) + resultsNoSlash.forEach(o => { + // Expect no trailing slash nowhere + expect(o.href.slice(-1)).toMatch(/[^\/]$/) + }) + const resultsWithSlash = await webdav.propfindURL(`/SOGo/dav/${config.username}/`) + resultsWithSlash.forEach(o => { + // Expect a trailing slash for collections only + if (o.props.resourcetype.collection) { + expect(o.href.slice(-1)).toMatch(/\/$/) + } + else { + expect(o.href.slice(-1)).toMatch(/[^\/]$/) + } + }) + const resultsNoColl = await webdav.propfindURL(`/SOGo/dav/${config.username}/freebusy.ifb`) + resultsNoColl.forEach(o => { + // Expect no collection + expect(o.props.resourcetype.collection).toBeFalsy() + }) + }) + + // REPORT + it("principal-property-search", async function() { + const resource = `/SOGo/dav/${config.username}/Calendar` + const user = await utility.fetchUserInfo(config.username) + const results = await webdav.principalPropertySearch(resource) + expect(results.length).toBe(1) + results.forEach(o => { + expect(o.props.displayname).toBe(user.displayname) + }) + }) + + // http://tools.ietf.org/html/rfc3253.html#section-3.8 + it("expand-property", async function () { + const resource = `/SOGo/dav/${config.username}/` + const user = await utility.fetchUserInfo(config.username) + const properties = [ + { + _attributes: { + name: 'owner' + }, + property: { _attributes: { name: 'displayname' } } + }, + { + _attributes: { + name: 'principal-collection-set' + }, + property: { _attributes: { name: 'displayname' } } + } + ] + const outcomes = { + owner: { + href: resource, + displayname: user.displayname + }, + principalCollectionSet: { + href: '/SOGo/dav/', + displayname: 'SOGo' + } + } + const results = await webdav.expendProperty(resource, properties) + expect(results.length).toBe(1) + results.forEach(o => { + const { props = {} } = o + expect(o.status).toBe(207) + Object.keys(outcomes).forEach(p => { + const { response: { href, propstat: { prop: { displayname }} }} = props[p] + expect(href).toBe(outcomes[p].href) + expect(displayname).toBe(outcomes[p].displayname) + }) + }) + }) +}) \ No newline at end of file diff --git a/Tests/spec/WebDavSyncSpec.js b/Tests/spec/WebDavSyncSpec.js new file mode 100644 index 000000000..da83cb6ae --- /dev/null +++ b/Tests/spec/WebDavSyncSpec.js @@ -0,0 +1,48 @@ +import config from '../lib/config' +import WebDAV from '../lib/WebDAV' + +describe('webdav sync', function() { + const webdav = new WebDAV(config.username, config.password) + const webdav_su = new WebDAV(config.superuser, config.superuser_password) + const resource = `/SOGo/dav/${config.username}/Calendar/test-webdavsync/` + + beforeEach(async function() { + }) + + afterEach(async function() { + await webdav_su.deleteObject(resource) + }) + + it("webdav sync", async function() { + let results + + // missing tests: + // invalid tokens: negative, non-numeric, > current timestamp + // non-empty collections: token validity, status codes for added, + // modified and removed elements + + results = await webdav.makeCalendar(resource) + expect(results.length).toBe(1) + expect(results[0].status).toBe(201) + + // test queries: + // empty collection: + // without a token (query1) + // with a token (query2) + // (when done, non-empty collection: + // without a token (query3) + // with a token (query4)) + + results = await webdav.syncQuery(resource, null, [ 'getetag' ]) + expect(results.length).toBe(1) + expect(results[0].status).toBe(207) + // TODO: sync-token is not returned by the tsdav library -- grep raw + + // we make sure that any token is accepted when the collection is + // empty, but that the returned token differs + results = await webdav.syncQuery(resource, '1234', [ 'getetag' ]) + expect(results.length).toBe(1) + expect(results[0].status).toBe(207) + // TODO: sync-token is not returned by the tsdav library -- grep raw? + }) +}) \ No newline at end of file