diff options
Diffstat (limited to 'cypress/e2e/files_sharing/public-share')
8 files changed, 920 insertions, 0 deletions
diff --git a/cypress/e2e/files_sharing/public-share/copy-move-files.cy.ts b/cypress/e2e/files_sharing/public-share/copy-move-files.cy.ts new file mode 100644 index 00000000000..078ecf747bf --- /dev/null +++ b/cypress/e2e/files_sharing/public-share/copy-move-files.cy.ts @@ -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') + }) +}) diff --git a/cypress/e2e/files_sharing/public-share/download-files.cy.ts b/cypress/e2e/files_sharing/public-share/download-files.cy.ts new file mode 100644 index 00000000000..4e37d1b38ae --- /dev/null +++ b/cypress/e2e/files_sharing/public-share/download-files.cy.ts @@ -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>') + }) + }) +}) diff --git a/cypress/e2e/files_sharing/public-share/header-menu.cy.ts b/cypress/e2e/files_sharing/public-share/header-menu.cy.ts new file mode 100644 index 00000000000..a89ee8eb90e --- /dev/null +++ b/cypress/e2e/files_sharing/public-share/header-menu.cy.ts @@ -0,0 +1,199 @@ +/*! + * 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 { 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 }, () => { + + before(() => setupPublicShare()) + beforeEach(() => { + cy.logout() + cy.visit(getShareUrl()) + }) + + it('Can download all files', () => { + cy.get('header') + .findByRole('button', { name: 'Download' }) + .should('be.visible') + cy.get('header') + .findByRole('button', { name: 'Download' }) + .click() + + // check a file is downloaded + const downloadsFolder = Cypress.config('downloadsFolder') + cy.readFile(`${downloadsFolder}/shared.zip`, null, { timeout: 15000 }) + .should('exist') + .and('have.length.gt', 30) + // Check all files are included + .and(zipFileContains([ + 'shared/', + 'shared/foo.txt', + 'shared/subfolder/', + 'shared/subfolder/bar.txt', + ])) + }) + + it('Can copy direct link', () => { + // Check the button + cy.get('header') + .findByRole('button', { name: /More actions/i }) + .should('be.visible') + cy.get('header') + .findByRole('button', { name: /More actions/i }) + .click() + // See the menu + cy.findByRole('menu', { name: /More action/i }) + .should('be.visible') + // see correct link in item + 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' }) + .click() + cy.findByRole('menu', { name: /More actions/i }) + .should('not.exist') + }) + + it('Can create federated share', () => { + // Check the button + cy.get('header') + .findByRole('button', { name: /More actions/i }) + .should('be.visible') + cy.get('header') + .findByRole('button', { name: /More actions/i }) + .click() + // See the menu + cy.findByRole('menu', { name: /More action/i }) + .should('be.visible') + // see correct button + cy.findByRole('menuitem', { name: /Add to your/i }) + .should('be.visible') + .click() + // see the dialog + cy.findByRole('dialog', { name: /Add to your Nextcloud/i }) + .should('be.visible') + cy.findByRole('dialog', { name: /Add to your Nextcloud/i }).within(() => { + cy.findByRole('textbox') + .type('user@nextcloud.local') + // create share + cy.intercept('POST', '**/apps/federatedfilesharing/createFederatedShare') + .as('createFederatedShare') + cy.findByRole('button', { name: 'Create share' }) + .click() + cy.wait('@createFederatedShare') + }) + }) + + it('Has user feedback while creating federated share', () => { + // Check the button + cy.get('header') + .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() + // see the dialog + cy.findByRole('dialog', { name: /Add to your Nextcloud/i }).should('be.visible').within(() => { + cy.findByRole('textbox') + .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', (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() + // see that while the share is created the button is disabled + cy.findByRole('button', { name: 'Create share' }) + .should('be.disabled') + .then(() => { + // continue the request + resolve(null) + }) + cy.wait('@createFederatedShare') + // see that the button is no longer disabled + cy.findByRole('button', { name: 'Create share' }) + .should('not.be.disabled') + }) + }) + + it('Has input validation for federated share', () => { + // Check the button + cy.get('header') + .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() + // see the dialog + cy.findByRole('dialog', { name: /Add to your Nextcloud/i }).should('be.visible').within(() => { + // Check domain only + cy.findByRole('textbox') + .type('nextcloud.local') + cy.findByRole('textbox') + .should(haveValidity(/user/i)) + // Check no valid domain + cy.findByRole('textbox') + .type('{selectAll}user@invalid') + cy.findByRole('textbox') + .should(haveValidity(/invalid.+url/i)) + }) + }) + + it('See primary action is moved to menu on small screens', () => { + cy.viewport(490, 490) + // Check the button does not exist + 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) + 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') + + // 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') + }) +}) diff --git a/cypress/e2e/files_sharing/public-share/rename-files.cy.ts b/cypress/e2e/files_sharing/public-share/rename-files.cy.ts new file mode 100644 index 00000000000..5f2fe00e650 --- /dev/null +++ b/cypress/e2e/files_sharing/public-share/rename-files.cy.ts @@ -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') + }) +}) diff --git a/cypress/e2e/files_sharing/public-share/setup-public-share.ts b/cypress/e2e/files_sharing/public-share/setup-public-share.ts new file mode 100644 index 00000000000..9549552c200 --- /dev/null +++ b/cypress/e2e/files_sharing/public-share/setup-public-share.ts @@ -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)) + } + }) +} diff --git a/cypress/e2e/files_sharing/public-share/view_file-drop.cy.ts b/cypress/e2e/files_sharing/public-share/view_file-drop.cy.ts new file mode 100644 index 00000000000..8bc4b9b8e15 --- /dev/null +++ b/cypress/e2e/files_sharing/public-share/view_file-drop.cy.ts @@ -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') + }) + }) +}) diff --git a/cypress/e2e/files_sharing/public-share/view_view-only-no-download.cy.ts b/cypress/e2e/files_sharing/public-share/view_view-only-no-download.cy.ts new file mode 100644 index 00000000000..abcb9ccae62 --- /dev/null +++ b/cypress/e2e/files_sharing/public-share/view_view-only-no-download.cy.ts @@ -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') + }) +}) diff --git a/cypress/e2e/files_sharing/public-share/view_view-only.cy.ts b/cypress/e2e/files_sharing/public-share/view_view-only.cy.ts new file mode 100644 index 00000000000..4a8aa6b89a3 --- /dev/null +++ b/cypress/e2e/files_sharing/public-share/view_view-only.cy.ts @@ -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') + }) +}) |