From 5622eda04b5cbe8a18d8693d81caf8061037c945 Mon Sep 17 00:00:00 2001 From: Francis Lachapelle Date: Thu, 26 Aug 2021 17:33:50 -0400 Subject: [PATCH] test: migration from Python to JavaScript --- Tests/lib/WebDAV.js | 77 +++++-- Tests/lib/utilities.js | 29 +++ Tests/spec/DAVCalendarAppleiCalSpec.js | 229 ++++++++++++++++++++ Tests/spec/DAVCalendarClassificationSpec.js | 4 +- Tests/spec/DAVContactsCategoriesSpec.js | 6 +- 5 files changed, 327 insertions(+), 18 deletions(-) create mode 100644 Tests/spec/DAVCalendarAppleiCalSpec.js diff --git a/Tests/lib/WebDAV.js b/Tests/lib/WebDAV.js index 82c6d0dde8..e71fe41c2c 100644 --- a/Tests/lib/WebDAV.js +++ b/Tests/lib/WebDAV.js @@ -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 diff --git a/Tests/lib/utilities.js b/Tests/lib/utilities.js index 01f36659b8..9c9b938bd3 100644 --- a/Tests/lib/utilities.js +++ b/Tests/lib/utilities.js @@ -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 = {} diff --git a/Tests/spec/DAVCalendarAppleiCalSpec.js b/Tests/spec/DAVCalendarAppleiCalSpec.js new file mode 100644 index 0000000000..46b28fa1b0 --- /dev/null +++ b/Tests/spec/DAVCalendarAppleiCalSpec.js @@ -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) + }) +}) \ No newline at end of file diff --git a/Tests/spec/DAVCalendarClassificationSpec.js b/Tests/spec/DAVCalendarClassificationSpec.js index 8c81f58a4d..ca72dfc195 100644 --- a/Tests/spec/DAVCalendarClassificationSpec.js +++ b/Tests/spec/DAVCalendarClassificationSpec.js @@ -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) diff --git a/Tests/spec/DAVContactsCategoriesSpec.js b/Tests/spec/DAVContactsCategoriesSpec.js index e420e3eabc..d21b2c94a3 100644 --- a/Tests/spec/DAVContactsCategoriesSpec.js +++ b/Tests/spec/DAVContactsCategoriesSpec.js @@ -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]