commands.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. /**
  2. * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
  3. * SPDX-License-Identifier: AGPL-3.0-or-later
  4. */
  5. // eslint-disable-next-line n/no-extraneous-import
  6. import axios, { type AxiosResponse } from 'axios'
  7. import { addCommands, User } from '@nextcloud/cypress'
  8. import { basename } from 'path'
  9. // Add custom commands
  10. import '@testing-library/cypress/add-commands'
  11. import 'cypress-if'
  12. import 'cypress-wait-until'
  13. addCommands()
  14. // Register this file's custom commands types
  15. declare global {
  16. // eslint-disable-next-line @typescript-eslint/no-namespace
  17. namespace Cypress {
  18. // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
  19. interface Chainable<Subject = any> {
  20. /**
  21. * Enable or disable a given user
  22. */
  23. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  24. enableUser(user: User, enable?: boolean): Cypress.Chainable<Cypress.Response<any>>,
  25. /**
  26. * Upload a file from the fixtures folder to a given user storage.
  27. * **Warning**: Using this function will reset the previous session
  28. */
  29. uploadFile(user: User, fixture?: string, mimeType?: string, target?: string): Cypress.Chainable<void>,
  30. /**
  31. * Upload a raw content to a given user storage.
  32. * **Warning**: Using this function will reset the previous session
  33. */
  34. uploadContent(user: User, content: Blob, mimeType: string, target: string, mtime?: number): Cypress.Chainable<AxiosResponse>,
  35. /**
  36. * Create a new directory
  37. * **Warning**: Using this function will reset the previous session
  38. */
  39. mkdir(user: User, target: string): Cypress.Chainable<void>,
  40. /**
  41. * Set a file as favorite (or remove from favorite)
  42. */
  43. setFileAsFavorite(user: User, target: string, favorite?: boolean): Cypress.Chainable<void>,
  44. /**
  45. * Reset the admin theming entirely.
  46. * **Warning**: Using this function will reset the previous session
  47. */
  48. resetAdminTheming(): Cypress.Chainable<void>,
  49. /**
  50. * Reset the user theming settings.
  51. * If provided, will clear session and login as the given user.
  52. * **Warning**: Providing a user will reset the previous session.
  53. */
  54. resetUserTheming(user?: User): Cypress.Chainable<void>,
  55. /**
  56. * Run an occ command in the docker container.
  57. */
  58. runOccCommand(command: string, options?: Partial<Cypress.ExecOptions>): Cypress.Chainable<Cypress.Exec>,
  59. userFileExists(user: string, path: string): Cypress.Chainable<number>
  60. /**
  61. * Create a snapshot of the current database
  62. */
  63. backupDB(): Cypress.Chainable<string>,
  64. /**
  65. * Restore a snapshot of the database
  66. * Default is the post-setup state
  67. */
  68. restoreDB(snapshot?: string): Cypress.Chainable
  69. backupData(users?: string[]): Cypress.Chainable<string>
  70. restoreData(snapshot?: string): Cypress.Chainable
  71. }
  72. }
  73. }
  74. const url = (Cypress.config('baseUrl') || '').replace(/\/index.php\/?$/g, '')
  75. Cypress.env('baseUrl', url)
  76. /**
  77. * Enable or disable a user
  78. * TODO: standardize in @nextcloud/cypress
  79. *
  80. * @param {User} user the user to dis- / enable
  81. * @param {boolean} enable True if the user should be enable, false to disable
  82. */
  83. Cypress.Commands.add('enableUser', (user: User, enable = true) => {
  84. const url = `${Cypress.config('baseUrl')}/ocs/v2.php/cloud/users/${user.userId}/${enable ? 'enable' : 'disable'}`.replace('index.php/', '')
  85. return cy.request({
  86. method: 'PUT',
  87. url,
  88. form: true,
  89. auth: {
  90. user: 'admin',
  91. password: 'admin',
  92. },
  93. headers: {
  94. 'OCS-ApiRequest': 'true',
  95. 'Content-Type': 'application/x-www-form-urlencoded',
  96. },
  97. }).then((response) => {
  98. cy.log(`Enabled user ${user}`, response.status)
  99. return cy.wrap(response)
  100. })
  101. })
  102. /**
  103. * cy.uploadedFile - uploads a file from the fixtures folder
  104. * TODO: standardize in @nextcloud/cypress
  105. *
  106. * @param {User} user the owner of the file, e.g. admin
  107. * @param {string} fixture the fixture file name, e.g. image1.jpg
  108. * @param {string} mimeType e.g. image/png
  109. * @param {string} [target] the target of the file relative to the user root
  110. */
  111. Cypress.Commands.add('uploadFile', (user, fixture = 'image.jpg', mimeType = 'image/jpeg', target = `/${fixture}`) => {
  112. // get fixture
  113. return cy.fixture(fixture, 'base64').then(async file => {
  114. // convert the base64 string to a blob
  115. const blob = Cypress.Blob.base64StringToBlob(file, mimeType)
  116. cy.uploadContent(user, blob, mimeType, target)
  117. })
  118. })
  119. Cypress.Commands.add('setFileAsFavorite', (user: User, target: string, favorite = true) => {
  120. // eslint-disable-next-line cypress/unsafe-to-chain-command
  121. cy.clearAllCookies()
  122. .then(async () => {
  123. try {
  124. const rootPath = `${Cypress.env('baseUrl')}/remote.php/dav/files/${encodeURIComponent(user.userId)}`
  125. const filePath = target.split('/').map(encodeURIComponent).join('/')
  126. const response = await axios({
  127. url: `${rootPath}${filePath}`,
  128. method: 'PROPPATCH',
  129. auth: {
  130. username: user.userId,
  131. password: user.password,
  132. },
  133. headers: {
  134. 'Content-Type': 'application/xml',
  135. },
  136. data: `<?xml version="1.0"?>
  137. <d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
  138. <d:set>
  139. <d:prop>
  140. <oc:favorite>${favorite ? 1 : 0}</oc:favorite>
  141. </d:prop>
  142. </d:set>
  143. </d:propertyupdate>`,
  144. })
  145. cy.log(`Created directory ${target}`, response)
  146. } catch (error) {
  147. cy.log('error', error)
  148. throw new Error('Unable to process fixture')
  149. }
  150. })
  151. })
  152. Cypress.Commands.add('mkdir', (user: User, target: string) => {
  153. // eslint-disable-next-line cypress/unsafe-to-chain-command
  154. cy.clearCookies()
  155. .then(async () => {
  156. try {
  157. const rootPath = `${Cypress.env('baseUrl')}/remote.php/dav/files/${encodeURIComponent(user.userId)}`
  158. const filePath = target.split('/').map(encodeURIComponent).join('/')
  159. const response = await axios({
  160. url: `${rootPath}${filePath}`,
  161. method: 'MKCOL',
  162. auth: {
  163. username: user.userId,
  164. password: user.password,
  165. },
  166. })
  167. cy.log(`Created directory ${target}`, response)
  168. } catch (error) {
  169. cy.log('error', error)
  170. throw new Error('Unable to create directory')
  171. }
  172. })
  173. })
  174. /**
  175. * cy.uploadedContent - uploads a raw content
  176. * TODO: standardize in @nextcloud/cypress
  177. *
  178. * @param {User} user the owner of the file, e.g. admin
  179. * @param {Blob} blob the content to upload
  180. * @param {string} mimeType e.g. image/png
  181. * @param {string} target the target of the file relative to the user root
  182. */
  183. Cypress.Commands.add('uploadContent', (user: User, blob: Blob, mimeType: string, target: string, mtime?: number) => {
  184. cy.clearCookies()
  185. return cy.then(async () => {
  186. const fileName = basename(target)
  187. // Process paths
  188. const rootPath = `${Cypress.env('baseUrl')}/remote.php/dav/files/${encodeURIComponent(user.userId)}`
  189. const filePath = target.split('/').map(encodeURIComponent).join('/')
  190. try {
  191. const file = new File([blob], fileName, { type: mimeType })
  192. const response = await axios({
  193. url: `${rootPath}${filePath}`,
  194. method: 'PUT',
  195. data: file,
  196. headers: {
  197. 'Content-Type': mimeType,
  198. 'X-OC-MTime': mtime ? `${mtime}` : undefined,
  199. },
  200. auth: {
  201. username: user.userId,
  202. password: user.password,
  203. },
  204. })
  205. cy.log(`Uploaded content as ${fileName}`, response)
  206. return response
  207. } catch (error) {
  208. cy.log('error', error)
  209. throw new Error('Unable to process fixture')
  210. }
  211. })
  212. })
  213. /**
  214. * Reset the admin theming entirely
  215. */
  216. Cypress.Commands.add('resetAdminTheming', () => {
  217. const admin = new User('admin', 'admin')
  218. cy.clearCookies()
  219. cy.login(admin)
  220. // Clear all settings
  221. cy.request('/csrftoken').then(({ body }) => {
  222. const requestToken = body.token
  223. axios({
  224. method: 'POST',
  225. url: '/index.php/apps/theming/ajax/undoAllChanges',
  226. headers: {
  227. requesttoken: requestToken,
  228. },
  229. })
  230. })
  231. // Clear admin session
  232. cy.clearCookies()
  233. })
  234. /**
  235. * Reset the current or provided user theming settings
  236. * It does not reset the theme config as it is enforced in the
  237. * server config for cypress testing.
  238. */
  239. Cypress.Commands.add('resetUserTheming', (user?: User) => {
  240. if (user) {
  241. cy.clearCookies()
  242. cy.login(user)
  243. }
  244. // Reset background config
  245. cy.request('/csrftoken').then(({ body }) => {
  246. const requestToken = body.token
  247. cy.request({
  248. method: 'POST',
  249. url: '/apps/theming/background/default',
  250. headers: {
  251. requesttoken: requestToken,
  252. },
  253. })
  254. })
  255. if (user) {
  256. // Clear current session
  257. cy.clearCookies()
  258. }
  259. })
  260. Cypress.Commands.add('runOccCommand', (command: string, options?: Partial<Cypress.ExecOptions>) => {
  261. const env = Object.entries(options?.env ?? {}).map(([name, value]) => `-e '${name}=${value}'`).join(' ')
  262. return cy.exec(`docker exec --user www-data ${env} nextcloud-cypress-tests-server php ./occ ${command}`, options)
  263. })
  264. Cypress.Commands.add('userFileExists', (user: string, path: string) => {
  265. user.replaceAll('"', '\\"')
  266. path.replaceAll('"', '\\"').replaceAll(/^\/+/gm, '')
  267. return cy.exec(`docker exec --user www-data nextcloud-cypress-tests-server stat --printf="%s" "data/${user}/files/${path}"`, { failOnNonZeroExit: true })
  268. .then((exec) => Number.parseInt(exec.stdout || '0'))
  269. })
  270. Cypress.Commands.add('backupDB', (): Cypress.Chainable<string> => {
  271. const randomString = Math.random().toString(36).substring(7)
  272. cy.exec(`docker exec --user www-data nextcloud-cypress-tests-server cp /var/www/html/data/owncloud.db /var/www/html/data/owncloud.db-${randomString}`)
  273. cy.log(`Created snapshot ${randomString}`)
  274. return cy.wrap(randomString)
  275. })
  276. Cypress.Commands.add('restoreDB', (snapshot: string = 'init') => {
  277. cy.exec(`docker exec --user www-data nextcloud-cypress-tests-server cp /var/www/html/data/owncloud.db-${snapshot} /var/www/html/data/owncloud.db`)
  278. cy.log(`Restored snapshot ${snapshot}`)
  279. })
  280. Cypress.Commands.add('backupData', (users: string[] = ['admin']) => {
  281. const snapshot = Math.random().toString(36).substring(7)
  282. const toBackup = users.map((user) => `'${user.replaceAll('\\', '').replaceAll('\'', '\\\'')}'`).join(' ')
  283. cy.exec(`docker exec --user www-data rm /var/www/html/data/data-${snapshot}.tar`, { failOnNonZeroExit: false })
  284. cy.exec(`docker exec --user www-data --workdir /var/www/html/data nextcloud-cypress-tests-server tar cf /var/www/html/data/data-${snapshot}.tar ${toBackup}`)
  285. return cy.wrap(snapshot as string)
  286. })
  287. Cypress.Commands.add('restoreData', (snapshot?: string) => {
  288. snapshot = snapshot ?? 'init'
  289. snapshot.replaceAll('\\', '').replaceAll('"', '\\"')
  290. cy.exec(`docker exec --user www-data --workdir /var/www/html/data nextcloud-cypress-tests-server rm -vfr $(tar --exclude='*/*' -tf '/var/www/html/data/data-${snapshot}.tar')`)
  291. cy.exec(`docker exec --user www-data --workdir /var/www/html/data nextcloud-cypress-tests-server tar -xf '/var/www/html/data/data-${snapshot}.tar'`)
  292. })