@open-xchange/appsuite-codeceptjs 0.1.1 → 0.2.1

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/index.js CHANGED
@@ -23,7 +23,7 @@ const dotenv = require('dotenv')
23
23
  dotenv.config({ path: '.env' })
24
24
  dotenv.config({ path: '.env.defaults' })
25
25
 
26
- const requiredEnvVars = ['LAUNCH_URL', 'PROVISIONING_URL', 'CONTEXT_ID']
26
+ const requiredEnvVars = ['LAUNCH_URL', 'PROVISIONING_URL']
27
27
 
28
28
  requiredEnvVars.forEach(function notdefined (key) {
29
29
  if (process.env[key]) return
@@ -58,7 +58,7 @@ module.exports = {
58
58
  '--no-first-run',
59
59
  '--no-sandbox',
60
60
  '--no-zygote'
61
- ]
61
+ ].concat((process.env.CHROME_ARGS || '').split(' '))
62
62
  },
63
63
  url: process.env.LAUNCH_URL,
64
64
  show: process.env.HEADLESS === 'false',
@@ -124,8 +124,8 @@ module.exports = {
124
124
  async teardown () {
125
125
  const { contexts } = global.inject()
126
126
  // we need to run this sequentially, less stress on the MW
127
- for (const ctx of contexts.filter(ctx => ctx.id > 100)) {
128
- if (ctx.id !== 10) await ctx.remove().catch(e => console.error(e.message))
127
+ for (const ctx of contexts.filter(ctx => /e2e-context-/.test(String(ctx.loginMappings)))) {
128
+ await ctx.remove().catch(e => console.error(e.message))
129
129
  }
130
130
  },
131
131
  reporter: 'mocha-multi',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-xchange/appsuite-codeceptjs",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "OX App Suite CodeceptJS Configuration and Helpers",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -14,14 +14,13 @@
14
14
  "@codeceptjs/helper": "^2.0.3",
15
15
  "@influxdata/influxdb-client": "^1.33.2",
16
16
  "@open-xchange/codecept-horizontal-scaler": "^0.1.7",
17
- "@playwright/test": "^1.42.0",
18
- "@types/node": "^20.11.22",
17
+ "@playwright/test": "1.44.0",
19
18
  "allure-codeceptjs": "^2.13.0",
20
19
  "chai": "^5.1.0",
21
20
  "chai-subset": "^1.6.0",
22
21
  "chalk": "^4.1.0",
23
22
  "chalk-table": "^1.0.2",
24
- "codeceptjs": "^3.5.14",
23
+ "codeceptjs": "3.6.2",
25
24
  "dotenv": "^16.4.4",
26
25
  "mocha": "^10.3.0",
27
26
  "mocha-junit-reporter": "^2.2.1",
@@ -29,11 +28,20 @@
29
28
  "moment": "^2.30.1",
30
29
  "moment-timezone": "^0.5.43",
31
30
  "p-retry": "^6.2.0",
32
- "playwright-core": "^1.42.0",
33
- "playwright": "^1.42.0",
34
- "short-uuid": "^4.2.2",
35
- "soap": "^1.0.0",
31
+ "playwright-core": "1.43.1",
32
+ "short-uuid": "^5.0.0",
33
+ "soap": "^1.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^20.11.22",
36
37
  "ts-node": "^10.9.2",
37
38
  "typescript": "^5.3.3"
39
+ },
40
+ "pnpm": {
41
+ "updateConfig": {
42
+ "ignoreDependencies": [
43
+ "chalk"
44
+ ]
45
+ }
38
46
  }
39
47
  }
@@ -26,6 +26,13 @@ const pRetry = import('p-retry').then(module => module.default)
26
26
  const cache = {}
27
27
  const baseURL = codecept.config.get().helpers.Playwright.url.replace(/\/$/, '')
28
28
 
29
+ /**
30
+ * Fetches data from a given URL with retry functionality.
31
+ * @param {string} url - The URL to fetch data from.
32
+ * @param {object} options - The options for the fetch request.
33
+ * @returns {Promise<object>} - A promise that resolves to an object containing the fetched data and the response.
34
+ * @throws {Error} - Throws an error if there is an HTTP request error.
35
+ */
29
36
  async function fetchWithRetry (url, options) {
30
37
  return (await pRetry)(async () => {
31
38
  const res = await fetch(url, options)
@@ -23,7 +23,7 @@ const users = require('../users/users')()
23
23
  const util = require('../util')
24
24
  const event = require('../event')
25
25
  const contextService = require('../soap/services/context')
26
- const utilService = require('../soap/services/util')
26
+ const crypto = require('node:crypto')
27
27
 
28
28
  class Context {
29
29
  constructor ({ ctxdata, admin, auth }) {
@@ -105,7 +105,7 @@ class Context {
105
105
  return contextService.change({ id: this.id, maxQuota })
106
106
  }
107
107
 
108
- static async create (ctx = { }, adminUser = { }, auth = util.admin()) {
108
+ static async create (ctx = { filestoreId: undefined, id: undefined }, adminUser = { }, auth = util.admin()) {
109
109
  adminUser = Object.assign({
110
110
  name: 'oxadmin',
111
111
  password: 'secret',
@@ -116,26 +116,25 @@ class Context {
116
116
  primaryEmail: `${adminUser.name || 'oxadmin'}@${util.mxDomain()}`
117
117
  }, adminUser)
118
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)
119
+ const loginMappings = [`e2e-context-${crypto.randomUUID()}`]
120
+ const newCtx = Object.assign({ maxQuota: -1, loginMappings }, ctx)
125
121
  event.emit(event.provisioning.context.create, newCtx, adminUser, auth)
126
122
  let data
127
-
128
123
  try {
129
124
  data = await contextService.create(newCtx)
130
- } catch (e) {
131
- newCtx.id = util.addJitter(newCtx.id)
132
- return this.create(newCtx)
125
+ } catch (error) {
126
+ // For mws that don't have autocontextid
127
+ if (error.message.includes('Mandatory fields in context not set: [id]')) {
128
+ newCtx.filestoreId = await util.getContextFilestorageId()
129
+ newCtx.id = util.addJitter(util.userContextId())
130
+ data = await contextService.create(newCtx)
131
+ } else throw error
133
132
  }
134
-
133
+ // Add loginMapping since we don't get the correct one from the mw
134
+ data.loginMappings.push(loginMappings[0])
135
135
  const context = new Context({ ctxdata: data, admin: adminUser, auth })
136
136
  created.push(context)
137
137
  // only provide defaults for fresh contexts
138
- await context.hasAccessCombination('all')
139
138
  event.emit(event.provisioning.context.created, context)
140
139
  return context
141
140
  }
@@ -68,20 +68,9 @@ class ResellerContext {
68
68
  const configMap = userAttributes.entries.find(entry => entry.key === 'config') || { key: 'config' }
69
69
  configMap.value = configMap.value || { entries: [] }
70
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) })
71
+ return resellerContextService
72
+ .change({ id: this.ctxdata.id, userAttributes })
73
+ .then(() => resellerContextService.get(this.ctxdata.id))
85
74
  }
86
75
 
87
76
  doesntHaveCapability (capability) {
@@ -169,7 +158,7 @@ class ResellerContext {
169
158
  }
170
159
  }
171
160
 
172
- static async create (ctx = { }, adminUser = { }, auth = util.admin(), numberOfUsers = Number(process.env.PROVISIONING_USERS || 10)) {
161
+ static async create (ctx = {}, adminUser = {}, auth = util.admin(), numberOfUsers = Number(process.env.PROVISIONING_USERS || 10)) {
173
162
  ctx = Object.assign({ id: util.userContextId(), maxQuota: -1 }, ctx)
174
163
  adminUser = Object.assign({
175
164
  name: `${ctx.name || ctx.id}_admin`
package/src/helper.js CHANGED
@@ -812,6 +812,18 @@ class AppSuiteHelper extends Helper {
812
812
  await helper.browserContext.grantPermissions(['clipboard-read'], { origin: config.url })
813
813
  return await page.evaluate(async () => { return navigator.clipboard.readText() })
814
814
  }
815
+
816
+ async createGenericFile (filename, size) {
817
+ const filePath = path.join(codecept_dir, 'media/files/generic', filename)
818
+ try {
819
+ await fs.access(filePath, fs.constants.F_OK)
820
+ } catch (error) {
821
+ if (error.code === 'ENOENT') {
822
+ const content = crypto.randomBytes(size)
823
+ await fs.writeFile(filePath, content)
824
+ }
825
+ }
826
+ }
815
827
  }
816
828
 
817
829
  module.exports = AppSuiteHelper
@@ -213,7 +213,7 @@ module.exports = {
213
213
  }, title)
214
214
  if (skipRefresh === true) return
215
215
  I.click('#io-ox-refresh-icon')
216
- I.waitForDetached('#io-ox-refresh-icon .fa-spin')
216
+ I.waitForDetached('#io-ox-refresh-icon .animate-spin')
217
217
  },
218
218
 
219
219
  async setDateTo (dateString) {
@@ -45,15 +45,15 @@ module.exports = {
45
45
  },
46
46
 
47
47
  /**
48
- * Set a browser selection.
49
- * @param {string} startText - The 'innerText' of the node that starts the selection.
50
- * @param {string} endText - The 'innerText' of the node that ends the selection.
51
- * @param {string} wrapperClass - The selection is searched inside the wrapper. Provide a class name ('.className') for it.
52
- * *
53
- * Using the recommended workaround by pupeteer(v20.30) to select text.
54
- * "dragging and selecting text is not possible using page.mouse"
55
- * https://pptr.dev/api/puppeteer.mouse#example-1
56
- */
48
+ * Set a browser selection.
49
+ * @param {string} startText - The 'innerText' of the node that starts the selection.
50
+ * @param {string} endText - The 'innerText' of the node that ends the selection.
51
+ * @param {string} wrapperClass - The selection is searched inside the wrapper. Provide a class name ('.className') for it.
52
+ * *
53
+ * Using the recommended workaround by pupeteer(v20.30) to select text.
54
+ * "dragging and selecting text is not possible using page.mouse"
55
+ * https://pptr.dev/api/puppeteer.mouse#example-1
56
+ */
57
57
  async setBrowserSelection (startText, endText, wrapperClass) {
58
58
  await I.executeScript(({ startText, endText, wrapperClass }) => {
59
59
  const selection = window.getSelection()
@@ -23,13 +23,13 @@ const { I } = inject()
23
23
  module.exports = {
24
24
 
25
25
  // attr: [startDate, endDate]
26
- setDate (attr, value) {
27
- const date = value.format('YYYY-MM-DDTHH:mm')
28
- I.executeScript(({ attr, date }) => {
26
+ async setDate (attr, value) {
27
+ const date = value.toFormat("yyyy-MM-dd'T'HH:mm")
28
+ await I.executeScript(async ({ attr, date }) => {
29
29
  const fieldset = document.querySelector(`fieldset[data-attribute="${attr}"]`)
30
30
  const input = fieldset.querySelector('input')
31
31
  input.value = date
32
- input.dispatchEvent(new Event('input'))
32
+ input.dispatchEvent(new Event('mouseout'))
33
33
  }, { attr, date })
34
34
  },
35
35
  async newAppointment () {
@@ -19,7 +19,6 @@
19
19
  */
20
20
 
21
21
  const { createClientAsync, logSoapError } = require('../soap.js')
22
- const { getFilestorageId } = require('./util.js')
23
22
 
24
23
  const OXContextService = createClientAsync('OXContextService').then(client => client)
25
24
 
@@ -43,18 +42,27 @@ async function remove (id) {
43
42
  /**
44
43
  * This function creates a new context.
45
44
  * @param {Object} ctx The context to create.
45
+ * @param {Object} adminUser The admin user of the context.
46
+ * Defaults to:
47
+ * ```JSON
48
+ * {
49
+ name: 'oxadmin',
50
+ password: 'secret',
51
+ display_name: 'context admin',
52
+ sur_name: 'admin',
53
+ given_name: 'context',
54
+ email1: `oxadmin@${process.env.MX_DOMAIN}`,
55
+ primaryEmail: `oxadmin@${process.env.MX_DOMAIN}`
56
+ }
57
+ All properties are needed and will be inserted if not provided.
58
+ ```
46
59
  * @returns {Promise<Object>} The created context.
47
60
  */
48
- async function create (ctx = {}) {
61
+ async function create (ctx = {}, adminUser = {}) {
49
62
  return await (await OXContextService)
50
63
  .createAsync({
51
- ctx: {
52
- id: process.env.CONTEXT_ID || Math.floor(Date.now() / 1000),
53
- maxQuota: 1000,
54
- filestoreId: String(await getFilestorageId()),
55
- ...ctx
56
- },
57
- admin_user: {
64
+ ctx,
65
+ admin_user: Object.assign({
58
66
  name: 'oxadmin',
59
67
  password: 'secret',
60
68
  display_name: 'context admin',
@@ -62,13 +70,16 @@ async function create (ctx = {}) {
62
70
  given_name: 'context',
63
71
  email1: `oxadmin@${process.env.MX_DOMAIN}`,
64
72
  primaryEmail: `oxadmin@${process.env.MX_DOMAIN}`
65
- }
73
+ }, adminUser)
66
74
  })
67
75
  .then(async context => {
68
76
  await changeModuleAccessByName(context.id, 'all')
69
77
  return context
70
78
  })
71
- // .catch(logSoapError)
79
+ .catch(e => {
80
+ logSoapError(e)
81
+ throw e
82
+ })
72
83
  }
73
84
 
74
85
  /**
@@ -116,6 +127,17 @@ async function changeModuleAccess (id, moduleAccess) {
116
127
  return await (await OXContextService).changeModuleAccessAsync({ ctx: { id }, access: { ...currentAccess, ...moduleAccess } })
117
128
  }
118
129
 
130
+ /**
131
+ * Search for contexts.
132
+ * @param {String} searchPattern The pattern to search for
133
+ * @param {Object} options Additional options
134
+ * @param {Boolean} options.excludeDisabled Exclude disabled contexts from the search results (default: true)
135
+ * @returns {Promise<Array<Object>>} The list of contexts that match the search pattern.
136
+ */
137
+ async function list (searchPattern, { excludeDisabled = true }) {
138
+ return await (await OXContextService).listAsync({ search_pattern: searchPattern, exclude_disabled: excludeDisabled })
139
+ }
140
+
119
141
  /**
120
142
  * This function retrieves the context with the specified ID.
121
143
  * @param {number} id The ID of the context to retrieve.
@@ -143,5 +165,6 @@ module.exports = {
143
165
  getModuleAccess,
144
166
  changeModuleAccess,
145
167
  get,
168
+ list,
146
169
  change
147
170
  }
package/src/soap/soap.js CHANGED
@@ -85,7 +85,9 @@ function shouldAbortRetry (error) {
85
85
  /already exists in this context/,
86
86
  /Shared Domain already exists/,
87
87
  /Shared Domain already in use/,
88
- /No such user/
88
+ /No such user/,
89
+ /Mandatory fields in context not set/,
90
+ /A mapping with login info .* already exists/
89
91
  ]
90
92
  const blockedExceptions = [
91
93
  'ContextExistsException',
@@ -154,7 +156,7 @@ async function createClientAsync (type) {
154
156
  throw new Error(soapError?.faultstring || e.message)
155
157
  })
156
158
  performance.mark(endMark)
157
- performance.measure(` ⏱ SOAP: ${type} -> ${prop}`, startMark, endMark)
159
+ performance.measure(` ⏱ SOAP: ${type} -> ${String(prop)}`, startMark, endMark)
158
160
  // Return only the first result from the SOAP method call, as we don't need the SOAP envelope and other stuff.
159
161
  if (!result || !result[0]) return
160
162
  return result[0]?.return
@@ -121,7 +121,6 @@ class User {
121
121
 
122
122
  static getRandom ({ name = 'test.user', password = util.getDefaultUserPassword(), sur_name = '', given_name = 'User' } = {}) {
123
123
  const id = short.generate().slice(0, 9).toLowerCase()
124
- // eslint-disable-next-line camelcase
125
124
  if (!sur_name) sur_name = id
126
125
  if (name === 'test.user') name = `${name}-${id}`
127
126
  const domain = util.mxDomain()
package/src/util.js CHANGED
@@ -20,6 +20,7 @@
20
20
 
21
21
  const codecept = require('codeceptjs')
22
22
  const url = require('node:url')
23
+ const { getFilestorageId } = require('./soap/services/util')
23
24
 
24
25
  module.exports = {
25
26
 
@@ -57,7 +58,7 @@ module.exports = {
57
58
 
58
59
  userContextId () {
59
60
  const ox = codecept.config.get().helpers.AppSuite
60
- return typeof ox.contextId === 'undefined' ? 10 : ox.contextId
61
+ return typeof ox.contextId === 'undefined' ? Math.floor(Date.now() / 1000) : ox.contextId
61
62
  },
62
63
 
63
64
  admin () {
@@ -84,6 +85,11 @@ module.exports = {
84
85
  return ox.defaultUserPassword || 'secret'
85
86
  },
86
87
 
88
+ async getContextFilestorageId () {
89
+ const ox = codecept.config.get().helpers.AppSuite
90
+ return ox.filestoreId || String(await getFilestorageId())
91
+ },
92
+
87
93
  PropagatedError: class PropagatedError extends Error {
88
94
  constructor (error) {
89
95
  super(error.message)
@@ -97,7 +103,7 @@ module.exports = {
97
103
  },
98
104
 
99
105
  addJitter (id) {
100
- const [JITTER_MIN, JITTER_MAX] = [1000, 2000]
106
+ const [JITTER_MIN, JITTER_MAX] = [1000, 5000]
101
107
  return Math.trunc(id) + Math.floor(Math.random() * (JITTER_MAX - JITTER_MIN + 1) + JITTER_MIN)
102
108
  }
103
109