test: migration from Python to JavaScript

pull/301/head
Francis Lachapelle 2021-08-26 17:33:50 -04:00
parent b81c4eac1c
commit 5622eda04b
5 changed files with 327 additions and 18 deletions

View File

@ -16,9 +16,15 @@ import {
} from 'tsdav'
import { formatProps, getDAVAttribute } from 'tsdav/dist/util/requestHelpers';
import { makeCollection } from 'tsdav/dist/collection';
import convert from 'xml-js'
import { fetch } from 'cross-fetch'
import config from './config'
const DAVInverse = 'urn:inverse:params:xml:ns:inverse-dav'
const DAVInverseShort = 'i'
export { DAVInverse, DAVInverseShort }
class WebDAV {
constructor(un, pw) {
this.serverUrl = `http://${config.hostname}:${config.port}`
@ -83,21 +89,25 @@ class WebDAV {
})
}
propfindWebdav(resource, properties, depth = 0) {
propfindWebdav(resource, properties, namespace = DAVNamespace.DAV, headers = {}) {
const nsShort = DAVNamespaceShorthandMap[namespace] || DAVInverseShort
const formattedProperties = properties.map(p => {
return { [`i:${p}`]: '' }
return { [`${nsShort}:${p}`]: '' }
})
if (typeof headers.depth == 'undefined') {
headers.depth = new String(0)
}
return davRequest({
url: this.serverUrl + resource,
init: {
method: 'PROPFIND',
headers: { ...this.headers, depth: new String(depth) },
headers: { ...this.headers, ...headers },
namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV],
body: {
propfind: {
_attributes: {
...getDAVAttribute([DAVNamespace.DAV]),
'xmlns:i': 'urn:inverse:params:xml:ns:inverse-dav'
[`xmlns:${nsShort}`]: namespace
},
prop: formattedProperties
}
@ -106,6 +116,43 @@ class WebDAV {
})
}
propfindWebdavRaw(resource, properties, headers = {}) {
const namespace = DAVNamespaceShorthandMap[DAVNamespace.DAV]
const formattedProperties = properties.map(prop => {
return { [`${namespace}:${prop}`]: '' }
})
let xmlBody = convert.js2xml(
{
propfind: {
_attributes: getDAVAttribute([DAVNamespace.DAV]),
prop: formattedProperties
}
},
{
compact: true,
spaces: 2,
elementNameFn: (name) => {
// add namespace to all keys without namespace
if (!/^.+:.+/.test(name)) {
return `${namespace}:${name}`;
}
return name;
},
}
)
return fetch(this.serverUrl + resource, {
headers: {
'Content-Type': 'application/xml; charset="utf-8"',
...headers,
...this.headers
},
method: 'PROPFIND',
body: xmlBody
})
}
propfindEvent(resource) {
return propfind({
url: this.serverUrl + resource,
@ -258,7 +305,7 @@ class WebDAV {
})
}
proppatchCaldav(resource, properties) {
proppatchCaldav(resource, properties, headers = {}) {
const formattedProperties = Object.keys(properties).map(p => {
return { name: p, namespace: DAVNamespace.CALDAV, value: properties[p] }
})
@ -266,7 +313,7 @@ class WebDAV {
url: this.serverUrl + resource,
init: {
method: 'PROPPATCH',
headers: this.headers,
headers: { ...this.headers, ...headers },
namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV],
body: {
propertyupdate: {
@ -287,27 +334,31 @@ class WebDAV {
})
}
proppatchWebdav(resource, properties, depth = 0) {
proppatchWebdav(resource, properties, namespace = DAVNamespace.DAV, headers = {}) {
const nsShort = DAVNamespaceShorthandMap[namespace] || DAVInverseShort
const formattedProperties = Object.keys(properties).map(p => {
if (typeof properties[p] == 'object') {
return { [`i:${p}`]: properties[p].map(pp => {
if (Array.isArray(properties[p])) {
return { [`${nsShort}:${p}`]: properties[p].map(pp => {
const [ key ] = Object.keys(pp)
return { [`i:${key}`]: pp[key] || '' }
return { [`${nsShort}:${key}`]: pp[key] || '' }
})}
}
return { [`i:${p}`]: properties[p] || '' }
return { [`${nsShort}:${p}`]: properties[p] || '' }
})
if (typeof headers.depth == 'undefined') {
headers.depth = new String(0)
}
return davRequest({
url: this.serverUrl + resource,
init: {
method: 'PROPPATCH',
headers: { ...this.headers, depth: new String(depth) },
headers: { ...this.headers, ...headers },
namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV],
body: {
propertyupdate: {
_attributes: {
...getDAVAttribute([DAVNamespace.DAV]),
'xmlns:i': 'urn:inverse:params:xml:ns:inverse-dav'
[`xmlns:${nsShort}`]: namespace
},
set: {
prop: formattedProperties

View File

@ -114,6 +114,35 @@ class TestUtility {
return this.setupRights(resource, username, sogoRights)
}
_subscriptionOperation(resource, subscribers, operation) {
return davRequest({
url: `${this.webdav.serverUrl}${resource}`,
init: {
method: 'POST',
headers: {
'Content-Type': 'application/xml; charset="utf-8"',
...this.webdav.headers
},
body: {
[operation]: {
_attributes: {
xmlns: 'urn:inverse:params:xml:ns:inverse-dav',
users: subscribers.join(',')
}
}
}
}
})
}
subscribe(resource, subscribers) {
return this._subscriptionOperation(resource, subscribers, 'subscribe')
}
unsubscribe(resource, subscribers) {
return this._subscriptionOperation(resource, subscribers, 'unsubscribe')
}
versitDict(cal) {
const comp = ICAL.Component.fromString(cal)
let props = {}

View File

@ -0,0 +1,229 @@
import { DAVNamespace } from 'tsdav'
import config from '../lib/config'
import { default as WebDAV, DAVInverse } from '../lib/WebDAV'
import TestUtility from '../lib/utilities'
/**
* NOTE
*
* To pass the following tests, make sure "username" and "subscriber_username" don't have
* additional calendars.
*/
describe('Apple iCal', function() {
const webdav = new WebDAV(config.username, config.password)
const webdav_su = new WebDAV(config.superuser, config.superuser_password)
const utility = new TestUtility(webdav_su)
const iCal4UserAgent = 'DAVKit/4.0.1 (730); CalendarStore/4.0.1 (973); iCal/4.0.1 (1374); Mac OS X/10.6.2 (10C540)'
const _setMemberSet = async function(owner, members, perm) {
const resource = `/SOGo/dav/${owner}/calendar-proxy-${perm}`
const headers = { 'User-Agent': iCal4UserAgent }
const membersHref = members.map(m => {
return `/SOGo/dav/${m}`
})
const properties = {
'group-member-set': membersHref.length ? { href: membersHref } : ''
}
const results = await webdav_su.proppatchWebdav(resource, properties, DAVNamespace.DAV, headers)
expect(results.length)
.withContext(`Number of responses from PROPPATCH on group-member-set for ${owner}`)
.toBe(1)
expect(results[0].status)
.withContext(`HTTP status code when setting group member on calendar-proxy-${perm} for ${owner}`)
.toBe(207)
}
const _getMembership = async function(user) {
const resource = `/SOGo/dav/${user}/`
const headers = { 'User-Agent': iCal4UserAgent }
const results = await webdav_su.propfindWebdav(resource, ['group-membership'], DAVNamespace.DAV, headers)
expect(results.length)
.withContext(`Number of responses from PROPFIND on group-membership for ${user}`)
.toBe(1)
expect(results[0].status)
.withContext(`HTTP status code when getting group membership for ${user}`)
.toBe(207)
const { props: { groupMembership: { href = [] } = {} } = {} } = results[0]
return Array.isArray(href) ? href : [href] // always return an array
}
const _getProxyFor = async function(user, perm) {
const resource = `/SOGo/dav/${user}/`
const headers = { 'User-Agent': iCal4UserAgent }
const results = await webdav_su.propfindWebdav(resource, [`calendar-proxy-${perm}-for`], DAVNamespace.CALENDAR_SERVER, headers)
expect(results.length)
.withContext(`Number of responses from PROPFIND on group-membership for ${user}`)
.toBe(1)
expect(results[0].status)
.withContext(`HTTP status code when getting group membership for ${user}`)
.toBe(207)
const { props = {} } = results[0]
const users = props[`calendarProxy${perm.replace(/^\w/, (c) => c.toUpperCase())}For`]
const { href = [] } = users
return Array.isArray(href) ? href : [href] // always return an array
}
const _testMapping = async function(perm, resource, rights) {
const results = await utility.setupCalendarRights(resource, config.subscriber_username, rights)
expect(results.length).toBe(1)
expect(results[0].status).toBe(204)
const membership = await _getMembership(config.subscriber_username)
expect(membership)
.withContext(`${perm.replace(/^\w/, (c) => c.toUpperCase())} access to /SOGo/dav/${config.subscriber_username}/`)
.toContain(`/SOGo/dav/${config.username}/calendar-proxy-${perm}/`)
const proxyFor = await _getProxyFor(config.subscriber_username, perm)
expect(proxyFor)
.withContext(`Proxy ${perm} on /SOGo/dav/${config.subscriber_username}/`)
.toContain(`/SOGo/dav/${config.username}/`)
}
// iCalTest
it(`principal-collection-set: 'DAV' header must be returned with iCal 4`, async function() {
const resource = `/SOGo/dav/${config.username}/`
const expectedDAVClasses = ['1', '2', 'access-control', 'calendar-access', 'calendar-schedule', 'calendar-auto-schedule', 'calendar-proxy']
let headers, response, davClasses, davClass
headers = { Depth: new String(0) }
// NOT iCal4
response = await webdav.propfindWebdavRaw(resource, ['principal-collection-set'], headers)
expect(response.status)
.withContext(`HTTP status code when fetching principal-collection-set`)
.toBe(207)
expect(response.headers.get('dav'))
.withContext(`DAV header must NOT be returned when user-agent is NOT iCal 4`)
.toBeFalsy()
// iCal4
headers['User-Agent'] = iCal4UserAgent
response = await webdav.propfindWebdavRaw(resource, ['principal-collection-set'], headers)
expect(response.status)
.withContext(`HTTP status code when fetching principal-collection-set`)
.toBe(207)
expect(response.headers.get('dav'))
.withContext(`DAV header must be returned when user-agent is iCal 4`)
.toBeTruthy()
davClasses = response.headers.get('dav').split(', ')
for (davClass of expectedDAVClasses) {
expect(davClasses.includes(davClass))
.withContext(`DAV header includes class ${davClass}`)
.toBeTrue()
}
})
it(`calendar-proxy as used from iCal`, async function() {
let membership, perm, users, proxyFor
await _setMemberSet(config.username, [], 'read')
await _setMemberSet(config.username, [], 'write')
await _setMemberSet(config.subscriber_username, [], 'read')
await _setMemberSet(config.subscriber_username, [], 'write')
membership = await _getMembership(config.username)
expect(membership.length)
.toBe(0)
membership = await _getMembership(config.subscriber_username)
expect(membership.length)
.toBe(0)
users = await _getProxyFor(config.username, 'read')
expect(users.length)
.withContext(`Proxy read for /SOGo/dav/${config.username}`)
.toBe(0)
users = await _getProxyFor(config.username, 'write')
expect(users.length)
.withContext(`Proxy write for /SOGo/dav/${config.username}`)
.toBe(0)
users = await _getProxyFor(config.subscriber_username, 'read')
expect(users.length)
.withContext(`Proxy read for /SOGo/dav/${config.subscriber_username}`)
.toBe(0)
users = await _getProxyFor(config.subscriber_username, 'write')
expect(users.length)
.withContext(`Proxy write for /SOGo/dav/${config.subscriber_username}`)
.toBe(0)
for (perm of ['read', 'write']) {
for (users of [[config.username, config.subscriber_username], [config.subscriber_username, config.username]]) {
const [owner, member] = users
await _setMemberSet(owner, [member], perm)
let [ membership ] = await _getMembership(member)
expect(membership)
.toBe(`/SOGo/dav/${owner}/calendar-proxy-${perm}/`)
proxyFor = await _getProxyFor(member, perm)
expect(proxyFor.length).toBe(1)
expect(proxyFor).toContain(`/SOGo/dav/${owner}/`)
}
}
})
it('calendar-proxy as used from SOGo', async function() {
const personalResource = `/SOGo/dav/${config.username}/Calendar/personal/`
const otherResource = `/SOGo/dav/${config.username}/Calendar/test-calendar-proxy2/`
let response, membership
// Remove rights on personal calendar
await utility.setupRights(personalResource, config.subscriber_username);
[response] = await utility.subscribe(personalResource, [config.subscriber_username])
expect(response.status)
.toBe(200)
await webdav_su.deleteObject(otherResource)
await webdav_su.makeCalendar(otherResource)
await utility.setupRights(otherResource, config.subscriber_username);
[response] = await utility.subscribe(otherResource, [config.subscriber_username])
expect(response.status)
.toBe(200)
// we test the rights mapping
// write: write on 'personal', none on 'test-calendar-proxy2'
await _testMapping('write', personalResource, { c: true, d: false, pu: 'v' })
await _testMapping('write', personalResource, { c: false, d: true, pu: 'v' })
await _testMapping('write', personalResource, { c: false, d: false, pu: 'm' })
await _testMapping('write', personalResource, { c: false, d: false, pu: 'r' })
// read: read on 'personal', none on 'test-calendar-proxy2'
await _testMapping('read', personalResource, { c: false, d: false, pu: 'd' })
await _testMapping('read', personalResource, { c: false, d: false, pu: 'v' })
// write: read on 'personal', write on 'test-calendar-proxy2'
await _testMapping('write', otherResource, { c: false, d: false, pu: 'r' });
// we test the unsubscription
// unsubscribed from personal, subscribed to 'test-calendar-proxy2'
[response] = await utility.unsubscribe(personalResource, [config.subscriber_username])
expect(response.status)
.toBe(200)
membership = await _getMembership(config.subscriber_username)
expect(membership)
.withContext(`Proxy write to /SOGo/dav/${config.subscriber_username}/`)
.toContain(`/SOGo/dav/${config.username}/calendar-proxy-write/`);
// unsubscribed from personal, unsubscribed from 'test-calendar-proxy2'
[response] = await utility.unsubscribe(otherResource, [config.subscriber_username])
expect(response.status)
.toBe(200)
membership = await _getMembership(config.subscriber_username)
expect(membership.length)
.withContext(`No more access to /SOGo/dav/${config.subscriber_username}/`)
.toBe(0)
await webdav_su.deleteObject(otherResource)
})
})

View File

@ -1,5 +1,5 @@
import config from '../lib/config'
import WebDAV from '../lib/WebDAV'
import { default as WebDAV, DAVInverse } from '../lib/WebDAV'
describe('calendar classification', function() {
const webdav = new WebDAV(config.username, config.password)
@ -8,7 +8,7 @@ describe('calendar classification', function() {
const resource = `/SOGo/dav/${config.username}/Calendar/`
const properties = { [`${component}-default-classification`]: classification }
const results = await webdav.proppatchWebdav(resource, properties)
const results = await webdav.proppatchWebdav(resource, properties, DAVInverse)
expect(results.length)
.withContext(`Set ${component} classification to ${classification}`)
.toBe(1)

View File

@ -1,5 +1,5 @@
import config from '../lib/config'
import WebDAV from '../lib/WebDAV'
import { default as WebDAV, DAVInverse } from '../lib/WebDAV'
describe('contacts categories', function() {
const webdav = new WebDAV(config.username, config.password)
@ -11,7 +11,7 @@ describe('contacts categories', function() {
})
const properties = { 'contacts-categories': elements.length ? elements : '' }
const results = await webdav.proppatchWebdav(resource, properties)
const results = await webdav.proppatchWebdav(resource, properties, DAVInverse)
expect(results.length)
.withContext(`Set contacts categories to ${categories.join(', ')}`)
.toBe(1)
@ -23,7 +23,7 @@ describe('contacts categories', function() {
const resource = `/SOGo/dav/${config.username}/Contacts/`
const properties = ['contacts-categories']
const results = await webdav.propfindWebdav(resource, properties)
const results = await webdav.propfindWebdav(resource, properties, DAVInverse)
expect(results.length)
.toBe(1)
const { props: { contactsCategories: { category } = {} } = {} } = results[0]