From 5afb659dc07e73de228f54408110be71153821ca Mon Sep 17 00:00:00 2001 From: Francis Lachapelle Date: Tue, 9 Nov 2021 11:13:23 -0500 Subject: [PATCH] test: migration from Python to JavaScript --- Tests/lib/ManageSieve.js | 111 ++++++++++++++ Tests/lib/Preferences.js | 34 +++-- Tests/lib/WebDAV.js | 86 ++++++----- Tests/package.json | 7 +- Tests/spec/CalDAVPreventInvitationsSpec.js | 18 ++- Tests/spec/CardDAVSpec.js | 5 +- Tests/spec/SieveSpec.js | 165 +++++++++++++++++++++ Tests/spec/WebDavSyncSpec.js | 22 ++- 8 files changed, 372 insertions(+), 76 deletions(-) create mode 100644 Tests/lib/ManageSieve.js create mode 100644 Tests/spec/SieveSpec.js diff --git a/Tests/lib/ManageSieve.js b/Tests/lib/ManageSieve.js new file mode 100644 index 000000000..006fd8037 --- /dev/null +++ b/Tests/lib/ManageSieve.js @@ -0,0 +1,111 @@ +import config from '../lib/config' + +class ManageSieve { + constructor(login, authname, password) { + const Telnet = require('telnet-client') + + this.login = login + this.authname = authname + this.password = password + this.params = { + host: config.sieve_server, + port: config.sieve_port, + shellPrompt: /\bOK "/, + timeout: 1500 + } + this.connection = new Telnet() + this.sasl = [] + this.ready = false + } + + async connect() { + let response, parsedResponse + + if (!this.ready) { + await this.connection.connect(this.params) + response = await this.connection.send('CAPABILITY', { waitfor: /\b(OK|NO) "/ }) + // console.debug(`ManageSieve.connect => ${response}`) + parsedResponse = this.parseResponse(response) + if (!parsedResponse['OK']) { + throw new Error(`Connection failed: ${parsedResponse['NO']}`) + } + this.ready = true + if (parsedResponse['SASL']) { + this.sasl = parsedResponse['SASL'].split(/ /) + } + } + } + + async authenticate() { + let buff, base64, response, parsedResponse + + await this.connect() + + buff = Buffer.from(`${this.login}\0${this.authname}\0${this.password}`) + base64 = buff.toString('base64') + response = await this.connection.send(`AUTHENTICATE "PLAIN" {${base64.length}+}\n${base64}`, { waitfor: /\b(OK|NO) "/ }) + // console.debug(`ManageSieve.authenticate => ${response}`) + parsedResponse = this.parseResponse(response) + if (!parsedResponse['OK']) { + throw new Error(`Authentication failed: ${parsedResponse['NO']}`) + } + } + + async listScripts() { + let response, parsedResponse + + await this.connect() + + response = await this.connection.send(`LISTSCRIPTS`, { waitfor: /\b(OK|NO) "/ }) + parsedResponse = this.parseResponse(response) + // console.debug(`ManageSieve.listScripts => ${JSON.stringify(parsedResponse, undefined, 2)}`) + if (!parsedResponse['OK']) { + throw new Error(`List scripts failed: ${parsedResponse['NO']}`) + } + return parsedResponse + } + + async getScript(scriptname) { + let response, parsedResponse, script = null + + await this.connect() + + response = await this.connection.send(`GETSCRIPT "${scriptname}"`, { waitfor: /\b(OK|NO) "/ }) + // console.debug(`ManageSieve.getScript(${scriptname}) => |${response}|`) + const lengthMatch = response.match(/{([0-9]+)}\r?\n/) + if (lengthMatch) { + const scriptLength = lengthMatch[1] + script = response.substr(lengthMatch[0].length, scriptLength) + } + else + throw new Error(`Can't find length of Sieve script`) + + return script + } + + parseResponse(str) { + const re = new RegExp(/[^\s"]+|"([^"]*)"/gi) + let parsed = {} + for (let line of (str.split(/\r?\n/))) { + if (line.length) { + let rematch, key, value, i = 0 + while ((rematch = re.exec(line))) { + value = rematch[1] ? rematch[1] : rematch[0] + if (key && i > 0) + parsed[key] = value + else + key = value + i++ + } + if (key && i == 1) { + parsed[key] = null + } + } + } + // console.debug(`ManageSieve.parseResponse => ${JSON.stringify(parsed, undefined, 2)}`) + return parsed + } + +} + +export default ManageSieve \ No newline at end of file diff --git a/Tests/lib/Preferences.js b/Tests/lib/Preferences.js index 5849042a2..baeef5004 100644 --- a/Tests/lib/Preferences.js +++ b/Tests/lib/Preferences.js @@ -28,17 +28,21 @@ class Preferences { headers: {'Content-Type': 'application/json'}, body: JSON.stringify({userName: this.username, password: this.password}) }) - const values = response.headers.get('set-cookie').split(/, /) - let authCookies = [] - for (let v of values) { - let c = cookie.parse(v) - for (let authCookie of ['0xHIGHFLYxSOGo', 'XSRF-TOKEN']) { - if (Object.keys(c).includes('0xHIGHFLYxSOGo')) { - authCookies.push(cookie.serialize(authCookie, c[authCookie])) + if (response.status == 200) { + const values = response.headers.get('set-cookie').split(/, /) + let authCookies = [] + for (let v of values) { + let c = cookie.parse(v) + for (let authCookie of ['0xHIGHFLYxSOGo', 'XSRF-TOKEN']) { + if (Object.keys(c).includes('0xHIGHFLYxSOGo')) { + authCookies.push(cookie.serialize(authCookie, c[authCookie])) + } } } + this.cookie = authCookies.join('; ') } - this.cookie = authCookies.join('; ') + else + throw new Error(`Can't authenticate with username ${this.username} (HTTP status code ${response.status})`) } return this.cookie } @@ -137,15 +141,13 @@ class Preferences { if (!this.preferences) await this.loadPreferences() - let obj = this.findKey(this.preferences, preference) - if (obj == null) { - obj = this.preferences - for (let path of paths) { - if (typeof obj[path] == 'undefined') - obj[path] = {} - obj = obj[path] - } + let obj = this.preferences + for (let path of paths) { + if (typeof obj[path] == 'undefined') + obj[path] = {} + obj = obj[path] } + obj[preference] = value } diff --git a/Tests/lib/WebDAV.js b/Tests/lib/WebDAV.js index 0df685c18..332cd74d5 100644 --- a/Tests/lib/WebDAV.js +++ b/Tests/lib/WebDAV.js @@ -5,9 +5,10 @@ import { davRequest, deleteObject, + formatProps, getBasicAuthHeaders, + getDAVAttribute, propfind, - syncCollection, calendarMultiGet, createCalendarObject, @@ -15,8 +16,6 @@ import { createVCard } 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' @@ -132,10 +131,14 @@ class WebDAV { } makeCollection(resource) { - return makeCollection({ + return davRequest({ url: this.serverUrl + resource, - headers: this.headers - }); + init: { + method: 'MKCOL', + headers: this.headers, + namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV] + } + }) } propfindWebdav(resource, properties, namespace = DAVNamespace.DAV, headers = {}) { @@ -325,6 +328,42 @@ class WebDAV { }) } + // https://datatracker.ietf.org/doc/html/rfc6578#section-3.2 + syncQuery(resource, token = '', properties) { + const formattedProperties = properties.map((p) => { + return { [`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:${p}`]: '' } + }); + let xmlBody = convert.js2xml( + { + 'sync-collection': { + _attributes: getDAVAttribute([DAVNamespace.DAV]), + 'sync-level': 1, + 'sync-token': token, + prop: formattedProperties + } + }, + { + compact: true, + spaces: 2, + elementNameFn: (name) => { + // add namespace to all keys without namespace + if (!/^.+:.+/.test(name)) { + return `${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:${name}` + } + return name + } + } + ) + return fetch(this.serverUrl + resource, { + headers: { + 'Content-Type': 'application/xml; charset="utf-8"', + ...this.headers + }, + method: 'REPORT', + body: xmlBody + }) + } + // CalDAV operations makeCalendar(resource) { @@ -426,41 +465,6 @@ class WebDAV { }) } - syncQuery(resource, token = '', properties) { - const formattedProperties = properties.map((p) => { - return { [`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:${p}`]: '' } - }); - let xmlBody = convert.js2xml( - { - 'sync-collection': { - _attributes: getDAVAttribute([DAVNamespace.DAV]), - 'sync-level': 1, - 'sync-token': token, - prop: formattedProperties - } - }, - { - compact: true, - spaces: 2, - elementNameFn: (name) => { - // add namespace to all keys without namespace - if (!/^.+:.+/.test(name)) { - return `${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:${name}` - } - return name - } - } - ) - return fetch(this.serverUrl + resource, { - headers: { - 'Content-Type': 'application/xml; charset="utf-8"', - ...this.headers - }, - method: 'REPORT', - body: xmlBody - }) - } - propfindCaldav(resource, properties, depth = 0) { return this.propfindWebdav(resource, properties, DAVNamespace.CALDAV, { depth: new String(depth) }) } diff --git a/Tests/package.json b/Tests/package.json index 556063ff3..ec6becb80 100644 --- a/Tests/package.json +++ b/Tests/package.json @@ -6,14 +6,15 @@ "test": "jasmine" }, "dependencies": { - "@babel/core": "^7.15.8", - "@babel/preset-env": "^7.15.8", + "@babel/core": "^7.16.0", + "@babel/preset-env": "^7.16.0", "babel-cli": "^6.26.0", "cookie": "^0.4.1", "cross-fetch": "^3.1.4", "esm": "^3.2.25", "ical.js": "^1.4.0", "jasmine": "^3.10.0", - "tsdav": "^1.1.1" + "telnet-client": "^1.4.10", + "tsdav": "^1.1.5" } } diff --git a/Tests/spec/CalDAVPreventInvitationsSpec.js b/Tests/spec/CalDAVPreventInvitationsSpec.js index 421399ec8..fed79b117 100644 --- a/Tests/spec/CalDAVPreventInvitationsSpec.js +++ b/Tests/spec/CalDAVPreventInvitationsSpec.js @@ -82,14 +82,6 @@ describe('PreventInvitations', function() { beforeAll(async function() { prefs = new Preferences(config.attendee1_username, config.attendee1_password) - const calendarPrefs = prefs.get('Calendar') - if (!calendarPrefs.PreventInvitationsWhitelist) - calendarPrefs.PreventInvitationsWhitelist = {} - await prefs.set('PreventInvitationsWhitelist', {}) - if (!calendarPrefs.PreventInvitations) - calendarPrefs.PreventInvitations = 0 - await prefs.set('PreventInvitations', 0) - webdav = new WebDAV(config.username, config.password) webdav_su = new WebDAV(config.superuser, config.superuser_password) webdavAttendee1 = new WebDAV(config.attendee1, config.attendee1_password) @@ -114,6 +106,16 @@ describe('PreventInvitations', function() { icsList = [] }) + beforeEach(async function() { + const calendarPrefs = prefs.get('Calendar') + if (!calendarPrefs.PreventInvitationsWhitelist) + calendarPrefs.PreventInvitationsWhitelist = {} + await prefs.set('PreventInvitationsWhitelist', {}) + if (!calendarPrefs.PreventInvitations) + calendarPrefs.PreventInvitations = 0 + await prefs.set('PreventInvitations', 0) + }) + afterAll(async function() { await prefs.set('PreventInvitationsWhitelist', {}) await prefs.set('PreventInvitations', 0) diff --git a/Tests/spec/CardDAVSpec.js b/Tests/spec/CardDAVSpec.js index 01d37a2d0..f0aa342bf 100644 --- a/Tests/spec/CardDAVSpec.js +++ b/Tests/spec/CardDAVSpec.js @@ -5,9 +5,10 @@ import TestUtility from '../lib/utilities' import { DAVNamespace, DAVNamespaceShorthandMap, - davRequest + davRequest, + formatProps, + getDAVAttribute } from 'tsdav' -import { formatProps, getDAVAttribute } from 'tsdav/dist/util/requestHelpers'; const cards = { 'new.vcf': `BEGIN:VCARD diff --git a/Tests/spec/SieveSpec.js b/Tests/spec/SieveSpec.js new file mode 100644 index 000000000..50160f92c --- /dev/null +++ b/Tests/spec/SieveSpec.js @@ -0,0 +1,165 @@ +import config from '../lib/config' +import WebDAV from '../lib/WebDAV' +import TestUtility from '../lib/utilities' +import Preferences from '../lib/Preferences' +import ManageSieve from '../lib/ManageSieve' + +let prefs, webdav, utility, manageSieve, user + +describe('Sieve', function() { + + async function _getSogoSieveScript() { + const scripts = await manageSieve.listScripts() + + expect(Object.keys(scripts)) + .withContext(`sogo sieve script has been created`) + .toContain('sogo') + expect(scripts['sogo']) + .withContext(`sogo sieve script is active`) + .toMatch(/ACTIVE/i) + + const script = await manageSieve.getScript('sogo') + + return script + } + + async function _killFilters() { + // kill existing filters + await prefs.setOrCreate('SOGoSieveFilters', [], ['defaults']) + // vacation filters + await prefs.setOrCreate('autoReplyText', '', ['defaults', 'Vacation']) + await prefs.setOrCreate('customSubjectEnabled', 0, ['defaults', 'Vacation']) + await prefs.setOrCreate('customSubject', '', ['defaults', 'Vacation']) + await prefs.setOrCreate('autoReplyEmailAddresses', [], ['defaults', 'Vacation']) + await prefs.setOrCreate('daysBetweenResponse', 7, ['defaults', 'Vacation']) + await prefs.setOrCreate('ignoreLists', 0, ['defaults', 'Vacation']) + await prefs.setOrCreate('startDate', 0, ['defaults', 'Vacation']) + await prefs.setOrCreate('endDate', 0, ['defaults', 'Vacation']) + await prefs.setOrCreate('enabled', 0, ['defaults', 'Vacation']) + // forwarding filters + await prefs.setOrCreate('forwardAddress', [], ['defaults', 'Forward']) + await prefs.setOrCreate('keepCopy', 0, ['defaults', 'Forward']) + } + + beforeAll(async function() { + prefs = new Preferences(config.username, config.password) + webdav = new WebDAV(config.username, config.password) + utility = new TestUtility(webdav) + manageSieve = new ManageSieve(config.username, config.username, config.password) + user = await utility.fetchUserInfo(config.username) + + await manageSieve.authenticate() + }) + + beforeEach(async function() { + await _killFilters() + }) + + afterAll(async function() { + await _killFilters() + await prefs.save() + }) + + it('enable simple vacation script', async function() { + const vacationMsg = 'vacation test' + const daysInterval = 5 + const mailaddr = user.email + const sieveSimpleVacation = `require ["vacation"];\r\nvacation :days ${daysInterval} :addresses ["${mailaddr}"] text:\r\n${vacationMsg}\r\n.\r\n;\r\n` + let vacation + + vacation = await prefs.get('Vacation') + vacation.enabled = 1 + await prefs.setNoSave('autoReplyText', vacationMsg) + await prefs.setNoSave('daysBetweenResponse', daysInterval) + await prefs.setNoSave('autoReplyEmailAddresses', [user.email]) + await prefs.save() + + const createdScript = await _getSogoSieveScript() + expect(createdScript) + .withContext(`sogo Sieve script`) + .toBe(sieveSimpleVacation) + }) + + it('enable vacation script - ignore lists', async function() { + const vacationMsg = 'vacation test - ignore list' + const daysInterval = 3 + const mailaddr = user.email + const sieveVacationIgnoreLists = `require ["vacation"];\r\nif allof ( not exists ["list-help", "list-unsubscribe", "list-subscribe", "list-owner", "list-post", "list-archive", "list-id", "Mailing-List"], not header :comparator "i;ascii-casemap" :is "Precedence" ["list", "bulk", "junk"], not header :comparator "i;ascii-casemap" :matches "To" "Multiple recipients of*" ) { vacation :days ${daysInterval} :addresses ["${mailaddr}"] text:\r\n${vacationMsg}\r\n.\r\n;\r\n}\r\n` + let vacation + + vacation = await prefs.get('Vacation') + vacation.enabled = 1 + await prefs.setNoSave('autoReplyText', vacationMsg) + await prefs.setNoSave('daysBetweenResponse', daysInterval) + await prefs.setNoSave('autoReplyEmailAddresses', [user.email]) + await prefs.setNoSave('ignoreLists', 1) + await prefs.save() + + const createdScript = await _getSogoSieveScript() + expect(createdScript) + .withContext(`sogo Sieve script`) + .toBe(sieveVacationIgnoreLists) + }) + + it('enable simple forwarding', async function() { + const redirectMailaddr = 'nonexistent@inverse.com' + const sieveSimpleForward = `redirect "${redirectMailaddr}";\r\n` + let forward + + // Enabling Forward now is an 'enabled' setting in the subdict Forward + // We need to get that subdict first -- next save/set will also save this + forward = await prefs.get('Forward') + forward.enabled = 1 + await prefs.set('forwardAddress', [redirectMailaddr]) + + const createdScript = await _getSogoSieveScript() + expect(createdScript) + .withContext(`sogo Sieve script`) + .toBe(sieveSimpleForward) + }) + + it('enable email forwarding - keep a copy', async function() { + const redirectMailaddr = 'nonexistent@inverse.com' + const sieveForwardKeep = `redirect "${redirectMailaddr}";\r\nkeep;\r\n` + let forward + + // Enabling Forward now is an 'enabled' setting in the subdict Forward + // We need to get that subdict first -- next save/set will also save this + forward = await prefs.get('Forward') + forward.enabled = 1 + await prefs.setNoSave('forwardAddress', [redirectMailaddr]) + await prefs.setNoSave('keepCopy', 1) + await prefs.save() + + const createdScript = await _getSogoSieveScript() + expect(createdScript) + .withContext(`sogo Sieve script`) + .toBe(sieveForwardKeep) + }) + + it('add simple sieve filter', async function() { + const folderName = 'Sent' + const subject = 'add simple sieve filter' + const sieveFilter = `require ["fileinto"];\r\nif anyof (header :contains "subject" "${subject}") {\r\n fileinto "${folderName}";\r\n}\r\n` + + await prefs.set('SOGoSieveFilters', [{ + active: true, + actions: [{ + method: 'fileinto', + argument: 'Sent' + }], + rules: [{ + operator: 'contains', + field: 'subject', + value: subject + }], + match: 'any', + name: folderName + }]) + + const createdScript = await _getSogoSieveScript() + expect(createdScript) + .withContext(`sogo Sieve script`) + .toBe(sieveFilter) + }) +}) \ No newline at end of file diff --git a/Tests/spec/WebDavSyncSpec.js b/Tests/spec/WebDavSyncSpec.js index d181c5937..34f03b727 100644 --- a/Tests/spec/WebDavSyncSpec.js +++ b/Tests/spec/WebDavSyncSpec.js @@ -12,7 +12,7 @@ describe('webdav sync', function() { await webdav_su.deleteObject(resource) }) - it("webdav sync", async function() { + it('webdav sync', async function() { const nsShort = DAVNamespaceShorthandMap[DAVNamespace.DAV].toUpperCase() let response, xml, token @@ -23,7 +23,9 @@ describe('webdav sync', function() { response = await webdav.makeCalendar(resource) expect(response.length).toBe(1) - expect(response[0].status).toBe(201) + expect(response[0].status) + .withContext(`HTTP status code when creating a Calendar`) + .toBe(201) // test queries: // empty collection: @@ -36,15 +38,23 @@ describe('webdav sync', function() { response = await webdav.syncQuery(resource, null, [ 'getetag' ]) xml = await response.text(); ({ [`${nsShort}:multistatus`]: { [`${nsShort}:sync-token`]: { _text: token } } } = convert.xml2js(xml, {compact: true, nativeType: true})) - expect(response.status).toBe(207) - expect(token).toBeGreaterThanOrEqual(0) + expect(response.status) + .withContext(`HTTP status code when performing sync-query without a token`) + .toBe(207) + expect(token) + .withContext(`Sync query returns valid token`) + .toBeGreaterThanOrEqual(0) // we make sure that any token is accepted when the collection is // empty, but that the returned token differs response = await webdav.syncQuery(resource, '1234', [ 'getetag' ]) xml = await response.text(); ({ [`${nsShort}:multistatus`]: { [`${nsShort}:sync-token`]: { _text: token } } } = convert.xml2js(xml, {compact: true, nativeType: true})) - expect(response.status).toBe(207) - expect(token).toBeGreaterThanOrEqual(0) + expect(response.status) + .withContext(`HTTP status code when performing sync-query with a token`) + .toBe(207) + expect(token) + .withContext(`Sync query returns valid token`) + .toBeGreaterThanOrEqual(0) }) }) \ No newline at end of file