test: migration from Python to JavaScript
parent
946419f434
commit
54dff23682
|
@ -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
|
|
@ -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 <sogo1@example.com>"
|
||||||
|
},
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
|
@ -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 })
|
||||||
|
})
|
||||||
|
})
|
|
@ -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({})
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
|
@ -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, '<default>', {})
|
||||||
|
}
|
||||||
|
|
||||||
|
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, '<default>', { c: true })
|
||||||
|
|
||||||
|
results = await webdav_subscriber.propfindURL(parentColl)
|
||||||
|
expect(results.length)
|
||||||
|
.withContext('Profind returns 4 href when <default> 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, '<default>', {})
|
||||||
|
|
||||||
|
// 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 <default> 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)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
|
@ -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)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -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?
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue