@open-xchange/appsuite-codeceptjs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.defaults +47 -0
- package/README.md +40 -0
- package/chai.d.ts +5 -0
- package/customRerun.js +135 -0
- package/global.d.ts +5 -0
- package/index.js +187 -0
- package/package.json +39 -0
- package/src/actor.js +174 -0
- package/src/appsuiteHttpClient.js +155 -0
- package/src/chai.d.ts +6 -0
- package/src/chai.js +58 -0
- package/src/contexts/contexts.js +172 -0
- package/src/contexts/reseller.js +248 -0
- package/src/contexts.js +29 -0
- package/src/event.js +54 -0
- package/src/helper.js +817 -0
- package/src/pageobjects/calendar.js +226 -0
- package/src/pageobjects/contacts.js +148 -0
- package/src/pageobjects/drive.js +96 -0
- package/src/pageobjects/fragments/contact-autocomplete.js +45 -0
- package/src/pageobjects/fragments/contact-picker.js +50 -0
- package/src/pageobjects/fragments/dialogs.js +41 -0
- package/src/pageobjects/fragments/search.js +54 -0
- package/src/pageobjects/fragments/settings-mailfilter.js +90 -0
- package/src/pageobjects/fragments/settings.js +71 -0
- package/src/pageobjects/fragments/tinymce.js +41 -0
- package/src/pageobjects/fragments/topbar.js +43 -0
- package/src/pageobjects/fragments/viewer.js +67 -0
- package/src/pageobjects/mail.js +67 -0
- package/src/pageobjects/mobile/mobileCalendar.js +41 -0
- package/src/pageobjects/mobile/mobileContacts.js +40 -0
- package/src/pageobjects/mobile/mobileMail.js +51 -0
- package/src/pageobjects/tasks.js +58 -0
- package/src/plugins/emptyModule/index.js +21 -0
- package/src/plugins/settingsInit/index.js +35 -0
- package/src/plugins/testmetrics/index.js +135 -0
- package/src/soap/services/context.js +147 -0
- package/src/soap/services/oxaas.js +36 -0
- package/src/soap/services/resellerContext.js +65 -0
- package/src/soap/services/resellerUser.js +100 -0
- package/src/soap/services/user.js +114 -0
- package/src/soap/services/util.js +39 -0
- package/src/soap/soap.js +172 -0
- package/src/users/reseller.js +233 -0
- package/src/users/users.js +183 -0
- package/src/users.js +29 -0
- package/src/util.js +104 -0
- package/steps.d.ts +16 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Copyright (c) Open-Xchange GmbH, Germany <info@open-xchange.com>
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
*
|
|
5
|
+
* This code is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
* (at your option) any later version.
|
|
9
|
+
*
|
|
10
|
+
* This program is distributed in the hope that it will be useful,
|
|
11
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
* GNU Affero General Public License for more details.
|
|
14
|
+
*
|
|
15
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
* along with OX App Suite. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
|
17
|
+
*
|
|
18
|
+
* Any use of the work other than as authorized under this license or copyright law is prohibited.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const codecept = require('codeceptjs')
|
|
22
|
+
const querystring = require('node:querystring')
|
|
23
|
+
|
|
24
|
+
const pRetry = import('p-retry').then(module => module.default)
|
|
25
|
+
|
|
26
|
+
const cache = {}
|
|
27
|
+
const baseURL = codecept.config.get().helpers.Playwright.url.replace(/\/$/, '')
|
|
28
|
+
|
|
29
|
+
async function fetchWithRetry (url, options) {
|
|
30
|
+
return (await pRetry)(async () => {
|
|
31
|
+
const res = await fetch(url, options)
|
|
32
|
+
let data
|
|
33
|
+
if (res.headers.get('content-type')?.includes('application/json')) {
|
|
34
|
+
data = await res.json()
|
|
35
|
+
} else {
|
|
36
|
+
data = await res.text()
|
|
37
|
+
}
|
|
38
|
+
if (data.error === 'A mail account with the given E-Mail address already exists.') {
|
|
39
|
+
console.log('A mail account with the given E-Mail address already exists.')
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
42
|
+
if (data.error) throw new Error(`HTTP Request Error: ${JSON.stringify(data)}`)
|
|
43
|
+
|
|
44
|
+
return { data, res }
|
|
45
|
+
}, {
|
|
46
|
+
retries: 4,
|
|
47
|
+
onFailedAttempt: error => {
|
|
48
|
+
console.error(`Attempt ${error.attemptNumber} failed. There are ${error.retriesLeft} retries left.`, error)
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createHttpClient (options) {
|
|
54
|
+
options = options || {}
|
|
55
|
+
let user = options.user || codecept.container.support('users')[0]
|
|
56
|
+
if (user.toJSON) user = user.toJSON()
|
|
57
|
+
|
|
58
|
+
async function request (url, options = {}) {
|
|
59
|
+
if (user) {
|
|
60
|
+
if (!cache[user.name]) await login(user)
|
|
61
|
+
options.headers = { Cookie: cache[user.name].cookies, ...options.headers }
|
|
62
|
+
if (!options.params) options.params = {}
|
|
63
|
+
options.params.session = cache[user.name].session
|
|
64
|
+
}
|
|
65
|
+
if (options.params) {
|
|
66
|
+
const urlObj = new URL(baseURL + url)
|
|
67
|
+
urlObj.search = new URLSearchParams(options.params).toString()
|
|
68
|
+
url = urlObj.toString()
|
|
69
|
+
} else {
|
|
70
|
+
url = baseURL + url
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return fetchWithRetry(url, options)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
get (url, options = {}) {
|
|
78
|
+
return request(url, { ...options, method: 'GET' })
|
|
79
|
+
},
|
|
80
|
+
post (url, body, options = {}) {
|
|
81
|
+
options = { ...options, method: 'POST', headers: { ...options.headers } }
|
|
82
|
+
if (body instanceof FormData) {
|
|
83
|
+
options.body = body
|
|
84
|
+
} else {
|
|
85
|
+
// Unify content-type header
|
|
86
|
+
if (options.headers['content-type']) {
|
|
87
|
+
options.headers['Content-Type'] = options.headers['content-type']
|
|
88
|
+
delete options.headers['content-type']
|
|
89
|
+
}
|
|
90
|
+
// Set default content-type header
|
|
91
|
+
if (!options.headers['Content-Type']) options.headers['Content-Type'] = 'application/json'
|
|
92
|
+
options.body = JSON.stringify(body)
|
|
93
|
+
}
|
|
94
|
+
return request(url, options)
|
|
95
|
+
},
|
|
96
|
+
put (url, body, options = {}) {
|
|
97
|
+
options = { ...options, method: 'PUT', headers: { 'Content-Type': 'application/json', ...options.headers } }
|
|
98
|
+
if (body) options.body = JSON.stringify(body)
|
|
99
|
+
return request(url, options)
|
|
100
|
+
},
|
|
101
|
+
delete (url, options = {}) {
|
|
102
|
+
return request(url, { ...options, method: 'DELETE' })
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Logs in a user and returns the session data.
|
|
109
|
+
*
|
|
110
|
+
* @param {object} user - The user object containing the name and password.
|
|
111
|
+
* @returns {Promise<object>} - A promise that resolves to the session data.
|
|
112
|
+
* @throws {Error} - If there is an error during the login process.
|
|
113
|
+
*/
|
|
114
|
+
async function login (user) {
|
|
115
|
+
if (cache[user.name]) {
|
|
116
|
+
const { session, cookies } = await cache[user.name]
|
|
117
|
+
try {
|
|
118
|
+
if ((await fetchWithRetry(baseURL + '/api/system', {
|
|
119
|
+
headers: { Cookie: cookies },
|
|
120
|
+
params: {
|
|
121
|
+
action: 'ping',
|
|
122
|
+
timestamp: (new Date()).getTime(),
|
|
123
|
+
session
|
|
124
|
+
}
|
|
125
|
+
})).data.data === true) {
|
|
126
|
+
return cache[user.name]
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
delete cache[user.name]
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const { data, res } = await fetchWithRetry(baseURL + '/api/login', {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: {
|
|
136
|
+
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'
|
|
137
|
+
},
|
|
138
|
+
body: querystring.stringify(Object.assign({
|
|
139
|
+
action: 'login',
|
|
140
|
+
client: 'open-xchange-appsuite'
|
|
141
|
+
}, user.login || {
|
|
142
|
+
name: `${user.name}${user.context.id ? '@' + user.context.id : ''}`,
|
|
143
|
+
password: user.password
|
|
144
|
+
}))
|
|
145
|
+
})
|
|
146
|
+
cache[user.name] = {
|
|
147
|
+
...data,
|
|
148
|
+
cookies: res.headers.getSetCookie().map(item => item.split(';')[0]).join(';')
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = {
|
|
153
|
+
createHttpClient,
|
|
154
|
+
login
|
|
155
|
+
}
|
package/src/chai.d.ts
ADDED
package/src/chai.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Copyright (c) Open-Xchange GmbH, Germany <info@open-xchange.com>
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
*
|
|
5
|
+
* This code is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
* (at your option) any later version.
|
|
9
|
+
*
|
|
10
|
+
* This program is distributed in the hope that it will be useful,
|
|
11
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
* GNU Affero General Public License for more details.
|
|
14
|
+
*
|
|
15
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
* along with OX App Suite. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
|
17
|
+
*
|
|
18
|
+
* Any use of the work other than as authorized under this license or copyright law is prohibited.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import('chai').then(chai => {
|
|
22
|
+
const chaiSubset = require('chai-subset')
|
|
23
|
+
chai.use(chaiSubset)
|
|
24
|
+
|
|
25
|
+
chai.util.addProperty(chai.Assertion.prototype, 'accessible', function () {
|
|
26
|
+
const problems = ['\n', 'Accessibility Violations (' + this._obj.violations.length + ')', '---']
|
|
27
|
+
const pad = '\n '
|
|
28
|
+
if (this._obj.violations.length) {
|
|
29
|
+
for (const violation of this._obj.violations) {
|
|
30
|
+
problems.push(pad + '[' + violation.impact.toUpperCase() + '] ' + violation.help + ' (ID: ' + violation.id + ')\n')
|
|
31
|
+
for (const node of violation.nodes) {
|
|
32
|
+
problems.push(node.failureSummary.split('\n').join(pad))
|
|
33
|
+
problems.push(' ' + node.target + ' => ' + node.html)
|
|
34
|
+
const relatedNodes = []
|
|
35
|
+
for (const combinedNodes of [node.all, node.any, node.none]) {
|
|
36
|
+
if (combinedNodes.length > 0) {
|
|
37
|
+
for (const any of combinedNodes) {
|
|
38
|
+
for (const relatedNode of any.relatedNodes) {
|
|
39
|
+
relatedNodes.push(' ' + relatedNode.target + ' => ' + relatedNode.html)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (relatedNodes.length > 0) problems.push(relatedNodes.join(pad))
|
|
45
|
+
}
|
|
46
|
+
problems.push(pad + '---\n')
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
this.assert(
|
|
50
|
+
this._obj.violations.length === 0,
|
|
51
|
+
`expected to have no violations:\n ${problems.join(pad)}`,
|
|
52
|
+
'expected to have violations'
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
globalThis.expect = chai.expect
|
|
57
|
+
globalThis.assert = chai.assert
|
|
58
|
+
})
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Copyright (c) Open-Xchange GmbH, Germany <info@open-xchange.com>
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
*
|
|
5
|
+
* This code is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
* (at your option) any later version.
|
|
9
|
+
*
|
|
10
|
+
* This program is distributed in the hope that it will be useful,
|
|
11
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
* GNU Affero General Public License for more details.
|
|
14
|
+
*
|
|
15
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
* along with OX App Suite. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
|
17
|
+
*
|
|
18
|
+
* Any use of the work other than as authorized under this license or copyright law is prohibited.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const created = []
|
|
22
|
+
const users = require('../users/users')()
|
|
23
|
+
const util = require('../util')
|
|
24
|
+
const event = require('../event')
|
|
25
|
+
const contextService = require('../soap/services/context')
|
|
26
|
+
const utilService = require('../soap/services/util')
|
|
27
|
+
|
|
28
|
+
class Context {
|
|
29
|
+
constructor ({ ctxdata, admin, auth }) {
|
|
30
|
+
this.id = ctxdata.id
|
|
31
|
+
this.ctxdata = ctxdata
|
|
32
|
+
admin.login = admin.login || admin.name
|
|
33
|
+
this.admin = admin
|
|
34
|
+
this.auth = auth
|
|
35
|
+
return new Proxy(this, {
|
|
36
|
+
get (target, prop) {
|
|
37
|
+
return prop in target ? target[prop] : target.ctxdata[prop]
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async remove () {
|
|
43
|
+
const defaultContext = await contextService.getDefault()
|
|
44
|
+
// do not remove default context
|
|
45
|
+
if (defaultContext !== undefined && this.ctxdata.id === defaultContext.id) throw new Error('Cannot remove default context')
|
|
46
|
+
try {
|
|
47
|
+
await contextService.remove(this.ctxdata.id)
|
|
48
|
+
} catch (e) {
|
|
49
|
+
if (!/Context \d+ does not exist/.test(e.message)) throw new util.PropagatedError(e)
|
|
50
|
+
else console.error(e.message)
|
|
51
|
+
}
|
|
52
|
+
created.splice(0, created.length, ...created.filter(c => c !== this))
|
|
53
|
+
users.splice(0, users.length, ...users.filter(u => u.context.id !== this.id))
|
|
54
|
+
event.emit(event.provisioning.context.removed, this)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
hasConfig (key, value) {
|
|
58
|
+
// Config structure is the following { userAttributes: { entries: [{ key: 'config', value: { entries: [{ key: 'key', value: value }] }}]} }
|
|
59
|
+
// Note that the soap lib will replace arrays with a single element by that element
|
|
60
|
+
|
|
61
|
+
// check if config exists and create config path if it does not exist
|
|
62
|
+
const emptyConfig = { entries: [{ key: 'config', value: null }] }
|
|
63
|
+
this.ctxdata.userAttributes = this.ctxdata.userAttributes || emptyConfig
|
|
64
|
+
const userAttributes = this.ctxdata.userAttributes
|
|
65
|
+
const entries = userAttributes.entries = userAttributes.entries ? [].concat(userAttributes.entries) : []
|
|
66
|
+
let configEntry = entries.find(e => e.key === 'config')
|
|
67
|
+
if (!configEntry) {
|
|
68
|
+
entries.push({ key: 'config', value: {} })
|
|
69
|
+
configEntry = entries[entries.length - 1]
|
|
70
|
+
}
|
|
71
|
+
const config = configEntry.value = configEntry.value || { entries: [] }
|
|
72
|
+
const configEntries = config.entries = config.entries ? [].concat(config.entries) : []
|
|
73
|
+
let targetConfig = configEntries.find(e => e.key === key)
|
|
74
|
+
if (!targetConfig) {
|
|
75
|
+
configEntries.push({ key })
|
|
76
|
+
targetConfig = configEntries[configEntries.length - 1]
|
|
77
|
+
}
|
|
78
|
+
targetConfig.value = value
|
|
79
|
+
|
|
80
|
+
return contextService.change({ id: this.ctxdata.id, userAttributes })
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
hasCapability (capsToAdd) {
|
|
84
|
+
return contextService.changeCapabilities(this.id, capsToAdd, undefined)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
doesntHaveCapability (capsToRemove) {
|
|
88
|
+
return contextService.changeCapabilities(this.id, undefined, capsToRemove)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
hasAccessCombination (accessCombinationName) {
|
|
92
|
+
return contextService.changeModuleAccessByName(this.id, accessCombinationName)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
getModuleAccess () {
|
|
96
|
+
return contextService.getModuleAccess(this.id)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
hasModuleAccess (moduleAccess) {
|
|
100
|
+
return contextService.changeModuleAccess(this.id, moduleAccess)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
hasQuota (maxQuota) {
|
|
104
|
+
this.ctxdata.maxQuota = maxQuota
|
|
105
|
+
return contextService.change({ id: this.id, maxQuota })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static async create (ctx = { }, adminUser = { }, auth = util.admin()) {
|
|
109
|
+
adminUser = Object.assign({
|
|
110
|
+
name: 'oxadmin',
|
|
111
|
+
password: 'secret',
|
|
112
|
+
display_name: 'context admin',
|
|
113
|
+
sur_name: 'admin',
|
|
114
|
+
given_name: 'context',
|
|
115
|
+
email1: `${adminUser.name || 'oxadmin'}@${util.mxDomain()}`,
|
|
116
|
+
primaryEmail: `${adminUser.name || 'oxadmin'}@${util.mxDomain()}`
|
|
117
|
+
}, adminUser)
|
|
118
|
+
|
|
119
|
+
let filestoreId = ctx.filestoreId
|
|
120
|
+
|
|
121
|
+
if (typeof filestoreId === 'undefined') {
|
|
122
|
+
filestoreId = await utilService.getFilestorageId()
|
|
123
|
+
}
|
|
124
|
+
const newCtx = Object.assign({ id: Number(util.userContextId()), maxQuota: -1, filestoreId }, ctx)
|
|
125
|
+
event.emit(event.provisioning.context.create, newCtx, adminUser, auth)
|
|
126
|
+
let data
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
data = await contextService.create(newCtx)
|
|
130
|
+
} catch (e) {
|
|
131
|
+
newCtx.id = util.addJitter(newCtx.id)
|
|
132
|
+
return this.create(newCtx)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const context = new Context({ ctxdata: data, admin: adminUser, auth })
|
|
136
|
+
created.push(context)
|
|
137
|
+
// only provide defaults for fresh contexts
|
|
138
|
+
await context.hasAccessCombination('all')
|
|
139
|
+
event.emit(event.provisioning.context.created, context)
|
|
140
|
+
return context
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
static async reuse (ctx, admin = { login: 'oxadmin', password: 'secret' }, auth = util.admin()) {
|
|
144
|
+
const searchCtx = Object.assign({ id: util.userContextId() }, ctx)
|
|
145
|
+
const existingContext = created.find(c => c.id === searchCtx.id)
|
|
146
|
+
if (existingContext) return existingContext
|
|
147
|
+
|
|
148
|
+
const data = await contextService.get(searchCtx)
|
|
149
|
+
|
|
150
|
+
const context = new Context({ ctxdata: data, admin, auth })
|
|
151
|
+
return context
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
static async removeAll (auth) {
|
|
155
|
+
let ctxt = created.pop()
|
|
156
|
+
while (ctxt) {
|
|
157
|
+
if (auth) ctxt.auth = auth
|
|
158
|
+
await ctxt.remove()
|
|
159
|
+
ctxt = created.pop()
|
|
160
|
+
}
|
|
161
|
+
return created
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = () => {
|
|
166
|
+
return new Proxy(created, {
|
|
167
|
+
get: function (target, prop) {
|
|
168
|
+
if (prop in target) return target[prop]
|
|
169
|
+
return Context[prop]
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Copyright (c) Open-Xchange GmbH, Germany <info@open-xchange.com>
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
*
|
|
5
|
+
* This code is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
* (at your option) any later version.
|
|
9
|
+
*
|
|
10
|
+
* This program is distributed in the hope that it will be useful,
|
|
11
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
* GNU Affero General Public License for more details.
|
|
14
|
+
*
|
|
15
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
* along with OX App Suite. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
|
17
|
+
*
|
|
18
|
+
* Any use of the work other than as authorized under this license or copyright law is prohibited.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const event = require('../event')
|
|
22
|
+
const users = require('../users/reseller')()
|
|
23
|
+
const util = require('../util')
|
|
24
|
+
const resellerContextService = require('../soap/services/resellerContext')
|
|
25
|
+
const resellerUserService = require('../soap/services/resellerUser')
|
|
26
|
+
const contexts = []
|
|
27
|
+
const created = []
|
|
28
|
+
|
|
29
|
+
class ResellerContext {
|
|
30
|
+
constructor (ctx, admin, auth) {
|
|
31
|
+
this.ctxdata = ctx
|
|
32
|
+
this.admin = admin
|
|
33
|
+
this.auth = auth
|
|
34
|
+
this.id = ctx.id
|
|
35
|
+
this.userAttributes = ctx.userAttributes
|
|
36
|
+
|
|
37
|
+
return new Proxy(this, {
|
|
38
|
+
get (target, prop) {
|
|
39
|
+
return prop in target ? target[prop] : target.ctxdata[prop]
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static async removeAll (auth) {
|
|
45
|
+
let ctx = created.pop()
|
|
46
|
+
while (ctx) {
|
|
47
|
+
if (auth) ctx.auth = auth
|
|
48
|
+
await ctx.remove()
|
|
49
|
+
ctx = created.pop()
|
|
50
|
+
}
|
|
51
|
+
return ResellerContext
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async remove () {
|
|
55
|
+
await resellerContextService.remove(this.id)
|
|
56
|
+
contexts.splice(0, contexts.length, ...contexts.filter(c => c !== this))
|
|
57
|
+
created.splice(0, created.length, ...created.filter(c => c !== this))
|
|
58
|
+
users.splice(0, users.length, ...users.filter(u => u.context.id !== this.id))
|
|
59
|
+
event.emit(event.provisioning.context.removed, this)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
hasCapability (capability) {
|
|
63
|
+
return this._setCapability(capability, 'true')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_setCapability (cap, value) {
|
|
67
|
+
const userAttributes = this.userAttributes || { entries: [] }
|
|
68
|
+
const configMap = userAttributes.entries.find(entry => entry.key === 'config') || { key: 'config' }
|
|
69
|
+
configMap.value = configMap.value || { entries: [] }
|
|
70
|
+
configMap.value.entries.push({ key: `com.openexchange.capability.${cap}`, value })
|
|
71
|
+
return resellerContextService.change({
|
|
72
|
+
ctx: {
|
|
73
|
+
id: this.id,
|
|
74
|
+
userAttributes
|
|
75
|
+
}
|
|
76
|
+
}).then(() => {
|
|
77
|
+
return resellerContextService.get({
|
|
78
|
+
ctx: { id: this.id },
|
|
79
|
+
auth: { login: this.admin.name, password: this.admin.password }
|
|
80
|
+
})
|
|
81
|
+
}).then(r => {
|
|
82
|
+
this.ctxdata = r[0].return
|
|
83
|
+
return this
|
|
84
|
+
}).catch((err) => { throw new util.PropagatedError(err) })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
doesntHaveCapability (capability) {
|
|
88
|
+
return this._setCapability(capability, false)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
hasConfig (key, value) {
|
|
92
|
+
// Config structure is the following { userAttributes: { entries: [{ key: 'config', value: { entries: [{ key: 'key', value: value }] }}]} }
|
|
93
|
+
// Note that the soap lib will replace arrays with a single element by that element
|
|
94
|
+
|
|
95
|
+
// check if config exists and create config path if it does not exist
|
|
96
|
+
const emptyConfig = { entries: [{ key: 'config', value: null }] }
|
|
97
|
+
this.ctxdata.userAttributes = this.ctxdata.userAttributes || emptyConfig
|
|
98
|
+
const userAttributes = this.ctxdata.userAttributes
|
|
99
|
+
const entries = userAttributes.entries = userAttributes.entries ? [].concat(userAttributes.entries) : []
|
|
100
|
+
let configEntry = entries.find(e => e.key === 'config')
|
|
101
|
+
if (!configEntry) {
|
|
102
|
+
entries.push({ key: 'config', value: {} })
|
|
103
|
+
configEntry = entries[entries.length - 1]
|
|
104
|
+
}
|
|
105
|
+
const config = configEntry.value = configEntry.value || { entries: [] }
|
|
106
|
+
const configEntries = config.entries = config.entries ? [].concat(config.entries) : []
|
|
107
|
+
let targetConfig = configEntries.find(e => e.key === key)
|
|
108
|
+
if (!targetConfig) {
|
|
109
|
+
configEntries.push({ key })
|
|
110
|
+
targetConfig = configEntries[configEntries.length - 1]
|
|
111
|
+
}
|
|
112
|
+
targetConfig.value = value
|
|
113
|
+
|
|
114
|
+
return resellerContextService.change({ id: this.ctxdata.id, userAttributes })
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
hasTaxonomy (taxonomy) {
|
|
118
|
+
// check if config exists and create config path if it does not exist
|
|
119
|
+
const userAttributes = this.ctxdata.userAttributes || {}
|
|
120
|
+
const entries = userAttributes.entries = userAttributes.entries ? [].concat(userAttributes.entries) : []
|
|
121
|
+
let taxonomyEntry = entries.find(e => e.key === 'taxonomy')
|
|
122
|
+
if (!taxonomyEntry) {
|
|
123
|
+
entries.push({ key: 'taxonomy', value: { entries: [{ key: 'types' }] } })
|
|
124
|
+
taxonomyEntry = entries[entries.length - 1]
|
|
125
|
+
}
|
|
126
|
+
const targetEntry = taxonomyEntry.value.entries.find(e => e.key === 'types')
|
|
127
|
+
targetEntry.value = taxonomy
|
|
128
|
+
|
|
129
|
+
return resellerContextService.change({ id: this.ctxdata.id, userAttributes })
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
hasQuota (maxQuota) {
|
|
133
|
+
this.ctxdata.maxQuota = maxQuota
|
|
134
|
+
return resellerContextService.change({ id: this.id, maxQuota })
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
getModuleAccess () {
|
|
138
|
+
return resellerContextService.getModuleAccess(this.id)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async hasModuleAccess (moduleAccess) {
|
|
142
|
+
return resellerContextService.changeModuleAccess(this.id, moduleAccess)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
get users () {
|
|
146
|
+
return resellerUserService.listAll({
|
|
147
|
+
ctx: { id: this.ctxdata.id }
|
|
148
|
+
}).then(([{ return: list }]) => {
|
|
149
|
+
return Promise.all(
|
|
150
|
+
list.map(u =>
|
|
151
|
+
resellerUserService.get({
|
|
152
|
+
user: { id: u.id },
|
|
153
|
+
ctx: { id: this.ctxdata.id },
|
|
154
|
+
auth: this.auth
|
|
155
|
+
}).then(([{ return: user }]) => user, (err) => { throw new util.PropagatedError(err) })
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
}, (err) => { throw new util.PropagatedError(err) })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
static defaultAdmin () {
|
|
162
|
+
return {
|
|
163
|
+
password: 'secret',
|
|
164
|
+
display_name: 'context admin',
|
|
165
|
+
sur_name: 'admin',
|
|
166
|
+
given_name: 'context',
|
|
167
|
+
email1: `oxadmin@${util.mxDomain()}`,
|
|
168
|
+
primaryEmail: `oxadmin@${util.mxDomain()}`
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
static async create (ctx = { }, adminUser = { }, auth = util.admin(), numberOfUsers = Number(process.env.PROVISIONING_USERS || 10)) {
|
|
173
|
+
ctx = Object.assign({ id: util.userContextId(), maxQuota: -1 }, ctx)
|
|
174
|
+
adminUser = Object.assign({
|
|
175
|
+
name: `${ctx.name || ctx.id}_admin`
|
|
176
|
+
}, this.defaultAdmin(), adminUser)
|
|
177
|
+
let data; let neededPrefix = ctx._auto_prefix
|
|
178
|
+
delete ctx._auto_prefix
|
|
179
|
+
ctx.name = (process.env.CONTEXT_PREFIX || '') + (ctx.name || `${ctx.id}`)
|
|
180
|
+
event.emit(event.provisioning.context.create, ctx, adminUser, auth)
|
|
181
|
+
try {
|
|
182
|
+
data = await resellerContextService.create({
|
|
183
|
+
ctx,
|
|
184
|
+
admin_user: adminUser,
|
|
185
|
+
auth
|
|
186
|
+
})
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (!neededPrefix && error.message.match(/name must beginn with ([^ ;]+)/)) {
|
|
189
|
+
const oldName = ctx.name
|
|
190
|
+
delete ctx.name
|
|
191
|
+
neededPrefix = error.message.match(/name must beginn with ([^ ;]+)/)[1]
|
|
192
|
+
return ResellerContext.create(Object.assign({
|
|
193
|
+
_auto_prefix: true,
|
|
194
|
+
name: `${neededPrefix}${oldName}`
|
|
195
|
+
}, ctx), adminUser, auth)
|
|
196
|
+
}
|
|
197
|
+
if (error.message.match(/already exists/)) {
|
|
198
|
+
const id = String(util.addJitter(Number.parseInt(ctx.id, 10)))
|
|
199
|
+
return ResellerContext.create(Object.assign({}, ctx, { name: ctx.name.replace(/_\d+/, `_${id}`), id }))
|
|
200
|
+
}
|
|
201
|
+
throw new util.PropagatedError(error)
|
|
202
|
+
}
|
|
203
|
+
const index = created.push(new ResellerContext(data, adminUser, auth)) - 1
|
|
204
|
+
event.emit(event.provisioning.context.created, created[index])
|
|
205
|
+
|
|
206
|
+
for (let i = 0; i < numberOfUsers; i++) {
|
|
207
|
+
// @ts-ignore
|
|
208
|
+
await users.create(users.getRandom(), created[index], true)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return created[index]
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
static async reuse (ctx, adminUser = ResellerContext.defaultAdmin(), auth = util.admin()) {
|
|
215
|
+
const existingContext = created.find(c => (c.name === ctx.name) || (c.id === ctx.id) || (new RegExp(`_${ctx.id}$`).test(c.name)))
|
|
216
|
+
if (existingContext) return existingContext
|
|
217
|
+
|
|
218
|
+
await ResellerContext.fetchContexts()
|
|
219
|
+
const remoteCtx = contexts.find(c => (c.name === ctx.name) || (c.id === ctx.id) || (new RegExp(`_${ctx.id}$`).test(c.name)))
|
|
220
|
+
if (!remoteCtx.auth) remoteCtx.auth = auth
|
|
221
|
+
if (!remoteCtx.admin_user) remoteCtx.admin_user = adminUser
|
|
222
|
+
return remoteCtx
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
static async all () {
|
|
226
|
+
if (contexts.length === 0) await this.fetchContexts()
|
|
227
|
+
return created.concat(contexts)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
static async fetchContexts () {
|
|
231
|
+
const list = await resellerContextService.listAll()
|
|
232
|
+
.catch((err) => { throw new util.PropagatedError(err) })
|
|
233
|
+
contexts.splice(0, contexts.length)
|
|
234
|
+
contexts.push.apply(contexts, list.map(c => new ResellerContext(c)))
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// can't return the class directly, because codecept tries to execute functions
|
|
239
|
+
module.exports = function () {
|
|
240
|
+
ResellerContext.fetchContexts()
|
|
241
|
+
return new Proxy(created, {
|
|
242
|
+
// act like an array if an index is being used
|
|
243
|
+
get (target, prop) {
|
|
244
|
+
if (prop in target) return target[prop]
|
|
245
|
+
return ResellerContext[prop]
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
}
|
package/src/contexts.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Copyright (c) Open-Xchange GmbH, Germany <info@open-xchange.com>
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
*
|
|
5
|
+
* This code is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
* (at your option) any later version.
|
|
9
|
+
*
|
|
10
|
+
* This program is distributed in the hope that it will be useful,
|
|
11
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
* GNU Affero General Public License for more details.
|
|
14
|
+
*
|
|
15
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
* along with OX App Suite. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
|
17
|
+
*
|
|
18
|
+
* Any use of the work other than as authorized under this license or copyright law is prohibited.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
let moduleToLoad
|
|
22
|
+
|
|
23
|
+
if (process.env.PROVISIONING_API === 'reseller') {
|
|
24
|
+
moduleToLoad = './contexts/reseller'
|
|
25
|
+
} else {
|
|
26
|
+
moduleToLoad = './contexts/contexts'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = require(moduleToLoad)
|