@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
package/src/helper.js
ADDED
|
@@ -0,0 +1,817 @@
|
|
|
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 AxeBuilder = require('@axe-core/playwright').default
|
|
23
|
+
const isEmpty = obj => [Object, Array].includes((obj || {}).constructor) && !Object.entries(obj || {}).length
|
|
24
|
+
|
|
25
|
+
const Helper = require('@codeceptjs/helper')
|
|
26
|
+
const { createHttpClient } = require('./appsuiteHttpClient')
|
|
27
|
+
const util = require('./util')
|
|
28
|
+
const fs = require('node:fs/promises')
|
|
29
|
+
const crypto = require('node:crypto')
|
|
30
|
+
const path = require('node:path')
|
|
31
|
+
const assert = require('node:assert')
|
|
32
|
+
const moment = require('moment')
|
|
33
|
+
const javascriptRoot = util.getJavascriptRoot()
|
|
34
|
+
|
|
35
|
+
const output = require('codeceptjs/lib/output')
|
|
36
|
+
|
|
37
|
+
function delay (ms) { return new Promise(resolve => setTimeout(resolve, ms)) }
|
|
38
|
+
|
|
39
|
+
async function retryPromise (fn, timeout = 5, retryDelay = 300) {
|
|
40
|
+
const start = Date.now()
|
|
41
|
+
const retry = async () => {
|
|
42
|
+
if (Date.now() - start > timeout * 1000) return Promise.reject(new Error('Timed out'))
|
|
43
|
+
await delay(retryDelay)
|
|
44
|
+
return fn().catch(retry)
|
|
45
|
+
}
|
|
46
|
+
return retry()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function jsonToForm (json) {
|
|
50
|
+
const form = new FormData()
|
|
51
|
+
for (const key in json) {
|
|
52
|
+
if (json[key] instanceof Object) form.append(key, JSON.stringify(json[key]))
|
|
53
|
+
else form.append(key, json[key])
|
|
54
|
+
}
|
|
55
|
+
return form
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function setDeepValue (key, value, target) {
|
|
59
|
+
const keys = key.split('/')
|
|
60
|
+
keys.forEach(function (key, index) {
|
|
61
|
+
if (index === keys.length - 1) {
|
|
62
|
+
target[key] = value
|
|
63
|
+
} else {
|
|
64
|
+
target = target[key] = target[key] || {}
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function saveSetting (name, settings, options) {
|
|
70
|
+
const httpClient = createHttpClient(options)
|
|
71
|
+
const data = await httpClient.get('/api/jslob', {
|
|
72
|
+
params: { id: name, action: 'get' }
|
|
73
|
+
}).then(res => res.data.data)
|
|
74
|
+
|
|
75
|
+
for (const key in settings) {
|
|
76
|
+
const value = settings[key]
|
|
77
|
+
setDeepValue(key, value, data.tree)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return httpClient.put('/api/jslob', data.tree, {
|
|
81
|
+
params: { action: 'set', id: name }
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function createBlobFromFile (file) {
|
|
86
|
+
let filename, blob
|
|
87
|
+
if (typeof file === 'string') {
|
|
88
|
+
filename = path.basename(file)
|
|
89
|
+
// @ts-ignore
|
|
90
|
+
blob = new Blob([await fs.readFile(path.join(codecept_dir, file))])
|
|
91
|
+
} else {
|
|
92
|
+
filename = file.filename || file.name
|
|
93
|
+
blob = new Blob([file.content])
|
|
94
|
+
}
|
|
95
|
+
return { filename, blob }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function compareBinaries (filePath1, filePath2) {
|
|
99
|
+
const [file1, file2] = await Promise.all([fs.readFile(filePath1), fs.readFile(filePath2)])
|
|
100
|
+
|
|
101
|
+
const hash1 = crypto.createHash('sha256').update(file1).digest('hex')
|
|
102
|
+
const hash2 = crypto.createHash('sha256').update(file2).digest('hex')
|
|
103
|
+
|
|
104
|
+
return hash1 === hash2
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function fileExists (filePath) {
|
|
108
|
+
try {
|
|
109
|
+
await fs.access(filePath)
|
|
110
|
+
return true
|
|
111
|
+
} catch (e) {
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
class AppSuiteHelper extends Helper {
|
|
117
|
+
constructor (config) {
|
|
118
|
+
super(config)
|
|
119
|
+
codecept.locator.addFilter((locator, result) => {
|
|
120
|
+
if (typeof locator === 'string' && locator.indexOf('~') === 0) {
|
|
121
|
+
// accessibility locator
|
|
122
|
+
result.value = `[aria-label^="${locator.slice(1)}"]`
|
|
123
|
+
result.type = 'css'
|
|
124
|
+
result.output = `aria-label^=${locator.slice(1)}`
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async waitForDownload (locator, filename, referenceFilePath) {
|
|
130
|
+
const { page } = this.helpers.Playwright
|
|
131
|
+
const downloadPromise = page.waitForEvent('download')
|
|
132
|
+
await page.locator(locator).click()
|
|
133
|
+
const download = await downloadPromise
|
|
134
|
+
const filePath = path.join(codecept_dir, '.tmp', download.suggestedFilename())
|
|
135
|
+
|
|
136
|
+
await download.saveAs(filePath)
|
|
137
|
+
|
|
138
|
+
// delay to ensure the file is written to disk
|
|
139
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
140
|
+
|
|
141
|
+
assert.ok(await fileExists(filePath), `Downloaded file "${filename}" does not exist`)
|
|
142
|
+
|
|
143
|
+
if (!referenceFilePath) return
|
|
144
|
+
const reference = path.join(codecept_dir, referenceFilePath)
|
|
145
|
+
assert.ok(await compareBinaries(filePath, reference), `Downloaded file "${filename}" does not match the reference file`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async grabAxeReport ({ disableRules = [], exclude = [], include = [] } = {}) {
|
|
149
|
+
const { page } = this.helpers.Playwright
|
|
150
|
+
const axeBuilder = new AxeBuilder({ page })
|
|
151
|
+
if (disableRules) axeBuilder.disableRules(disableRules)
|
|
152
|
+
if (exclude) {
|
|
153
|
+
exclude = Array.isArray(exclude) ? exclude : [exclude]
|
|
154
|
+
exclude.forEach(element => {
|
|
155
|
+
axeBuilder.exclude(element)
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
if (include) {
|
|
159
|
+
include = Array.isArray(include) ? include : [include]
|
|
160
|
+
include.forEach(element => {
|
|
161
|
+
axeBuilder.include(element)
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const report = await axeBuilder.analyze()
|
|
166
|
+
|
|
167
|
+
const nodes = []
|
|
168
|
+
for (const violation of report.violations) {
|
|
169
|
+
for (const node of violation.nodes) {
|
|
170
|
+
nodes.push(node.target)
|
|
171
|
+
for (const combinedNodes of [node.all, node.any, node.none]) {
|
|
172
|
+
if (!isEmpty(combinedNodes)) {
|
|
173
|
+
for (const any of combinedNodes) {
|
|
174
|
+
for (const relatedNode of any.relatedNodes) {
|
|
175
|
+
nodes.push(relatedNode.target)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await page.evaluate(selectors => {
|
|
184
|
+
// Loop through the array of selectors and apply a red border to highlight elements
|
|
185
|
+
selectors.forEach(selector => {
|
|
186
|
+
const element = document.querySelector(selector)
|
|
187
|
+
if (element) element.style.border = '2px solid red'
|
|
188
|
+
})
|
|
189
|
+
}, nodes)
|
|
190
|
+
if (typeof report === 'string') throw report
|
|
191
|
+
|
|
192
|
+
return report
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async selectFolder (id, context) {
|
|
196
|
+
const { page } = this.helpers.Playwright
|
|
197
|
+
const error = await page.evaluate(async ({ id, context, timeout }) => {
|
|
198
|
+
const { _, $, ox } = await import(`${window.location.pathname}e2e.js`)
|
|
199
|
+
|
|
200
|
+
import(`${window.location.pathname}io.ox/core/folder/api.js`).then(function ({ default: folderAPI }) {
|
|
201
|
+
function repeatUntil (cb, interval, timeout) {
|
|
202
|
+
const start = _.now()
|
|
203
|
+
const def = new $.Deferred()
|
|
204
|
+
const iterate = function () {
|
|
205
|
+
const result = cb()
|
|
206
|
+
if (result) return def.resolve(result)
|
|
207
|
+
if (_.now() - start < timeout) return _.delay(iterate, interval)
|
|
208
|
+
def.reject({ message: `Folder API could not resolve folder after ${timeout / 1000} seconds` })
|
|
209
|
+
}
|
|
210
|
+
iterate()
|
|
211
|
+
return def
|
|
212
|
+
}
|
|
213
|
+
return Promise.all([
|
|
214
|
+
repeatUntil(function () {
|
|
215
|
+
return _(folderAPI.pool.models).find(function (m) {
|
|
216
|
+
const res = m.get('title') === id || m.get('id') === id || m.get('display_title') === id
|
|
217
|
+
return context ? res && m.get('module') === context : res
|
|
218
|
+
})
|
|
219
|
+
}, 100, timeout),
|
|
220
|
+
repeatUntil(function () {
|
|
221
|
+
return ox.ui.App.getCurrentApp()
|
|
222
|
+
}, 100, timeout)
|
|
223
|
+
]).then(function ([model, app]) {
|
|
224
|
+
// special handling for virtual folders
|
|
225
|
+
if (model.get('id').indexOf('virtual/') === 0) {
|
|
226
|
+
const body = app.getWindow().nodes.body
|
|
227
|
+
if (body) {
|
|
228
|
+
const view = body.find('.folder-tree').data('view')
|
|
229
|
+
if (view) {
|
|
230
|
+
view.trigger('virtual', model.get('id'))
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return app.folder.set(model.get('id'))
|
|
235
|
+
})
|
|
236
|
+
}).then(function () { })
|
|
237
|
+
}, { id, context, timeout: 5000, javascriptRoot })
|
|
238
|
+
if (error) throw error
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async throttleNetwork (networkConfig) {
|
|
242
|
+
// some network speed presets
|
|
243
|
+
const presets = {
|
|
244
|
+
OFFLINE: {
|
|
245
|
+
offline: true,
|
|
246
|
+
downloadThroughput: 0,
|
|
247
|
+
uploadThroughput: 0,
|
|
248
|
+
latency: 0
|
|
249
|
+
},
|
|
250
|
+
GPRS: {
|
|
251
|
+
offline: false,
|
|
252
|
+
downloadThroughput: 50 * 1024 / 8,
|
|
253
|
+
uploadThroughput: 20 * 1024 / 8,
|
|
254
|
+
latency: 500
|
|
255
|
+
},
|
|
256
|
+
'2G': {
|
|
257
|
+
offline: false,
|
|
258
|
+
downloadThroughput: 250 * 1024 / 8,
|
|
259
|
+
uploadThroughput: 50 * 1024 / 8,
|
|
260
|
+
latency: 300
|
|
261
|
+
},
|
|
262
|
+
'3G': {
|
|
263
|
+
offline: false,
|
|
264
|
+
downloadThroughput: 750 * 1024 / 8,
|
|
265
|
+
uploadThroughput: 250 * 1024 / 8,
|
|
266
|
+
latency: 100
|
|
267
|
+
},
|
|
268
|
+
'4G': {
|
|
269
|
+
offline: false,
|
|
270
|
+
downloadThroughput: 4 * 1024 * 1024 / 8,
|
|
271
|
+
uploadThroughput: 3 * 1024 * 1024 / 8,
|
|
272
|
+
latency: 20
|
|
273
|
+
},
|
|
274
|
+
DSL: {
|
|
275
|
+
offline: false,
|
|
276
|
+
downloadThroughput: 2 * 1024 * 1024 / 8,
|
|
277
|
+
uploadThroughput: 1 * 1024 * 1024 / 8,
|
|
278
|
+
latency: 5
|
|
279
|
+
},
|
|
280
|
+
// no throttling, use this to reset the connection
|
|
281
|
+
ONLINE: {
|
|
282
|
+
offline: false,
|
|
283
|
+
downloadThroughput: -1,
|
|
284
|
+
uploadThroughput: -1,
|
|
285
|
+
latency: 0
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const { page } = this.helpers.Playwright
|
|
290
|
+
|
|
291
|
+
// get Chrome DevTools session
|
|
292
|
+
const devTools = await page.context().newCDPSession(page)
|
|
293
|
+
|
|
294
|
+
// Set network speed, use preset if its there
|
|
295
|
+
await devTools.send('Network.emulateNetworkConditions', presets[networkConfig.toUpperCase()] || networkConfig)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async haveSetting (obj, options) {
|
|
299
|
+
if (typeof obj === 'string') {
|
|
300
|
+
const input = obj.split('//')
|
|
301
|
+
const moduleName = input[0]
|
|
302
|
+
const key = input[1]
|
|
303
|
+
const value = options
|
|
304
|
+
obj = { [moduleName]: { [key]: value } }
|
|
305
|
+
options = arguments[2]
|
|
306
|
+
}
|
|
307
|
+
options = options || {}
|
|
308
|
+
|
|
309
|
+
// need to save settings sequentially, because concurrent requests will fail
|
|
310
|
+
for (const moduleName in obj) {
|
|
311
|
+
await saveSetting(moduleName, obj[moduleName], options)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async haveSnippet (snippet, options) {
|
|
316
|
+
const httpClient = createHttpClient(options)
|
|
317
|
+
const response = await httpClient.put('/api/snippet', snippet, { params: { action: 'new' } })
|
|
318
|
+
assert.strictEqual(response.data.error, undefined, JSON.stringify(response.data))
|
|
319
|
+
return response.data
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async haveMail (data, options) {
|
|
323
|
+
const httpClient = createHttpClient(options)
|
|
324
|
+
|
|
325
|
+
let form, response
|
|
326
|
+
if (!data.folder) data.folder = 'default0/INBOX'
|
|
327
|
+
if (data.folder && (data.path || data.source)) {
|
|
328
|
+
// import the mail
|
|
329
|
+
form = new FormData()
|
|
330
|
+
if (data.path) {
|
|
331
|
+
const { blob } = await createBlobFromFile(data.path)
|
|
332
|
+
form.append('file', blob)
|
|
333
|
+
} else if (data.source) {
|
|
334
|
+
form.append('file', new Blob([data.source], { type: 'text/plain' }), data.filename || 'test.eml')
|
|
335
|
+
}
|
|
336
|
+
response = await httpClient.post('/api/mail', form, {
|
|
337
|
+
params: {
|
|
338
|
+
action: 'import',
|
|
339
|
+
folder: data.folder,
|
|
340
|
+
force: true
|
|
341
|
+
}
|
|
342
|
+
})
|
|
343
|
+
} else {
|
|
344
|
+
// send the mail
|
|
345
|
+
// transform some data
|
|
346
|
+
if (data.content) {
|
|
347
|
+
data.attachments = data.attachments || []
|
|
348
|
+
data.attachments.unshift({
|
|
349
|
+
content: data.content,
|
|
350
|
+
content_type: 'text/html',
|
|
351
|
+
disp: 'inline'
|
|
352
|
+
})
|
|
353
|
+
delete data.content
|
|
354
|
+
}
|
|
355
|
+
const files = []
|
|
356
|
+
for (let i = 1; data.attachments && i < data.attachments.length; i++) {
|
|
357
|
+
const file = data.attachments[i]
|
|
358
|
+
const { filename, blob } = await createBlobFromFile(file)
|
|
359
|
+
|
|
360
|
+
if (filename && blob) {
|
|
361
|
+
data.attachments[i] = { filename, disp: 'attachment', name: filename }
|
|
362
|
+
files.push({ filename, blob })
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (!(data.from instanceof Array) && data.from.get) {
|
|
366
|
+
data.from = [[data.from.get('displayname'), data.from.get('primaryEmail')]]
|
|
367
|
+
}
|
|
368
|
+
if (!(data.to instanceof Array) && data.to.get) {
|
|
369
|
+
data.to = [[data.to.get('displayname'), data.to.get('primaryEmail')]]
|
|
370
|
+
}
|
|
371
|
+
if (data.sendtype === undefined) {
|
|
372
|
+
data.sendtype = 0
|
|
373
|
+
}
|
|
374
|
+
form = jsonToForm({ json_0: data })
|
|
375
|
+
for (let i = 0; i < files.length; i++) {
|
|
376
|
+
form.append(`file_${i}`, files[i].blob, files[i].filename)
|
|
377
|
+
}
|
|
378
|
+
response = await httpClient.post('/api/mail', form, {
|
|
379
|
+
params: {
|
|
380
|
+
action: 'new',
|
|
381
|
+
force_json_response: true,
|
|
382
|
+
lineWrapAfter: 0
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
const matches = /\((\{.*?\})\)/.exec(response.data)
|
|
387
|
+
const resData = matches && matches[1] ? JSON.parse(matches[1]) : response.data
|
|
388
|
+
assert.strictEqual(resData.error, undefined, JSON.stringify(resData))
|
|
389
|
+
return resData
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async haveMails (iterable, options) {
|
|
393
|
+
for (const mail of iterable) await this.haveMail(mail, options)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async haveAnAlias (alias, options = {}) {
|
|
397
|
+
console.warn('This method is deprecated, use `user.hasAlias` instead')
|
|
398
|
+
const user = options.user || codecept.container.support('users')[0]
|
|
399
|
+
return user.hasAlias(alias)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async haveMailFilterRule (rule, options) {
|
|
403
|
+
const httpClient = createHttpClient(options)
|
|
404
|
+
|
|
405
|
+
const response = await httpClient.put('/api/mailfilter/v2', rule, {
|
|
406
|
+
params: { action: 'new' }
|
|
407
|
+
})
|
|
408
|
+
assert.strictEqual(response.data.error, undefined, JSON.stringify(response.data))
|
|
409
|
+
return response.data
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async haveFolder ({ title, module, permissions = [], subscribed = 1, parent }, options) {
|
|
413
|
+
const httpClient = createHttpClient(options)
|
|
414
|
+
|
|
415
|
+
const roles = { viewer: 257, reviewer: 33025, author: 4227332, administrator: 272662788, owner: 272662788 }
|
|
416
|
+
permissions = permissions.map((perm) => {
|
|
417
|
+
if (!perm.access || !perm.user) return perm
|
|
418
|
+
assert.notEqual(undefined, roles[perm.access])
|
|
419
|
+
return { entity: perm.user.userdata.id, bits: roles[perm.access], group: false }
|
|
420
|
+
})
|
|
421
|
+
const payload = { title, module, subscribed, permissions }
|
|
422
|
+
if (permissions.length === 0) delete payload.permissions
|
|
423
|
+
const response = await httpClient.put('/api/folders', payload, {
|
|
424
|
+
params: { action: 'new', folder: parent }
|
|
425
|
+
})
|
|
426
|
+
assert.strictEqual(response.data.error, undefined, JSON.stringify(response.data))
|
|
427
|
+
return response.data.data
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async haveContact (contact, options) {
|
|
431
|
+
const httpClient = createHttpClient(options)
|
|
432
|
+
const response = await httpClient.put('/api/contacts', contact, {
|
|
433
|
+
params: { action: 'new' }
|
|
434
|
+
})
|
|
435
|
+
assert.strictEqual(response.data.error, undefined, JSON.stringify(response.data))
|
|
436
|
+
return response.data.data
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async grabDefaultFolder (module, options = {}) {
|
|
440
|
+
const httpClient = createHttpClient(options)
|
|
441
|
+
const jslob = module === 'mail' ? 'io.ox/mail' : 'io.ox/core'
|
|
442
|
+
const response = await httpClient.put('/api/jslob', [jslob], {
|
|
443
|
+
params: { action: 'list' }
|
|
444
|
+
})
|
|
445
|
+
assert.strictEqual(response.data.error, undefined, JSON.stringify(response.data))
|
|
446
|
+
const tree = response.data.data[0].tree
|
|
447
|
+
if (module === 'mail') return tree.defaultFolder[options.type || 'inbox']
|
|
448
|
+
return tree.folder[module]
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Uploads a file to a specified folder.
|
|
453
|
+
*
|
|
454
|
+
* @param {string} folder - The ID of the folder where the file will be uploaded.
|
|
455
|
+
* @param {string|object} file - The path to the file or an object containing the file information.
|
|
456
|
+
* @param {object} [options] - Additional options for the file upload.
|
|
457
|
+
* @returns {Promise<Object>} - The uploaded file data.
|
|
458
|
+
*/
|
|
459
|
+
async haveFile (folder, file, options) {
|
|
460
|
+
options = options || {}
|
|
461
|
+
const form = new FormData()
|
|
462
|
+
// prepare file for usage
|
|
463
|
+
const { filename, blob } = await createBlobFromFile(file)
|
|
464
|
+
form.append('file', blob, filename)
|
|
465
|
+
form.append('json', JSON.stringify({
|
|
466
|
+
folder_id: folder,
|
|
467
|
+
description: ''
|
|
468
|
+
}))
|
|
469
|
+
const httpClient = createHttpClient(options)
|
|
470
|
+
const response = await httpClient.post('/api/files', form, {
|
|
471
|
+
params: {
|
|
472
|
+
action: 'new',
|
|
473
|
+
extendedResponse: true,
|
|
474
|
+
try_add_version: true,
|
|
475
|
+
cryptoAction: options.cryptoAction || ''
|
|
476
|
+
},
|
|
477
|
+
headers: null
|
|
478
|
+
})
|
|
479
|
+
const matches = /\((\{.*?\})\)/.exec(response.data)
|
|
480
|
+
const resData = matches && matches[1] ? JSON.parse(matches[1]) : response.data.updated[0]
|
|
481
|
+
assert.strictEqual(resData.error, undefined, JSON.stringify(resData))
|
|
482
|
+
return resData.data.file
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async haveTask (task, options) {
|
|
486
|
+
const httpClient = createHttpClient(options)
|
|
487
|
+
const response = await httpClient.put('/api/tasks', task, {
|
|
488
|
+
params: { action: 'new' }
|
|
489
|
+
})
|
|
490
|
+
assert.strictEqual(response.data.error, undefined, JSON.stringify(response.data))
|
|
491
|
+
return response.data.data
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Uploads an attachment to the specified module and object.
|
|
496
|
+
*
|
|
497
|
+
* @param {string} module - The module to attach the file to.
|
|
498
|
+
* @param {object} obj - The object to attach the file to.
|
|
499
|
+
* @param {string} file - The file to be attached.
|
|
500
|
+
* @param {object} options - The options for creating the HTTP client.
|
|
501
|
+
* @returns {Promise<object>} - The response data after attaching the file.
|
|
502
|
+
*/
|
|
503
|
+
async haveAttachment (module, obj, file, options) {
|
|
504
|
+
const mapping = { tasks: 4, contacts: 7, drive: 137, infostore: 137, files: 137 }
|
|
505
|
+
const httpClient = createHttpClient(options)
|
|
506
|
+
const { blob, filename } = await createBlobFromFile(file)
|
|
507
|
+
const form = new FormData()
|
|
508
|
+
let response
|
|
509
|
+
let resData
|
|
510
|
+
if (module === 'calendar' || module === 'chronos') {
|
|
511
|
+
const index = obj.attachments ? obj.attachments.length : 0
|
|
512
|
+
obj = Object.assign({ attachments: [] }, obj)
|
|
513
|
+
obj.attachments.push({ filename, uri: `cid:file_${index}` })
|
|
514
|
+
form.append('file_' + index, blob, filename)
|
|
515
|
+
form.append('json_0', JSON.stringify(obj))
|
|
516
|
+
response = await httpClient.post('/api/chronos', form, {
|
|
517
|
+
params: {
|
|
518
|
+
action: 'update',
|
|
519
|
+
folder: obj.folder,
|
|
520
|
+
id: obj.id,
|
|
521
|
+
timestamp: moment().add(100000).valueOf()
|
|
522
|
+
}
|
|
523
|
+
})
|
|
524
|
+
const matches = /\((\{.*?\})\)/.exec(response.data)
|
|
525
|
+
resData = matches && matches[1] ? JSON.parse(matches[1]) : response.data.updated[0]
|
|
526
|
+
if (resData.data && resData.data.updated && resData.data.updated.length > 0) resData = resData.data.updated[0]
|
|
527
|
+
} else {
|
|
528
|
+
form.append('file_0', blob, filename)
|
|
529
|
+
form.append('json_0', JSON.stringify({ module: mapping[module], attached: obj.id, folder: obj.folder_id || obj.folder }))
|
|
530
|
+
response = await httpClient.post('/api/attachment', form, {
|
|
531
|
+
params: {
|
|
532
|
+
action: 'attach',
|
|
533
|
+
force_json_response: true
|
|
534
|
+
}
|
|
535
|
+
})
|
|
536
|
+
resData = response.data
|
|
537
|
+
}
|
|
538
|
+
assert.strictEqual(resData.error, undefined, JSON.stringify(resData))
|
|
539
|
+
return resData
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async haveAppointment (appointment, options) {
|
|
543
|
+
const httpClient = createHttpClient(options)
|
|
544
|
+
|
|
545
|
+
if (appointment.startDate?.value?._isAMomentObject) appointment.startDate.value = appointment.startDate.value.format('YYYYMMDD[T]HHmmss')
|
|
546
|
+
if (appointment.endDate?.value?._isAMomentObject) appointment.endDate.value = appointment.endDate.value.format('YYYYMMDD[T]HHmmss')
|
|
547
|
+
|
|
548
|
+
if (appointment.startDate && !appointment.startDate.tzid) appointment.startDate.tzid = 'Europe/Berlin'
|
|
549
|
+
if (appointment.endDate && !appointment.endDate.tzid) appointment.endDate.tzid = 'Europe/Berlin'
|
|
550
|
+
|
|
551
|
+
if (!appointment.folder) {
|
|
552
|
+
const jslob = await httpClient.put('/api/jslob', ['io.ox/core'], {
|
|
553
|
+
params: { action: 'list' }
|
|
554
|
+
})
|
|
555
|
+
assert.strictEqual(jslob.data.error, undefined, JSON.stringify(jslob.data))
|
|
556
|
+
appointment.folder = jslob.data.data[0].tree.folder.calendar
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (appointment.folder && !appointment.folder.startsWith('cal://0/')) appointment.folder = `cal://0/${appointment.folder}`
|
|
560
|
+
|
|
561
|
+
const response = await httpClient.put('/api/chronos', appointment, {
|
|
562
|
+
params: {
|
|
563
|
+
action: 'new',
|
|
564
|
+
folder: appointment.folder
|
|
565
|
+
}
|
|
566
|
+
})
|
|
567
|
+
assert.strictEqual(response.data.error, undefined, JSON.stringify(response.data))
|
|
568
|
+
return response.data.data.created[0]
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async importAppointment ({ sourcePath, folder = undefined }, options) {
|
|
572
|
+
const httpClient = createHttpClient(options)
|
|
573
|
+
|
|
574
|
+
// prepare folder
|
|
575
|
+
if (!folder) {
|
|
576
|
+
const jslob = await httpClient.put('/api/jslob', ['io.ox/core'], {
|
|
577
|
+
params: { action: 'list' }
|
|
578
|
+
})
|
|
579
|
+
assert.strictEqual(jslob.data.error, undefined, JSON.stringify(jslob.data))
|
|
580
|
+
folder = jslob.data.data[0].tree.folder.calendar
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (folder && !folder.startsWith('cal://0/')) folder = `cal://0/${folder}`
|
|
584
|
+
|
|
585
|
+
// prepare form upload
|
|
586
|
+
const form = new FormData()
|
|
587
|
+
const { blob } = await createBlobFromFile(sourcePath)
|
|
588
|
+
form.append('file', blob)
|
|
589
|
+
|
|
590
|
+
const response = await httpClient.post('/api/import', form, {
|
|
591
|
+
params: {
|
|
592
|
+
action: 'ICAL',
|
|
593
|
+
folder,
|
|
594
|
+
ignoreUIDs: true
|
|
595
|
+
}
|
|
596
|
+
})
|
|
597
|
+
assert.strictEqual(response.data.error, undefined, JSON.stringify(response.data))
|
|
598
|
+
return response
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async haveResource (data, options) {
|
|
602
|
+
const httpClient = createHttpClient(options)
|
|
603
|
+
let response = await httpClient.put('/api/resource', data, {
|
|
604
|
+
params: { action: 'new' }
|
|
605
|
+
})
|
|
606
|
+
if (response.data.error && response.data.code === 'RES-0006') {
|
|
607
|
+
const { id } = (await httpClient.put('/api/resource', { pattern: data.name }, {
|
|
608
|
+
params: { action: 'search' }
|
|
609
|
+
})).data.data[0]
|
|
610
|
+
response = await httpClient.put('/api/resource', data, {
|
|
611
|
+
params: { action: 'update', id }
|
|
612
|
+
})
|
|
613
|
+
// middleware returns no data here
|
|
614
|
+
Object.assign(response.data.data, data, { id })
|
|
615
|
+
}
|
|
616
|
+
if (response.data.error) throw response.data
|
|
617
|
+
return response.data.data.id
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async dontHaveResource (pattern, options) {
|
|
621
|
+
const httpClient = createHttpClient(options)
|
|
622
|
+
let data
|
|
623
|
+
if (typeof pattern.id !== 'undefined') {
|
|
624
|
+
data = [(await httpClient.get('/api/resource', {
|
|
625
|
+
params: { action: 'get', id: pattern.id }
|
|
626
|
+
})).data.data]
|
|
627
|
+
} else {
|
|
628
|
+
data = (await httpClient.put('/api/resource', { pattern }, {
|
|
629
|
+
params: { action: 'search' }
|
|
630
|
+
})).data.data
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const timestamp = require('moment')().add(30, 'years').format('x')
|
|
634
|
+
return Promise.all(data.map(async ({ id }) => {
|
|
635
|
+
await httpClient.put('/api/resource', { id }, {
|
|
636
|
+
params: { action: 'delete', timestamp }
|
|
637
|
+
})
|
|
638
|
+
return { id, pattern }
|
|
639
|
+
}))
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
async haveGroup (group, options) {
|
|
643
|
+
const httpClient = createHttpClient(options)
|
|
644
|
+
const response = await httpClient.put('/api/group', group, {
|
|
645
|
+
params: { action: 'new' }
|
|
646
|
+
})
|
|
647
|
+
return response.data.data
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
async dontHaveGroup (name, options) {
|
|
651
|
+
const httpClient = createHttpClient(options)
|
|
652
|
+
const { data: { data } } = await httpClient.put('/api/group', '', {
|
|
653
|
+
params: {
|
|
654
|
+
action: 'all',
|
|
655
|
+
columns: '1,701'
|
|
656
|
+
}
|
|
657
|
+
})
|
|
658
|
+
const timestamp = require('moment')().add(30, 'years').format('x')
|
|
659
|
+
const test = typeof name.test === 'function' ? g => name.test(g[1]) : g => name === g[1]
|
|
660
|
+
|
|
661
|
+
const ids = data.filter(test).map(g => g[0])
|
|
662
|
+
return Promise.all(ids.map(async (id) => {
|
|
663
|
+
await httpClient.put('/api/group', { id }, {
|
|
664
|
+
params: {
|
|
665
|
+
action: 'delete',
|
|
666
|
+
timestamp
|
|
667
|
+
}
|
|
668
|
+
})
|
|
669
|
+
return { id, name }
|
|
670
|
+
}))
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
async haveLockedFile (data, options) {
|
|
674
|
+
const httpClient = createHttpClient(options)
|
|
675
|
+
const response = await httpClient.put('/api/files', data, {
|
|
676
|
+
params: {
|
|
677
|
+
action: 'lock',
|
|
678
|
+
id: data.id,
|
|
679
|
+
folder: data.folder_id
|
|
680
|
+
}
|
|
681
|
+
})
|
|
682
|
+
return response.data
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
async setMailCategories (data, options) {
|
|
686
|
+
const httpClient = createHttpClient(options)
|
|
687
|
+
const { mailId, folder, categories } = data
|
|
688
|
+
const response = await httpClient.put('/api/mail', { set_user_flags: categories }, {
|
|
689
|
+
params: {
|
|
690
|
+
action: 'update',
|
|
691
|
+
id: mailId,
|
|
692
|
+
folder
|
|
693
|
+
}
|
|
694
|
+
})
|
|
695
|
+
return response.data
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
async pressKeys (key) {
|
|
699
|
+
return [...key].forEach(k => this.helpers.Playwright.pressKey(k))
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* @param {object} options
|
|
704
|
+
* @param {object} [options.user] a user object as returned by provisioning helper, default is the "first" user
|
|
705
|
+
* @param {object} [options.additionalAccount] an additional user that will be provisioned as the external account
|
|
706
|
+
* @param {string} [options.extension] optional extension added to the mail address ("ext" will be translated to: $user.primary+ext@mailDomain)
|
|
707
|
+
* @param {string} [options.name] name of the account
|
|
708
|
+
* @param {string} [options.transport_auth] transport authentication, default: 'none'
|
|
709
|
+
*/
|
|
710
|
+
async haveMailAccount ({ user, additionalAccount, extension, name, transport_auth: transportAuth }) {
|
|
711
|
+
if (!user) user = inject().users[0]
|
|
712
|
+
if (!additionalAccount) additionalAccount = user
|
|
713
|
+
if (!transportAuth) transportAuth = 'none'
|
|
714
|
+
|
|
715
|
+
const httpClient = createHttpClient({ user })
|
|
716
|
+
const mailDomain = additionalAccount.get('primaryEmail').replace(/.*@/, '')
|
|
717
|
+
const imapServer = additionalAccount.get('imapServer') === 'localhost' ? mailDomain : additionalAccount.get('imapServer')
|
|
718
|
+
const smtpServer = additionalAccount.get('smtpServer') === 'localhost' ? mailDomain : additionalAccount.get('smtpServer')
|
|
719
|
+
|
|
720
|
+
const account = {
|
|
721
|
+
name,
|
|
722
|
+
primary_address: `${additionalAccount.get('primaryEmail').replace(/@.*/, '')}${extension ? '-' + extension : ''}@${mailDomain}`,
|
|
723
|
+
login: additionalAccount.get('imapLogin'),
|
|
724
|
+
password: additionalAccount.get('password'),
|
|
725
|
+
mail_url: `${additionalAccount.get('imapSchema')}${imapServer}:${additionalAccount.get('imapPort')}`,
|
|
726
|
+
transport_url: `${additionalAccount.get('smtpSchema')}${smtpServer}:${additionalAccount.get('smtpPort')}`,
|
|
727
|
+
transport_auth: transportAuth
|
|
728
|
+
}
|
|
729
|
+
const response = await httpClient.put('/api/account', account, {
|
|
730
|
+
params: {
|
|
731
|
+
action: 'new',
|
|
732
|
+
session
|
|
733
|
+
}
|
|
734
|
+
})
|
|
735
|
+
return response.data
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* @param {object} obj
|
|
740
|
+
* @param {number} timeout
|
|
741
|
+
* @param {object} options
|
|
742
|
+
* @returns
|
|
743
|
+
*/
|
|
744
|
+
async waitForSetting (obj, timeout = 5, options = {}) {
|
|
745
|
+
const httpClient = createHttpClient(options)
|
|
746
|
+
const [moduleName] = Object.keys(obj)
|
|
747
|
+
|
|
748
|
+
async function checkSetting () {
|
|
749
|
+
const data = await httpClient.get('/api/jslob', {
|
|
750
|
+
params: {
|
|
751
|
+
id: moduleName,
|
|
752
|
+
action: 'get'
|
|
753
|
+
}
|
|
754
|
+
}).then(function (res) {
|
|
755
|
+
if (res.data.error) throw new Error(res.data.error)
|
|
756
|
+
return res.data.data
|
|
757
|
+
})
|
|
758
|
+
expect(data.tree).to.containSubset(obj[moduleName])
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const startTime = Date.now()
|
|
762
|
+
|
|
763
|
+
return retryPromise(checkSetting, timeout).finally(() =>
|
|
764
|
+
output.say(`Waiting for settings to be saved took ${(Date.now() - startTime) / 1000} seconds`)
|
|
765
|
+
)
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
async waitForCapability (capability, timeout = 10, options = { shouldBe: true }) {
|
|
769
|
+
const httpClient = createHttpClient(options)
|
|
770
|
+
|
|
771
|
+
async function checkCapability () {
|
|
772
|
+
const data = await httpClient.get('/api/capabilities', {
|
|
773
|
+
params: {
|
|
774
|
+
action: 'get',
|
|
775
|
+
id: capability,
|
|
776
|
+
session
|
|
777
|
+
}
|
|
778
|
+
}).then(function (res) {
|
|
779
|
+
if (options.shouldBe && Object.keys(res.data).length === 0) throw new Error(res.data.error)
|
|
780
|
+
else if (!options.shouldBe && Object.keys(res.data).length !== 0) throw new Error(res.data.error)
|
|
781
|
+
return res.data
|
|
782
|
+
})
|
|
783
|
+
return data
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const startTime = Date.now()
|
|
787
|
+
// We wait 2500ms to make sure that the cache was invalidated in the meantime
|
|
788
|
+
return retryPromise(checkCapability, timeout, 1000).finally(() =>
|
|
789
|
+
output.say(`Waiting for capability to be saved took ${(Date.now() - startTime) / 1000} seconds`)
|
|
790
|
+
)
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
async waitForApp () {
|
|
794
|
+
const { Playwright } = this.helpers
|
|
795
|
+
|
|
796
|
+
await Playwright.waitForInvisible('#background-loader.busy', 30)
|
|
797
|
+
await Playwright.waitForVisible({ css: 'html.complete' }, 10)
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
async copyToClipboard () {
|
|
801
|
+
const helper = this.helpers.Playwright
|
|
802
|
+
const { page } = helper
|
|
803
|
+
await page.evaluate(async () => { document.execCommand('copy') })
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
async getClipboardContent () {
|
|
807
|
+
const helper = this.helpers.Playwright
|
|
808
|
+
const { page, config } = helper
|
|
809
|
+
// make sure the browser is focused, might have lost the focus due to executing steps manually with 'pause()'
|
|
810
|
+
await page.bringToFront()
|
|
811
|
+
// reading the clipboard requires correct permissions
|
|
812
|
+
await helper.browserContext.grantPermissions(['clipboard-read'], { origin: config.url })
|
|
813
|
+
return await page.evaluate(async () => { return navigator.clipboard.readText() })
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
module.exports = AppSuiteHelper
|