test: Add end-to-end tests for new public share Vue UI

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
pull/45652/head
Ferdinand Thiessen 3 months ago
parent be884eeaec
commit 4a90d5328c
No known key found for this signature in database
GPG Key ID: 45FAE7268762B400
  1. 6
      __tests__/mock-window.js
  2. 3
      apps/files/src/actions/editLocallyAction.spec.ts
  3. 2
      apps/files/src/actions/editLocallyAction.ts
  4. 13
      cypress.config.ts
  5. 6
      cypress/dockerNode.ts
  6. 10
      cypress/e2e/files/FilesUtils.ts
  7. 11
      cypress/e2e/files_sharing/FilesSharingUtils.ts
  8. 44
      cypress/e2e/files_sharing/file-request.cy.ts
  9. 49
      cypress/e2e/files_sharing/public-share/copy-move-files.cy.ts
  10. 141
      cypress/e2e/files_sharing/public-share/download-files.cy.ts
  11. 134
      cypress/e2e/files_sharing/public-share/header-menu.cy.ts
  12. 32
      cypress/e2e/files_sharing/public-share/rename-files.cy.ts
  13. 119
      cypress/e2e/files_sharing/public-share/setup-public-share.ts
  14. 169
      cypress/e2e/files_sharing/public-share/view_file-drop.cy.ts
  15. 104
      cypress/e2e/files_sharing/public-share/view_view-only-no-download.cy.ts
  16. 107
      cypress/e2e/files_sharing/public-share/view_view-only.cy.ts
  17. 36
      cypress/support/commands.ts
  18. 4
      cypress/support/utils/assertions.ts
  19. 2
      package-lock.json
  20. 2
      package.json
  21. 5
      vitest.config.ts

@ -2,12 +2,8 @@
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { beforeEach } from 'vitest'
window.OC = { ...window.OC }
window.OCA = { ...window.OCA }
window.OCP = { ...window.OCP }
beforeEach(() => {
window.location = new URL('http://nextcloud.local')
})
window._oc_webroot = ''

@ -120,6 +120,7 @@ describe('Edit locally action execute tests', () => {
data: { ocs: { data: { token: 'foobar' } } },
}))
const showError = vi.spyOn(nextcloudDialogs, 'showError')
const windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
const file = new File({
id: 1,
@ -138,7 +139,7 @@ describe('Edit locally action execute tests', () => {
expect(axios.post).toBeCalledTimes(1)
expect(axios.post).toBeCalledWith('http://nextcloud.local/ocs/v2.php/apps/files/api/v1/openlocaleditor?format=json', { path: '/foobar.txt' })
expect(showError).toBeCalledTimes(0)
expect(window.location.href).toBe('nc://open/test@nextcloud.local/foobar.txt?token=foobar')
expect(windowOpenSpy).toBeCalledWith('nc://open/test@nextcloud.local/foobar.txt?token=foobar', '_self')
})
test('Edit locally fails and shows error', async () => {

@ -73,7 +73,7 @@ const openLocalClient = async function(path: string) {
let url = `nc://open/${uid}@` + window.location.host + encodePath(path)
url += '?token=' + result.data.ocs.data.token
window.location.href = url
window.open(url, '_self')
} catch (error) {
showError(t('files', 'Failed to redirect to client'))
}

@ -66,6 +66,19 @@ export default defineConfig({
on('task', { removeDirectory })
// This allows to store global data (e.g. the name of a snapshot)
// because Cypress.env() and other options are local to the current spec file.
const data = {}
on('task', {
setVariable({ key, value }) {
data[key] = value
return null
},
getVariable({ key }) {
return data[key] ?? null
},
})
// Disable spell checking to prevent rendering differences
on('before:browser:launch', (browser, launchOptions) => {
if (browser.family === 'chromium' && browser.name !== 'electron') {

@ -147,6 +147,8 @@ export const configureNextcloud = async function() {
// Saving DB state
console.log('├─ Creating init DB snapshot...')
await runExec(container, ['cp', '/var/www/html/data/owncloud.db', '/var/www/html/data/owncloud.db-init'], true)
console.log('├─ Creating init data backup...')
await runExec(container, ['tar', 'cf', 'data-init.tar', 'admin'], true, undefined, '/var/www/html/data')
console.log('└─ Nextcloud is now ready to use 🎉')
}
@ -277,9 +279,11 @@ const runExec = async function(
command: string[],
verbose = false,
user = 'www-data',
workdir?: string,
): Promise<string> {
const exec = await container.exec({
Cmd: command,
WorkingDir: workdir,
AttachStdout: true,
AttachStderr: true,
User: user,
@ -296,7 +300,7 @@ const runExec = async function(
stream.on('data', str => {
str = str.trim()
// Remove non printable characters
.replace(/[^\x20-\x7E]+/g, '')
.replace(/[^\x0A\x0D\x20-\x7E]+/g, '')
// Remove non alphanumeric leading characters
.replace(/^[^a-z]/gi, '')
output += str

@ -9,8 +9,8 @@ export const getRowForFile = (filename: string) => cy.get(`[data-cy-files-list-r
export const getActionsForFileId = (fileid: number) => getRowForFileId(fileid).find('[data-cy-files-list-row-actions]')
export const getActionsForFile = (filename: string) => getRowForFile(filename).find('[data-cy-files-list-row-actions]')
export const getActionButtonForFileId = (fileid: number) => getActionsForFileId(fileid).find('button[aria-label="Actions"]')
export const getActionButtonForFile = (filename: string) => getActionsForFile(filename).find('button[aria-label="Actions"]')
export const getActionButtonForFileId = (fileid: number) => getActionsForFileId(fileid).findByRole('button', { name: 'Actions' })
export const getActionButtonForFile = (filename: string) => getActionsForFile(filename).findByRole('button', { name: 'Actions' })
export const triggerActionForFileId = (fileid: number, actionId: string) => {
getActionButtonForFileId(fileid).click()
@ -34,7 +34,7 @@ export const moveFile = (fileName: string, dirPath: string) => {
cy.get('.file-picker').within(() => {
// intercept the copy so we can wait for it
cy.intercept('MOVE', /\/remote.php\/dav\/files\//).as('moveFile')
cy.intercept('MOVE', /\/(remote|public)\.php\/dav\/files\//).as('moveFile')
if (dirPath === '/') {
// select home folder
@ -65,7 +65,7 @@ export const copyFile = (fileName: string, dirPath: string) => {
cy.get('.file-picker').within(() => {
// intercept the copy so we can wait for it
cy.intercept('COPY', /\/remote.php\/dav\/files\//).as('copyFile')
cy.intercept('COPY', /\/(remote|public)\.php\/dav\/files\//).as('copyFile')
if (dirPath === '/') {
// select home folder
@ -95,7 +95,7 @@ export const renameFile = (fileName: string, newFileName: string) => {
triggerActionForFile(fileName, 'rename')
// intercept the move so we can wait for it
cy.intercept('MOVE', /\/remote.php\/dav\/files\//).as('moveFile')
cy.intercept('MOVE', /\/(remote|public)\.php\/dav\/files\//).as('moveFile')
getRowForFile(fileName).find('[data-cy-files-list-row-name] input').clear()
getRowForFile(fileName).find('[data-cy-files-list-row-name] input').type(`${newFileName}{enter}`)

@ -170,14 +170,3 @@ export const createFileRequest = (path: string, options: FileRequestOptions = {}
// Close
cy.get('[data-cy-file-request-dialog-controls="finish"]').click()
}
export const enterGuestName = (name: string) => {
cy.get('[data-cy-public-auth-prompt-dialog]').should('be.visible')
cy.get('[data-cy-public-auth-prompt-dialog-name]').should('be.visible')
cy.get('[data-cy-public-auth-prompt-dialog-submit]').should('be.visible')
cy.get('[data-cy-public-auth-prompt-dialog-name]').type(`{selectall}${name}`)
cy.get('[data-cy-public-auth-prompt-dialog-submit]').click()
cy.get('[data-cy-public-auth-prompt-dialog]').should('not.exist')
}

@ -5,12 +5,31 @@
import type { User } from '@nextcloud/cypress'
import { createFolder, getRowForFile, navigateToFolder } from '../files/FilesUtils'
import { createFileRequest, enterGuestName } from './FilesSharingUtils'
import { createFileRequest } from './FilesSharingUtils'
const enterGuestName = (name: string) => {
cy.findByRole('dialog', { name: /Upload files to/ })
.should('be.visible')
.within(() => {
cy.findByRole('textbox', { name: 'Nickname' })
.should('be.visible')
cy.findByRole('textbox', { name: 'Nickname' })
.type(`{selectall}${name}`)
cy.findByRole('button', { name: 'Submit name' })
.should('be.visible')
.click()
})
cy.findByRole('dialog', { name: /Upload files to/ })
.should('not.exist')
}
describe('Files', { testIsolation: true }, () => {
const folderName = 'test-folder'
let user: User
let url = ''
let folderName = 'test-folder'
it('Login with a user and create a file request', () => {
cy.createRandomUser().then((_user) => {
@ -33,19 +52,22 @@ describe('Files', { testIsolation: true }, () => {
enterGuestName('Guest')
// Check various elements on the page
cy.get('#public-upload .emptycontent').should('be.visible')
cy.get('#public-upload h2').contains(`Upload files to ${folderName}`)
cy.get('#public-upload input[type="file"]').as('fileInput').should('exist')
cy.contains(`Upload files to ${folderName}`)
.should('be.visible')
cy.findByRole('button', { name: 'Upload' })
.should('be.visible')
cy.intercept('PUT', '/public.php/dav/files/*/*').as('uploadFile')
// Upload a file
cy.get('@fileInput').selectFile({
contents: Cypress.Buffer.from('abcdef'),
fileName: 'file.txt',
mimeType: 'text/plain',
lastModified: Date.now(),
}, { force: true })
cy.get('[data-cy-files-sharing-file-drop] input[type="file"]')
.should('exist')
.selectFile({
contents: Cypress.Buffer.from('abcdef'),
fileName: 'file.txt',
mimeType: 'text/plain',
lastModified: Date.now(),
}, { force: true })
cy.wait('@uploadFile').its('response.statusCode').should('eq', 201)
})

@ -0,0 +1,49 @@
/*!
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { copyFile, getRowForFile, moveFile, navigateToFolder } from '../../files/FilesUtils.ts'
import { getShareUrl, setupPublicShare } from './setup-public-share.ts'
describe('files_sharing: Public share - copy and move files', { testIsolation: true }, () => {
beforeEach(() => {
setupPublicShare()
.then(() => cy.logout())
.then(() => cy.visit(getShareUrl()))
})
it('Can copy a file to new folder', () => {
getRowForFile('foo.txt').should('be.visible')
getRowForFile('subfolder').should('be.visible')
copyFile('foo.txt', 'subfolder')
// still visible
getRowForFile('foo.txt').should('be.visible')
navigateToFolder('subfolder')
cy.url().should('contain', 'dir=/subfolder')
getRowForFile('foo.txt').should('be.visible')
getRowForFile('bar.txt').should('be.visible')
getRowForFile('subfolder').should('not.exist')
})
it('Can move a file to new folder', () => {
getRowForFile('foo.txt').should('be.visible')
getRowForFile('subfolder').should('be.visible')
moveFile('foo.txt', 'subfolder')
// wait until visible again
getRowForFile('subfolder').should('be.visible')
// file should be moved -> not exist anymore
getRowForFile('foo.txt').should('not.exist')
navigateToFolder('subfolder')
cy.url().should('contain', 'dir=/subfolder')
getRowForFile('foo.txt').should('be.visible')
getRowForFile('subfolder').should('not.exist')
})
})

@ -0,0 +1,141 @@
/*!
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
// @ts-expect-error The package is currently broken - but works...
import { deleteDownloadsFolderBeforeEach } from 'cypress-delete-downloads-folder'
import { zipFileContains } from '../../../support/utils/assertions.ts'
import { getRowForFile, triggerActionForFile } from '../../files/FilesUtils.ts'
import { getShareUrl, setupPublicShare } from './setup-public-share.ts'
describe('files_sharing: Public share - downloading files', { testIsolation: true }, () => {
const shareName = 'shared'
before(() => setupPublicShare())
deleteDownloadsFolderBeforeEach()
beforeEach(() => {
cy.logout()
cy.visit(getShareUrl())
})
it('Can download all files', () => {
getRowForFile('foo.txt').should('be.visible')
cy.get('[data-cy-files-list]').within(() => {
cy.findByRole('checkbox', { name: /Toggle selection for all files/i })
.should('exist')
.check({ force: true })
// see that two files are selected
cy.contains('2 selected').should('be.visible')
// click download
cy.findByRole('button', { name: 'Download (selected)' })
.should('be.visible')
.click()
// check a file is downloaded
const downloadsFolder = Cypress.config('downloadsFolder')
cy.readFile(`${downloadsFolder}/${shareName}.zip`, null, { timeout: 15000 })
.should('exist')
.and('have.length.gt', 30)
// Check all files are included
.and(zipFileContains([
'foo.txt',
'subfolder/',
'subfolder/bar.txt',
]))
})
})
it('Can download selected files', () => {
getRowForFile('subfolder')
.should('be.visible')
cy.get('[data-cy-files-list]').within(() => {
getRowForFile('subfolder')
.findByRole('checkbox')
.check({ force: true })
// see that two files are selected
cy.contains('1 selected').should('be.visible')
// click download
cy.findByRole('button', { name: 'Download (selected)' })
.should('be.visible')
.click()
// check a file is downloaded
const downloadsFolder = Cypress.config('downloadsFolder')
cy.readFile(`${downloadsFolder}/subfolder.zip`, null, { timeout: 15000 })
.should('exist')
.and('have.length.gt', 30)
// Check all files are included
.and(zipFileContains([
'subfolder/',
'subfolder/bar.txt',
]))
})
})
it('Can download folder by action', () => {
getRowForFile('subfolder')
.should('be.visible')
cy.get('[data-cy-files-list]').within(() => {
triggerActionForFile('subfolder', 'download')
// check a file is downloaded
const downloadsFolder = Cypress.config('downloadsFolder')
cy.readFile(`${downloadsFolder}/subfolder.zip`, null, { timeout: 15000 })
.should('exist')
.and('have.length.gt', 30)
// Check all files are included
.and(zipFileContains([
'subfolder/',
'subfolder/bar.txt',
]))
})
})
it('Can download file by action', () => {
getRowForFile('foo.txt')
.should('be.visible')
cy.get('[data-cy-files-list]').within(() => {
triggerActionForFile('foo.txt', 'download')
// check a file is downloaded
const downloadsFolder = Cypress.config('downloadsFolder')
cy.readFile(`${downloadsFolder}/foo.txt`, 'utf-8', { timeout: 15000 })
.should('exist')
.and('have.length.gt', 5)
.and('contain', '<content>foo</content>')
})
})
it('Can download file by selection', () => {
getRowForFile('foo.txt')
.should('be.visible')
cy.get('[data-cy-files-list]').within(() => {
getRowForFile('foo.txt')
.findByRole('checkbox')
.check({ force: true })
cy.findByRole('button', { name: 'Download (selected)' })
.click()
// check a file is downloaded
const downloadsFolder = Cypress.config('downloadsFolder')
cy.readFile(`${downloadsFolder}/foo.txt`, 'utf-8', { timeout: 15000 })
.should('exist')
.and('have.length.gt', 5)
.and('contain', '<content>foo</content>')
})
})
})

@ -2,67 +2,39 @@
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { haveValidity, zipFileContains } from '../../support/utils/assertions.ts'
import { openSharingPanel } from './FilesSharingUtils.ts'
// @ts-expect-error The package is currently broken - but works...
import { deleteDownloadsFolderBeforeEach } from 'cypress-delete-downloads-folder'
import { haveValidity, zipFileContains } from '../../../support/utils/assertions.ts'
import { getShareUrl, setupPublicShare } from './setup-public-share.ts'
/**
* This tests ensures that on public shares the header actions menu correctly works
*/
describe('files_sharing: Public share - header actions menu', { testIsolation: true }, () => {
let shareUrl: string
const shareName = 'to be shared'
before(() => {
cy.createRandomUser().then(($user) => {
cy.mkdir($user, `/${shareName}`)
cy.mkdir($user, `/${shareName}/subfolder`)
cy.uploadContent($user, new Blob([]), 'text/plain', `/${shareName}/foo.txt`)
cy.uploadContent($user, new Blob([]), 'text/plain', `/${shareName}/subfolder/bar.txt`)
cy.login($user)
// open the files app
cy.visit('/apps/files')
// open the sidebar
openSharingPanel(shareName)
// create the share
cy.intercept('POST', '**/ocs/v2.php/apps/files_sharing/api/v1/shares').as('createShare')
cy.findByRole('button', { name: 'Create a new share link' })
.click()
// extract the link
cy.wait('@createShare').should(({ response }) => {
const { ocs } = response?.body ?? {}
shareUrl = ocs?.data.url
expect(shareUrl).to.match(/^http:\/\//)
})
})
})
deleteDownloadsFolderBeforeEach()
before(() => setupPublicShare())
beforeEach(() => {
cy.logout()
cy.visit(shareUrl)
cy.visit(getShareUrl())
})
it('Can download all files', () => {
// Check the button
cy.get('header')
.findByRole('button', { name: 'Download all files' })
.findByRole('button', { name: 'Download' })
.should('be.visible')
cy.get('header')
.findByRole('button', { name: 'Download all files' })
.findByRole('button', { name: 'Download' })
.click()
// check a file is downloaded
const downloadsFolder = Cypress.config('downloadsFolder')
cy.readFile(`${downloadsFolder}/${shareName}.zip`, null, { timeout: 15000 })
cy.readFile(`${downloadsFolder}/shared.zip`, null, { timeout: 15000 })
.should('exist')
.and('have.length.gt', 30)
// Check all files are included
.and(zipFileContains([
`${shareName}/`,
`${shareName}/foo.txt`,
`${shareName}/subfolder/`,
`${shareName}/subfolder/bar.txt`,
'shared/',
'shared/foo.txt',
'shared/subfolder/',
'shared/subfolder/bar.txt',
]))
})
@ -78,12 +50,12 @@ describe('files_sharing: Public share - header actions menu', { testIsolation: t
cy.findByRole('menu', { name: /More action/i })
.should('be.visible')
// see correct link in item
cy.findByRole('menuitem', { name: /Direct link/i })
cy.findByRole('menuitem', { name: 'Direct link' })
.should('be.visible')
.and('have.attr', 'href')
.then((attribute) => expect(attribute).to.match(/^http:\/\/.+\/download$/))
// see menu closes on click
cy.findByRole('menuitem', { name: /Direct link/i })
cy.findByRole('menuitem', { name: 'Direct link' })
.click()
cy.findByRole('menu', { name: /More actions/i })
.should('not.exist')
@ -100,7 +72,7 @@ describe('files_sharing: Public share - header actions menu', { testIsolation: t
// See the menu
cy.findByRole('menu', { name: /More action/i })
.should('be.visible')
// see correct item
// see correct button
cy.findByRole('menuitem', { name: /Add to your/i })
.should('be.visible')
.click()
@ -125,6 +97,7 @@ describe('files_sharing: Public share - header actions menu', { testIsolation: t
.findByRole('button', { name: /More actions/i })
.should('be.visible')
.click()
// see correct button
cy.findByRole('menuitem', { name: /Add to your/i })
.should('be.visible')
.click()
@ -134,10 +107,11 @@ describe('files_sharing: Public share - header actions menu', { testIsolation: t
.type('user@nextcloud.local')
// intercept request, the request is continued when the promise is resolved
const { promise, resolve } = Promise.withResolvers()
cy.intercept('POST', '**/apps/federatedfilesharing/createFederatedShare', async (req) => {
await promise
req.reply({ statusCode: 503 })
cy.intercept('POST', '**/apps/federatedfilesharing/createFederatedShare', (request) => {
// we need to wait in the onResponse handler as the intercept handler times out otherwise
request.on('response', async (response) => { await promise; response.statusCode = 503 })
}).as('createFederatedShare')
// create the share
cy.findByRole('button', { name: 'Create share' })
.click()
@ -161,7 +135,7 @@ describe('files_sharing: Public share - header actions menu', { testIsolation: t
.findByRole('button', { name: /More actions/i })
.should('be.visible')
.click()
// see correct item
// see correct button
cy.findByRole('menuitem', { name: /Add to your/i })
.should('be.visible')
.click()
@ -183,37 +157,43 @@ describe('files_sharing: Public share - header actions menu', { testIsolation: t
it('See primary action is moved to menu on small screens', () => {
cy.viewport(490, 490)
// Check the button does not exist
cy.get('header')
.should('be.visible')
.findByRole('button', { name: 'Download all files' })
.should('not.exist')
// Open the menu
cy.get('header')
.findByRole('button', { name: /More actions/i })
.should('be.visible')
.click()
// See that the button is located in the menu
cy.findByRole('menuitem', { name: /Download all files/i })
.should('be.visible')
// See all other items are also available
cy.get('header').within(() => {
cy.findByRole('button', { name: 'Direct link' })
.should('not.exist')
cy.findByRole('button', { name: 'Download' })
.should('not.exist')
cy.findByRole('button', { name: /Add to your/i })
.should('not.exist')
// Open the menu
cy.findByRole('button', { name: /More actions/i })
.should('be.visible')
.click()
})
// See correct number of menu item
cy.findByRole('menu', { name: 'More actions' })
.findAllByRole('menuitem')
.should('have.length', 3)
// Click the button to test the download
cy.findByRole('menuitem', { name: /Download all files/i })
.click()
cy.findByRole('menu', { name: 'More actions' })
.within(() => {
// See that download, federated share and direct link are moved to the menu
cy.findByRole('menuitem', { name: /^Download/ })
.should('be.visible')
cy.findByRole('menuitem', { name: /Add to your/i })
.should('be.visible')
cy.findByRole('menuitem', { name: 'Direct link' })
.should('be.visible')
// check a file is downloaded
const downloadsFolder = Cypress.config('downloadsFolder')
cy.readFile(`${downloadsFolder}/${shareName}.zip`, null, { timeout: 15000 })
.should('exist')
.and('have.length.gt', 30)
// Check all files are included
.and(zipFileContains([
`${shareName}/`,
`${shareName}/foo.txt`,
`${shareName}/subfolder/`,
`${shareName}/subfolder/bar.txt`,
]))
// See that direct link works
cy.findByRole('menuitem', { name: 'Direct link' })
.should('be.visible')
.and('have.attr', 'href')
.then((attribute) => expect(attribute).to.match(/^http:\/\/.+\/download$/))
// See remote share works
cy.findByRole('menuitem', { name: /Add to your/i })
.should('be.visible')
.click()
})
cy.findByRole('dialog', { name: /Add to your Nextcloud/i }).should('be.visible')
})
})

@ -0,0 +1,32 @@
/*!
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { getRowForFile, haveValidity, triggerActionForFile } from '../../files/FilesUtils.ts'
import { getShareUrl, setupPublicShare } from './setup-public-share.ts'
describe('files_sharing: Public share - renaming files', { testIsolation: true }, () => {
beforeEach(() => {
setupPublicShare()
.then(() => cy.logout())
.then(() => cy.visit(getShareUrl()))
})
it('can rename a file', () => {
// All are visible by default
getRowForFile('foo.txt').should('be.visible')
triggerActionForFile('foo.txt', 'rename')
getRowForFile('foo.txt')
.findByRole('textbox', { name: 'Filename' })
.should('be.visible')
.type('{selectAll}other.txt')
.should(haveValidity(''))
.type('{enter}')
// See it is renamed
getRowForFile('other.txt').should('be.visible')
})
})

@ -0,0 +1,119 @@
/*!
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { User } from '@nextcloud/cypress'
import { openSharingPanel } from '../FilesSharingUtils.ts'
let user: User
let url: string
/**
* URL of the share
*/
export function getShareUrl() {
if (url === undefined) {
throw new Error('You need to setup the share first!')
}
return url
}
/**
* Setup the available data
* @param shareName The name of the shared folder
*/
function setupData(shareName: string) {
cy.mkdir(user, `/${shareName}`)
cy.mkdir(user, `/${shareName}/subfolder`)
cy.uploadContent(user, new Blob(['<content>foo</content>']), 'text/plain', `/${shareName}/foo.txt`)
cy.uploadContent(user, new Blob(['<content>bar</content>']), 'text/plain', `/${shareName}/subfolder/bar.txt`)
}
/**
* Create a public link share
* @param shareName The name of the shared folder
*/
function createShare(shareName: string) {
cy.login(user)
// open the files app
cy.visit('/apps/files')
// open the sidebar
openSharingPanel(shareName)
// create the share
cy.intercept('POST', '**/ocs/v2.php/apps/files_sharing/api/v1/shares').as('createShare')
cy.findByRole('button', { name: 'Create a new share link' })
.click()
// extract the link
return cy.wait('@createShare')
.should(({ response }) => {
const { ocs } = response!.body
url = ocs?.data.url
expect(url).to.match(/^http:\/\//)
})
.then(() => cy.wrap(url))
}
/**
* Adjust share permissions to be editable
*/
function adjustSharePermission() {
// Update the share to be a file drop
cy.findByRole('list', { name: 'Link shares' })
.findAllByRole('listitem')
.first()
.findByRole('button', { name: /Actions/i })
.click()
cy.findByRole('menuitem', { name: /Customize link/i })
.should('be.visible')
.click()
// Enable upload-edit
cy.get('[data-cy-files-sharing-share-permissions-bundle]')
.should('be.visible')
cy.get('[data-cy-files-sharing-share-permissions-bundle="upload-edit"]')
.click()
// save changes
cy.intercept('PUT', '**/ocs/v2.php/apps/files_sharing/api/v1/shares/*').as('updateShare')
cy.findByRole('button', { name: 'Update share' })
.click()
cy.wait('@updateShare')
}
/**
* Setup a public share and backup the state.
* If the setup was already done in another run, the state will be restored.
*
* @return The URL of the share
*/
export function setupPublicShare(): Cypress.Chainable<string> {
const shareName = 'shared'
return cy.task('getVariable', { key: 'public-share-data' })
.then((data) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { dataSnapshot, dbSnapshot, shareUrl } = data as any || {}
if (dataSnapshot && dbSnapshot) {
cy.restoreDB(dbSnapshot)
cy.restoreData(dataSnapshot)
url = shareUrl
return cy.wrap(shareUrl as string)
} else {
cy.restoreData()
cy.restoreDB()
const shareData: Record<string, unknown> = {}
return cy.createRandomUser()
.then(($user) => { user = $user })
.then(() => setupData(shareName))
.then(() => createShare(shareName))
.then((value) => { shareData.shareUrl = value })
.then(() => adjustSharePermission())
.then(() => cy.backupDB().then((value) => { shareData.dbSnapshot = value }))
.then(() => cy.backupData([user.userId]).then((value) => { shareData.dataSnapshot = value }))
.then(() => cy.task('setVariable', { key: 'public-share-data', value: shareData }))
.then(() => cy.log(`Public share setup, URL: ${shareData.shareUrl}`))
.then(() => cy.wrap(url))
}
})
}

@ -0,0 +1,169 @@
/*!
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { getRowForFile } from '../../files/FilesUtils.ts'
import { openSharingPanel } from '../FilesSharingUtils.ts'
describe('files_sharing: Public share - File drop', { testIsolation: true }, () => {
let shareUrl: string
let user: string
const shareName = 'shared'
before(() => {
cy.createRandomUser().then(($user) => {
user = $user.userId
cy.mkdir($user, `/${shareName}`)
cy.uploadContent($user, new Blob(['content']), 'text/plain', `/${shareName}/foo.txt`)
cy.login($user)
// open the files app
cy.visit('/apps/files')
// open the sidebar
openSharingPanel(shareName)
// create the share
cy.intercept('POST', '**/ocs/v2.php/apps/files_sharing/api/v1/shares').as('createShare')
cy.findByRole('button', { name: 'Create a new share link' })
.click()
// extract the link
cy.wait('@createShare').should(({ response }) => {
const { ocs } = response?.body ?? {}
shareUrl = ocs?.data.url
expect(shareUrl).to.match(/^http:\/\//)
})
// Update the share to be a file drop
cy.findByRole('list', { name: 'Link shares' })
.findAllByRole('listitem')
.first()
.findByRole('button', { name: /Actions/i })
.click()
cy.findByRole('menuitem', { name: /Customize link/i })
.should('be.visible')
.click()
cy.get('[data-cy-files-sharing-share-permissions-bundle]')
.should('be.visible')
cy.get('[data-cy-files-sharing-share-permissions-bundle="file-drop"]')
.click()
// save the update
cy.intercept('PUT', '**/ocs/v2.php/apps/files_sharing/api/v1/shares/*').as('updateShare')
cy.findByRole('button', { name: 'Update share' })
.click()
cy.wait('@updateShare')
})
})
beforeEach(() => {
cy.logout()
cy.visit(shareUrl)
})
it('Cannot see share content', () => {
cy.contains(`Upload files to ${shareName}`)
.should('be.visible')
// foo exists
cy.userFileExists(user, `${shareName}/foo.txt`).should('be.gt', 0)
// but is not visible
getRowForFile('foo.txt')
.should('not.exist')
})
it('Can only see upload files and upload folders menu entries', () => {
cy.contains(`Upload files to ${shareName}`)
.should('be.visible')
cy.findByRole('button', { name: 'New' })
.should('be.visible')
.click()
// See upload actions
cy.findByRole('menuitem', { name: 'Upload files' })
.should('be.visible')
cy.findByRole('menuitem', { name: 'Upload folders' })
.should('be.visible')
// But no other
cy.findByRole('menu')
.findAllByRole('menuitem')
.should('have.length', 2)
})
it('Can only see dedicated upload button', () => {
cy.contains(`Upload files to ${shareName}`)
.should('be.visible')
cy.findByRole('button', { name: 'Upload' })
.should('be.visible')
.click()
// See upload actions
cy.findByRole('menuitem', { name: 'Upload files' })
.should('be.visible')
cy.findByRole('menuitem', { name: 'Upload folders' })
.should('be.visible')
// But no other
cy.findByRole('menu')
.findAllByRole('menuitem')
.should('have.length', 2)
})
it('Can upload files', () => {
cy.contains(`Upload files to ${shareName}`)
.should('be.visible')
const { promise, resolve } = Promise.withResolvers()
cy.intercept('PUT', '**/public.php/dav/files/**', (request) => {
if (request.url.includes('first.txt')) {
// just continue the first one
request.continue()
} else {
// We delay the second one until we checked that the progress bar is visible
request.on('response', async () => { await promise })
}
}).as('uploadFile')
cy.get('[data-cy-files-sharing-file-drop] input[type="file"]')
.should('exist')
.selectFile([
{ fileName: 'first.txt', contents: Buffer.from('8 bytes!') },
{ fileName: 'second.md', contents: Buffer.from('x'.repeat(128)) },
], { force: true })
cy.wait('@uploadFile')
cy.findByRole('progressbar')
.should('be.visible')
.and((el) => { expect(Number.parseInt(el.attr('value') ?? '0')).be.gte(50) })
// continue second request
.then(() => resolve(null))
cy.wait('@uploadFile')
// Check files uploaded
cy.userFileExists(user, `${shareName}/first.txt`).should('eql', 8)
cy.userFileExists(user, `${shareName}/second.md`).should('eql', 128)
})
describe('Terms of service', { testIsolation: true }, () => {
before(() => cy.runOccCommand('config:app:set --value "TEST: Some disclaimer text" --type string core shareapi_public_link_disclaimertext'))
beforeEach(() => cy.visit(shareUrl))
after(() => cy.runOccCommand('config:app:delete core shareapi_public_link_disclaimertext'))
it('shows ToS on file-drop view', () => {
cy.contains(`Upload files to ${shareName}`)
.should('be.visible')
.should('contain.text', 'agree to the terms of service')
cy.findByRole('button', { name: /Terms of service/i })
.should('be.visible')
.click()
cy.findByRole('dialog', { name: 'Terms of service' })
.should('contain.text', 'TEST: Some disclaimer text')
// close
.findByRole('button', { name: 'Close' })
.click()
cy.findByRole('dialog', { name: 'Terms of service' })
.should('not.exist')
})
})
})

@ -0,0 +1,104 @@
/*!
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { getActionButtonForFile, getRowForFile, navigateToFolder } from '../../files/FilesUtils.ts'
import { openSharingPanel } from '../FilesSharingUtils.ts'
describe('files_sharing: Public share - View only', { testIsolation: true }, () => {
let shareUrl: string
const shareName = 'shared'
before(() => {
cy.createRandomUser().then(($user) => {
cy.mkdir($user, `/${shareName}`)
cy.mkdir($user, `/${shareName}/subfolder`)
cy.uploadContent($user, new Blob([]), 'text/plain', `/${shareName}/foo.txt`)
cy.uploadContent($user, new Blob([]), 'text/plain', `/${shareName}/subfolder/bar.txt`)
cy.login($user)
// open the files app
cy.visit('/apps/files')
// open the sidebar
openSharingPanel(shareName)
// create the share
cy.intercept('POST', '**/ocs/v2.php/apps/files_sharing/api/v1/shares').as('createShare')
cy.findByRole('button', { name: 'Create a new share link' })
.click()
// extract the link
cy.wait('@createShare').should(({ response }) => {
const { ocs } = response?.body ?? {}
shareUrl = ocs?.data.url
expect(shareUrl).to.match(/^http:\/\//)
})
// Update the share to be a view-only-no-download share
cy.findByRole('list', { name: 'Link shares' })
.findAllByRole('listitem')
.first()
.findByRole('button', { name: /Actions/i })
.click()
cy.findByRole('menuitem', { name: /Customize link/i })
.should('be.visible')
.click()
cy.get('[data-cy-files-sharing-share-permissions-bundle]')
.should('be.visible')
cy.get('[data-cy-files-sharing-share-permissions-bundle="read-only"]')
.click()
cy.findByRole('checkbox', { name: 'Hide download' })
.check({ force: true })
// save the update
cy.intercept('PUT', '**/ocs/v2.php/apps/files_sharing/api/v1/shares/*').as('updateShare')
cy.findByRole('button', { name: 'Update share' })
.click()
cy.wait('@updateShare')
})
})
beforeEach(() => {
cy.logout()
cy.visit(shareUrl)
})
it('Can see the files list', () => {
// foo exists
getRowForFile('foo.txt')
.should('be.visible')
})
it('But no actions available', () => {
// foo exists
getRowForFile('foo.txt')
.should('be.visible')
// but no actions
getActionButtonForFile('foo.txt')
.should('not.exist')
// TODO: We really need Viewer in the server repo.
// So we could at least test viewing images
})
it('Can navigate to subfolder', () => {
getRowForFile('subfolder')
.should('be.visible')
navigateToFolder('subfolder')
getRowForFile('bar.txt')
.should('be.visible')
// but also no actions
getActionButtonForFile('bar.txt')
.should('not.exist')
})
it('Cannot upload files', () => {
// wait for file list to be ready
getRowForFile('foo.txt')
.should('be.visible')
cy.contains('button', 'New')
.should('be.visible')
.and('be.disabled')
})
})

@ -0,0 +1,107 @@
/*!
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { getActionsForFile, getRowForFile, navigateToFolder } from '../../files/FilesUtils.ts'
import { openSharingPanel } from '../FilesSharingUtils.ts'
describe('files_sharing: Public share - View only', { testIsolation: true }, () => {
let shareUrl: string
const shareName = 'shared'
before(() => {
cy.createRandomUser().then(($user) => {
cy.mkdir($user, `/${shareName}`)
cy.mkdir($user, `/${shareName}/subfolder`)
cy.uploadContent($user, new Blob(['content']), 'text/plain', `/${shareName}/foo.txt`)
cy.uploadContent($user, new Blob(['content']), 'text/plain', `/${shareName}/subfolder/bar.txt`)
cy.login($user)
// open the files app
cy.visit('/apps/files')
// open the sidebar
openSharingPanel(shareName)
// create the share
cy.intercept('POST', '**/ocs/v2.php/apps/files_sharing/api/v1/shares').as('createShare')
cy.findByRole('button', { name: 'Create a new share link' })
.click()
// extract the link
cy.wait('@createShare').should(({ response }) => {
const { ocs } = response?.body ?? {}
shareUrl = ocs?.data.url
expect(shareUrl).to.match(/^http:\/\//)
})
// Update the share to be a view-only-no-download share
cy.findByRole('list', { name: 'Link shares' })
.findAllByRole('listitem')
.first()
.findByRole('button', { name: /Actions/i })
.click()
cy.findByRole('menuitem', { name: /Customize link/i })
.should('be.visible')
.click()
cy.get('[data-cy-files-sharing-share-permissions-bundle]')
.should('be.visible')
cy.get('[data-cy-files-sharing-share-permissions-bundle="read-only"]')
.click()
// save the update
cy.intercept('PUT', '**/ocs/v2.php/apps/files_sharing/api/v1/shares/*').as('updateShare')
cy.findByRole('button', { name: 'Update share' })
.click()
cy.wait('@updateShare')
})
})
beforeEach(() => {
cy.logout()
cy.visit(shareUrl)
})
it('Can see the files list', () => {
// foo exists
getRowForFile('foo.txt')
.should('be.visible')
})
it('Can navigate to subfolder', () => {
getRowForFile('subfolder')
.should('be.visible')
navigateToFolder('subfolder')
getRowForFile('bar.txt')
.should('be.visible')
})
it('Cannot upload files', () => {
// wait for file list to be ready
getRowForFile('foo.txt')
.should('be.visible')
cy.contains('button', 'New')
.should('be.visible')
.and('be.disabled')
})
it('Only download action is actions available', () => {
getActionsForFile('foo.txt')
.should('be.visible')
.click()
// Only the download action
cy.findByRole('menuitem', { name: 'Download' })
.should('be.visible')
cy.findAllByRole('menuitem')
.should('have.length', 1)
// Can download
cy.findByRole('menuitem', { name: 'Download' }).click()
// check a file is downloaded
const downloadsFolder = Cypress.config('downloadsFolder')
cy.readFile(`${downloadsFolder}/foo.txt`, 'utf-8', { timeout: 15000 })
.should('exist')
.and('have.length.gt', 5)
.and('contain', 'content')
})
})

@ -66,6 +66,8 @@ declare global {
*/
runOccCommand(command: string, options?: Partial<Cypress.ExecOptions>): Cypress.Chainable<Cypress.Exec>,
userFileExists(user: string, path: string): Cypress.Chainable<number>
/**
* Create a snapshot of the current database
*/
@ -75,7 +77,11 @@ declare global {
* Restore a snapshot of the database
* Default is the post-setup state
*/
restoreDB(snapshot?: string): Cypress.Chainable,
restoreDB(snapshot?: string): Cypress.Chainable
backupData(users?: string[]): Cypress.Chainable<string>
restoreData(snapshot?: string): Cypress.Chainable
}
}
}
@ -85,7 +91,7 @@ Cypress.env('baseUrl', url)
/**
* Enable or disable a user
* TODO: standardise in @nextcloud/cypress
* TODO: standardize in @nextcloud/cypress
*
* @param {User} user the user to dis- / enable
* @param {boolean} enable True if the user should be enable, false to disable
@ -112,7 +118,7 @@ Cypress.Commands.add('enableUser', (user: User, enable = true) => {
/**
* cy.uploadedFile - uploads a file from the fixtures folder
* TODO: standardise in @nextcloud/cypress
* TODO: standardize in @nextcloud/cypress
*
* @param {User} user the owner of the file, e.g. admin
* @param {string} fixture the fixture file name, e.g. image1.jpg
@ -188,7 +194,7 @@ Cypress.Commands.add('mkdir', (user: User, target: string) => {
/**
* cy.uploadedContent - uploads a raw content
* TODO: standardise in @nextcloud/cypress
* TODO: standardize in @nextcloud/cypress
*
* @param {User} user the owner of the file, e.g. admin
* @param {Blob} blob the content to upload
@ -288,6 +294,13 @@ Cypress.Commands.add('runOccCommand', (command: string, options?: Partial<Cypres
return cy.exec(`docker exec --user www-data ${env} nextcloud-cypress-tests-server php ./occ ${command}`, options)
})
Cypress.Commands.add('userFileExists', (user: string, path: string) => {
user.replaceAll('"', '\\"')
path.replaceAll('"', '\\"').replaceAll(/^\/+/gm, '')
return cy.exec(`docker exec --user www-data nextcloud-cypress-tests-server stat --printf="%s" "data/${user}/files/${path}"`, { failOnNonZeroExit: true })
.then((exec) => Number.parseInt(exec.stdout || '0'))
})
Cypress.Commands.add('backupDB', (): Cypress.Chainable<string> => {
const randomString = Math.random().toString(36).substring(7)
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}`)
@ -299,3 +312,18 @@ Cypress.Commands.add('restoreDB', (snapshot: string = 'init') => {
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`)
cy.log(`Restored snapshot ${snapshot}`)
})
Cypress.Commands.add('backupData', (users: string[] = ['admin']) => {
const snapshot = Math.random().toString(36).substring(7)
const toBackup = users.map((user) => `'${user.replaceAll('\\', '').replaceAll('\'', '\\\'')}'`).join(' ')
cy.exec(`docker exec --user www-data rm /var/www/html/data/data-${snapshot}.tar`, { failOnNonZeroExit: false })
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}`)
return cy.wrap(snapshot as string)
})
Cypress.Commands.add('restoreData', (snapshot?: string) => {
snapshot = snapshot ?? 'init'
snapshot.replaceAll('\\', '').replaceAll('"', '\\"')
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')`)
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'`)
})

@ -17,9 +17,9 @@ export function zipFileContains(expectedFiles: string[]) {
const blob = new Blob([buffer])
const zip = new ZipReader(blob.stream())
// check the real file names
const entries = (await zip.getEntries()).map((e) => e.filename)
const entries = (await zip.getEntries()).map((e) => e.filename).sort()
console.info('Zip contains entries:', entries)
expect(entries).to.deep.equal(expectedFiles)
expect(entries).to.deep.equal(expectedFiles.sort())
}
}

2
package-lock.json generated

@ -27,7 +27,7 @@
"@nextcloud/moment": "^1.3.1",
"@nextcloud/password-confirmation": "^5.1.1",
"@nextcloud/paths": "^2.2.1",
"@nextcloud/router": "^3.0.0",
"@nextcloud/router": "^3.0.1",
"@nextcloud/sharing": "^0.2.3",
"@nextcloud/upload": "^1.6.0",
"@nextcloud/vue": "^8.17.1",

@ -58,7 +58,7 @@
"@nextcloud/moment": "^1.3.1",
"@nextcloud/password-confirmation": "^5.1.1",
"@nextcloud/paths": "^2.2.1",
"@nextcloud/router": "^3.0.0",
"@nextcloud/router": "^3.0.1",
"@nextcloud/sharing": "^0.2.3",
"@nextcloud/upload": "^1.6.0",
"@nextcloud/vue": "^8.17.1",

@ -10,6 +10,11 @@ export default defineConfig({
test: {
include: ['{apps,core}/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
environment: 'jsdom',
environmentOptions: {
jsdom: {
url: 'http://nextcloud.local',
},
},
coverage: {
include: ['apps/*/src/**', 'core/src/**'],
exclude: ['**.spec.*', '**.test.*', '**.cy.*', 'core/src/tests/**'],

Loading…
Cancel
Save