diff options
Diffstat (limited to 'cypress/e2e/files_sharing/public-share')
12 files changed, 1644 insertions, 0 deletions
diff --git a/cypress/e2e/files_sharing/public-share/PublicShareUtils.ts b/cypress/e2e/files_sharing/public-share/PublicShareUtils.ts new file mode 100644 index 00000000000..e0cbd06a4c7 --- /dev/null +++ b/cypress/e2e/files_sharing/public-share/PublicShareUtils.ts @@ -0,0 +1,191 @@ +/*! + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import type { User } from '@nextcloud/cypress' +import type { ShareOptions } from '../ShareOptionsType.ts' +import { openSharingPanel } from '../FilesSharingUtils.ts' + +export interface ShareContext { + user: User + url?: string +} + +const defaultShareContext: ShareContext = { + user: {} as User, + url: undefined, +} + +/** + * Retrieves the URL of the share. + * Throws an error if the share context is not initialized properly. + * + * @param context The current share context (defaults to `defaultShareContext` if not provided). + * @return The share URL. + * @throws Error if the share context has no URL. + */ +export function getShareUrl(context: ShareContext = defaultShareContext): string { + if (!context.url) { + throw new Error('You need to setup the share first!') + } + return context.url +} + +/** + * Setup the available data + * @param user The current share context + * @param shareName The name of the shared folder + */ +export function setupData(user: User, shareName: string): void { + 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`) +} + +/** + * Check the password state based on enforcement and default presence. + * + * @param enforced Whether the password is enforced. + * @param alwaysAskForPassword Wether the password should always be asked for. + */ +function checkPasswordState(enforced: boolean, alwaysAskForPassword: boolean) { + if (enforced) { + cy.contains('Password protection (enforced)').should('exist') + } else if (alwaysAskForPassword) { + cy.contains('Password protection').should('exist') + } + cy.contains('Enter a password') + .should('exist') + .and('not.be.disabled') +} + +/** + * Check the expiration date state based on enforcement and default presence. + * + * @param enforced Whether the expiration date is enforced. + * @param hasDefault Whether a default expiration date is set. + */ +function checkExpirationDateState(enforced: boolean, hasDefault: boolean) { + if (enforced) { + cy.contains('Enable link expiration (enforced)').should('exist') + } else if (hasDefault) { + cy.contains('Enable link expiration').should('exist') + } + cy.contains('Enter expiration date') + .should('exist') + .and('not.be.disabled') + cy.get('input[data-cy-files-sharing-expiration-date-input]').should('exist') + cy.get('input[data-cy-files-sharing-expiration-date-input]') + .invoke('val') + .then((val) => { + // eslint-disable-next-line no-unused-expressions + expect(val).to.not.be.undefined + + const inputDate = new Date(typeof val === 'number' ? val : String(val)) + const expectedDate = new Date() + expectedDate.setDate(expectedDate.getDate() + 2) + expect(inputDate.toDateString()).to.eq(expectedDate.toDateString()) + }) + +} + +/** + * Create a public link share + * @param context The current share context + * @param shareName The name of the shared folder + * @param options The share options + */ +export function createLinkShare(context: ShareContext, shareName: string, options: ShareOptions | null = null): Cypress.Chainable<string> { + cy.login(context.user) + cy.visit('/apps/files') + openSharingPanel(shareName) + + cy.intercept('POST', '**/ocs/v2.php/apps/files_sharing/api/v1/shares').as('createLinkShare') + cy.findByRole('button', { name: 'Create a new share link' }).click() + // Conduct optional checks based on the provided options + if (options) { + cy.get('.sharing-entry__actions').should('be.visible') // Wait for the dialog to open + checkPasswordState(options.enforcePassword ?? false, options.alwaysAskForPassword ?? false) + checkExpirationDateState(options.enforceExpirationDate ?? false, options.defaultExpirationDateSet ?? false) + cy.findByRole('button', { name: 'Create share' }).click() + } + + return cy.wait('@createLinkShare') + .should(({ response }) => { + expect(response?.statusCode).to.eq(200) + const url = response?.body?.ocs?.data?.url + expect(url).to.match(/^https?:\/\//) + context.url = url + }) + .then(() => cy.wrap(context.url as string)) +} + +/** + * open link share details for specific index + * + * @param index + */ +export function openLinkShareDetails(index: number) { + cy.findByRole('list', { name: 'Link shares' }) + .findAllByRole('listitem') + .eq(index) + .findByRole('button', { name: /Actions/i }) + .click() + cy.findByRole('menuitem', { name: /Customize link/i }).click() +} + +/** + * Adjust share permissions to be editable + */ +function adjustSharePermission(): void { + openLinkShareDetails(0) + + cy.get('[data-cy-files-sharing-share-permissions-bundle]').should('be.visible') + cy.get('[data-cy-files-sharing-share-permissions-bundle="upload-edit"]').click() + + cy.intercept('PUT', '**/ocs/v2.php/apps/files_sharing/api/v1/shares/*').as('updateShare') + cy.findByRole('button', { name: 'Update share' }).click() + cy.wait('@updateShare').its('response.statusCode').should('eq', 200) +} + +/** + * Setup a public share and backup the state. + * If the setup was already done in another run, the state will be restored. + * + * @param shareName The name of the shared folder + * @return The URL of the share + */ +export function setupPublicShare(shareName = 'shared'): Cypress.Chainable<string> { + + return cy.task('getVariable', { key: 'public-share-data' }) + .then((data) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { dataSnapshot, shareUrl } = data as any || {} + if (dataSnapshot) { + cy.restoreState(dataSnapshot) + defaultShareContext.url = shareUrl + return cy.wrap(shareUrl as string) + } else { + const shareData: Record<string, unknown> = {} + return cy.createRandomUser() + .then((user) => { + defaultShareContext.user = user + }) + .then(() => setupData(defaultShareContext.user, shareName)) + .then(() => createLinkShare(defaultShareContext, shareName)) + .then((url) => { + shareData.shareUrl = url + }) + .then(() => adjustSharePermission()) + .then(() => + cy.saveState().then((snapshot) => { + shareData.dataSnapshot = snapshot + }), + ) + .then(() => cy.task('setVariable', { key: 'public-share-data', value: shareData })) + .then(() => cy.log(`Public share setup, URL: ${shareData.shareUrl}`)) + .then(() => cy.wrap(defaultShareContext.url)) + } + }) +} 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..87f16b01387 --- /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 './PublicShareUtils.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/default-view.cy.ts b/cypress/e2e/files_sharing/public-share/default-view.cy.ts new file mode 100644 index 00000000000..33e0a57da11 --- /dev/null +++ b/cypress/e2e/files_sharing/public-share/default-view.cy.ts @@ -0,0 +1,102 @@ +/*! + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import type { User } from '@nextcloud/cypress' +import { getRowForFile } from '../../files/FilesUtils.ts' +import { createLinkShare, setupData } from './PublicShareUtils.ts' + +describe('files_sharing: Public share - setting the default view mode', () => { + + let user: User + + beforeEach(() => { + cy.createRandomUser() + .then(($user) => (user = $user)) + .then(() => setupData(user, 'shared')) + }) + + it('is by default in list view', () => { + const context = { user } + createLinkShare(context, 'shared') + .then((url) => { + cy.logout() + cy.visit(url!) + + // See file is visible + getRowForFile('foo.txt').should('be.visible') + // See we are in list view + cy.findByRole('button', { name: 'Switch to grid view' }) + .should('be.visible') + .and('not.be.disabled') + }) + }) + + it('can be toggled by user', () => { + const context = { user } + createLinkShare(context, 'shared') + .then((url) => { + cy.logout() + cy.visit(url!) + + // See file is visible + getRowForFile('foo.txt') + .should('be.visible') + // See we are in list view + .find('.files-list__row-icon') + .should(($el) => expect($el.outerWidth()).to.be.lessThan(99)) + + // See the grid view toggle + cy.findByRole('button', { name: 'Switch to grid view' }) + .should('be.visible') + .and('not.be.disabled') + // And can change to grid view + .click() + + // See we are in grid view + getRowForFile('foo.txt') + .find('.files-list__row-icon') + .should(($el) => expect($el.outerWidth()).to.be.greaterThan(99)) + + // See the grid view toggle is now the list view toggle + cy.findByRole('button', { name: 'Switch to list view' }) + .should('be.visible') + .and('not.be.disabled') + }) + }) + + it('can be changed to default grid view', () => { + const context = { user } + createLinkShare(context, 'shared') + .then((url) => { + // Can set the "grid" view checkbox + cy.findByRole('list', { name: 'Link shares' }) + .findAllByRole('listitem') + .first() + .findByRole('button', { name: /Actions/i }) + .click() + cy.findByRole('menuitem', { name: /Customize link/i }).click() + cy.findByRole('checkbox', { name: /Show files in grid view/i }) + .scrollIntoView() + cy.findByRole('checkbox', { name: /Show files in grid view/i }) + .should('not.be.checked') + .check({ force: true }) + + // Wait for the share 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').its('response.statusCode').should('eq', 200) + + // Logout and visit the share + cy.logout() + cy.visit(url!) + + // See file is visible + getRowForFile('foo.txt').should('be.visible') + // See we are in list view + cy.findByRole('button', { name: 'Switch to list view' }) + .should('be.visible') + .and('not.be.disabled') + }) + }) +}) diff --git a/cypress/e2e/files_sharing/public-share/download.cy.ts b/cypress/e2e/files_sharing/public-share/download.cy.ts new file mode 100644 index 00000000000..372f553a8a0 --- /dev/null +++ b/cypress/e2e/files_sharing/public-share/download.cy.ts @@ -0,0 +1,266 @@ +/*! + * 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 { createLinkShare, getShareUrl, openLinkShareDetails, setupPublicShare, type ShareContext } from './PublicShareUtils.ts' +import { getRowForFile, getRowForFileId, triggerActionForFile, triggerActionForFileId } from '../../files/FilesUtils.ts' +import { zipFileContains } from '../../../support/utils/assertions.ts' +import type { User } from '@nextcloud/cypress' + +describe('files_sharing: Public share - downloading files', { testIsolation: true }, () => { + + // in general there is no difference except downloading + // as file shares have the source of the share token but a different displayname + describe('file share', () => { + let fileId: number + + before(() => { + cy.createRandomUser().then((user) => { + const context: ShareContext = { user } + cy.uploadContent(user, new Blob(['<content>foo</content>']), 'text/plain', '/file.txt') + .then(({ headers }) => { fileId = Number.parseInt(headers['oc-fileid']) }) + cy.login(user) + createLinkShare(context, 'file.txt') + .then(() => cy.logout()) + .then(() => cy.visit(context.url!)) + }) + }) + + it('can download the file', () => { + getRowForFileId(fileId) + .should('be.visible') + getRowForFileId(fileId) + .find('[data-cy-files-list-row-name]') + .should((el) => expect(el.text()).to.match(/file\s*\.txt/)) // extension is sparated so there might be a space between + triggerActionForFileId(fileId, 'download') + // check a file is downloaded with the correct name + const downloadsFolder = Cypress.config('downloadsFolder') + cy.readFile(`${downloadsFolder}/file.txt`, 'utf-8', { timeout: 15000 }) + .should('exist') + .and('have.length.gt', 5) + .and('contain', '<content>foo</content>') + }) + }) + + describe('folder share', () => { + 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}/download.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>') + }) + }) + }) + + describe('download permission - link share', () => { + let context: ShareContext + beforeEach(() => { + cy.createRandomUser().then((user) => { + cy.mkdir(user, '/test') + + context = { user } + createLinkShare(context, 'test') + cy.login(context.user) + cy.visit('/apps/files') + }) + }) + + deleteDownloadsFolderBeforeEach() + + it('download permission is retained', () => { + getRowForFile('test').should('be.visible') + triggerActionForFile('test', 'details') + + openLinkShareDetails(0) + + cy.intercept('PUT', '**/ocs/v2.php/apps/files_sharing/api/v1/shares/*').as('update') + + cy.findByRole('checkbox', { name: /hide download/i }) + .should('exist') + .and('not.be.checked') + .check({ force: true }) + cy.findByRole('checkbox', { name: /hide download/i }) + .should('be.checked') + cy.findByRole('button', { name: /update share/i }) + .click() + + cy.wait('@update') + + openLinkShareDetails(0) + cy.findByRole('checkbox', { name: /hide download/i }) + .should('be.checked') + + cy.reload() + + openLinkShareDetails(0) + cy.findByRole('checkbox', { name: /hide download/i }) + .should('be.checked') + }) + }) + + describe('download permission - mail share', () => { + let user: User + + beforeEach(() => { + cy.createRandomUser().then(($user) => { + user = $user + cy.mkdir(user, '/test') + cy.login(user) + cy.visit('/apps/files') + }) + }) + + it('download permission is retained', () => { + getRowForFile('test').should('be.visible') + triggerActionForFile('test', 'details') + + cy.findByRole('combobox', { name: /Enter external recipients/i }) + .type('test@example.com') + + cy.get('.option[sharetype="4"][user="test@example.com"]') + .parent('li') + .click() + cy.findByRole('button', { name: /advanced settings/i }) + .should('be.visible') + .click() + + cy.intercept('PUT', '**/ocs/v2.php/apps/files_sharing/api/v1/shares/*').as('update') + + cy.findByRole('checkbox', { name: /hide download/i }) + .should('exist') + .and('not.be.checked') + .check({ force: true }) + cy.findByRole('button', { name: /save share/i }) + .click() + + cy.wait('@update') + + openLinkShareDetails(0) + cy.findByRole('button', { name: /advanced settings/i }) + .click() + cy.findByRole('checkbox', { name: /hide download/i }) + .should('exist') + .and('be.checked') + }) + }) +}) diff --git a/cypress/e2e/files_sharing/public-share/header-avatar.cy.ts b/cypress/e2e/files_sharing/public-share/header-avatar.cy.ts new file mode 100644 index 00000000000..c7227062293 --- /dev/null +++ b/cypress/e2e/files_sharing/public-share/header-avatar.cy.ts @@ -0,0 +1,193 @@ +/*! + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import type { ShareContext } from './PublicShareUtils.ts' +import { createLinkShare, setupData } from './PublicShareUtils.ts' + +/** + * This tests ensures that on public shares the header avatar menu correctly works + */ +describe('files_sharing: Public share - header avatar menu', { testIsolation: true }, () => { + let context: ShareContext + let firstPublicShareUrl = '' + let secondPublicShareUrl = '' + + before(() => { + cy.createRandomUser() + .then((user) => { + context = { + user, + url: undefined, + } + setupData(context.user, 'public1') + setupData(context.user, 'public2') + createLinkShare(context, 'public1').then((shareUrl) => { + firstPublicShareUrl = shareUrl + cy.log(`Created first share with URL: ${shareUrl}`) + }) + createLinkShare(context, 'public2').then((shareUrl) => { + secondPublicShareUrl = shareUrl + cy.log(`Created second share with URL: ${shareUrl}`) + }) + }) + }) + + beforeEach(() => { + cy.logout() + cy.visit(firstPublicShareUrl) + }) + + it('See the undefined avatar menu', () => { + cy.get('header') + .findByRole('navigation', { name: /User menu/i }) + .should('be.visible') + .findByRole('button', { name: /User menu/i }) + .should('be.visible') + .click() + cy.get('#header-menu-public-page-user-menu') + .as('headerMenu') + + // Note that current guest user is not identified + cy.get('@headerMenu') + .should('be.visible') + .findByRole('note') + .should('be.visible') + .should('contain', 'not identified') + + // Button to set guest name + cy.get('@headerMenu') + .findByRole('link', { name: /Set public name/i }) + .should('be.visible') + }) + + it('Can set public name', () => { + cy.get('header') + .findByRole('navigation', { name: /User menu/i }) + .should('be.visible') + .findByRole('button', { name: /User menu/i }) + .should('be.visible') + .as('userMenuButton') + + // Open the user menu + cy.get('@userMenuButton').click() + cy.get('#header-menu-public-page-user-menu') + .as('headerMenu') + + cy.get('@headerMenu') + .findByRole('link', { name: /Set public name/i }) + .should('be.visible') + .click() + + // Check the dialog is visible + cy.findByRole('dialog', { name: /Guest identification/i }) + .should('be.visible') + .as('guestIdentificationDialog') + + // Check the note is visible + cy.get('@guestIdentificationDialog') + .findByRole('note') + .should('contain', 'not identified') + + // Check the input is visible + cy.get('@guestIdentificationDialog') + .findByRole('textbox', { name: /Name/i }) + .should('be.visible') + .type('{selectAll}John Doe{enter}') + + // Check that the dialog is closed + cy.get('@guestIdentificationDialog') + .should('not.exist') + + // Check that the avatar changed + cy.get('@userMenuButton') + .find('img') + .invoke('attr', 'src') + .should('include', 'avatar/guest/John%20Doe') + }) + + it('Guest name us persistent and can be changed', () => { + cy.get('header') + .findByRole('navigation', { name: /User menu/i }) + .should('be.visible') + .findByRole('button', { name: /User menu/i }) + .should('be.visible') + .as('userMenuButton') + + // Open the user menu + cy.get('@userMenuButton').click() + cy.get('#header-menu-public-page-user-menu') + .as('headerMenu') + + cy.get('@headerMenu') + .findByRole('link', { name: /Set public name/i }) + .should('be.visible') + .click() + + // Check the dialog is visible + cy.findByRole('dialog', { name: /Guest identification/i }) + .should('be.visible') + .as('guestIdentificationDialog') + + // Set the name + cy.get('@guestIdentificationDialog') + .findByRole('textbox', { name: /Name/i }) + .should('be.visible') + .type('{selectAll}Jane Doe{enter}') + + // Check that the dialog is closed + cy.get('@guestIdentificationDialog') + .should('not.exist') + + // Create another share + cy.visit(secondPublicShareUrl) + + cy.get('header') + .findByRole('navigation', { name: /User menu/i }) + .should('be.visible') + .findByRole('button', { name: /User menu/i }) + .should('be.visible') + .as('userMenuButton') + + // Open the user menu + cy.get('@userMenuButton').click() + cy.get('#header-menu-public-page-user-menu') + .as('headerMenu') + + // See the note with the current name + cy.get('@headerMenu') + .findByRole('note') + .should('contain', 'You will be identified as Jane Doe') + + cy.get('@headerMenu') + .findByRole('link', { name: /Change public name/i }) + .should('be.visible') + .click() + + // Check the dialog is visible + cy.findByRole('dialog', { name: /Guest identification/i }) + .should('be.visible') + .as('guestIdentificationDialog') + + // Check that the note states the current name + // cy.get('@guestIdentificationDialog') + // .findByRole('note') + // .should('contain', 'are currently identified as Jane Doe') + + // Change the name + cy.get('@guestIdentificationDialog') + .findByRole('textbox', { name: /Name/i }) + .should('be.visible') + .type('{selectAll}Foo Bar{enter}') + + // Check that the dialog is closed + cy.get('@guestIdentificationDialog') + .should('not.exist') + + // Check that the avatar changed with the second name + cy.get('@userMenuButton') + .find('img') + .invoke('attr', 'src') + .should('include', 'avatar/guest/Foo%20Bar') + }) +}) 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..1dd0de13477 --- /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 './PublicShareUtils.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(new RegExp(`^${Cypress.env('baseUrl')}/public.php/dav/files/.+/?accept=zip$`))) + // 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(new RegExp(`^${Cypress.env('baseUrl')}/public.php/dav/files/.+/?accept=zip$`))) + // 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..adeb6e52504 --- /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 './PublicShareUtils.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/required-before-create.cy.ts b/cypress/e2e/files_sharing/public-share/required-before-create.cy.ts new file mode 100644 index 00000000000..772b7fa8380 --- /dev/null +++ b/cypress/e2e/files_sharing/public-share/required-before-create.cy.ts @@ -0,0 +1,192 @@ +/*! + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { ShareContext } from './PublicShareUtils.ts' +import type { ShareOptions } from '../ShareOptionsType.ts' +import { defaultShareOptions } from '../ShareOptionsType.ts' +import { setupData, createLinkShare } from './PublicShareUtils.ts' + +describe('files_sharing: Before create checks', () => { + + let shareContext: ShareContext + + before(() => { + // Setup data for the shared folder once before all tests + cy.createRandomUser().then((randomUser) => { + shareContext = { + user: randomUser, + } + }) + }) + + afterEach(() => { + cy.runOccCommand('config:app:delete core shareapi_enable_link_password_by_default') + cy.runOccCommand('config:app:delete core shareapi_enforce_links_password') + cy.runOccCommand('config:app:delete core shareapi_default_expire_date') + cy.runOccCommand('config:app:delete core shareapi_enforce_expire_date') + cy.runOccCommand('config:app:delete core shareapi_expire_after_n_days') + }) + + const applyShareOptions = (options: ShareOptions = defaultShareOptions): void => { + cy.runOccCommand(`config:app:set --value ${options.alwaysAskForPassword ? 'yes' : 'no'} core shareapi_enable_link_password_by_default`) + cy.runOccCommand(`config:app:set --value ${options.enforcePassword ? 'yes' : 'no'} core shareapi_enforce_links_password`) + cy.runOccCommand(`config:app:set --value ${options.enforceExpirationDate ? 'yes' : 'no'} core shareapi_enforce_expire_date`) + cy.runOccCommand(`config:app:set --value ${options.defaultExpirationDateSet ? 'yes' : 'no'} core shareapi_default_expire_date`) + if (options.defaultExpirationDateSet) { + cy.runOccCommand('config:app:set --value 2 core shareapi_expire_after_n_days') + } + } + + it('Checks if user can create share when both password and expiration date are enforced', () => { + const shareOptions : ShareOptions = { + alwaysAskForPassword: true, + enforcePassword: true, + enforceExpirationDate: true, + defaultExpirationDateSet: true, + } + applyShareOptions(shareOptions) + const shareName = 'passwordAndExpireEnforced' + setupData(shareContext.user, shareName) + createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => { + shareContext.url = shareUrl + cy.log(`Created share with URL: ${shareUrl}`) + }) + }) + + it('Checks if user can create share when password is enforced and expiration date has a default set', () => { + const shareOptions : ShareOptions = { + alwaysAskForPassword: true, + enforcePassword: true, + defaultExpirationDateSet: true, + } + applyShareOptions(shareOptions) + const shareName = 'passwordEnforcedDefaultExpire' + setupData(shareContext.user, shareName) + createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => { + shareContext.url = shareUrl + cy.log(`Created share with URL: ${shareUrl}`) + }) + }) + + it('Checks if user can create share when password is optionally requested and expiration date is enforced', () => { + const shareOptions : ShareOptions = { + alwaysAskForPassword: true, + defaultExpirationDateSet: true, + enforceExpirationDate: true, + } + applyShareOptions(shareOptions) + const shareName = 'defaultPasswordExpireEnforced' + setupData(shareContext.user, shareName) + createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => { + shareContext.url = shareUrl + cy.log(`Created share with URL: ${shareUrl}`) + }) + }) + + it('Checks if user can create share when password is optionally requested and expiration date have defaults set', () => { + const shareOptions : ShareOptions = { + alwaysAskForPassword: true, + defaultExpirationDateSet: true, + } + applyShareOptions(shareOptions) + const shareName = 'defaultPasswordAndExpire' + setupData(shareContext.user, shareName) + createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => { + shareContext.url = shareUrl + cy.log(`Created share with URL: ${shareUrl}`) + }) + }) + + it('Checks if user can create share with password enforced and expiration date set but not enforced', () => { + const shareOptions : ShareOptions = { + alwaysAskForPassword: true, + enforcePassword: true, + defaultExpirationDateSet: true, + enforceExpirationDate: false, + } + applyShareOptions(shareOptions) + const shareName = 'passwordEnforcedExpireSetNotEnforced' + setupData(shareContext.user, shareName) + createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => { + shareContext.url = shareUrl + cy.log(`Created share with URL: ${shareUrl}`) + }) + }) + + it('Checks if user can create a share when both password and expiration date have default values but are both not enforced', () => { + const shareOptions : ShareOptions = { + alwaysAskForPassword: true, + enforcePassword: false, + defaultExpirationDateSet: true, + enforceExpirationDate: false, + } + applyShareOptions(shareOptions) + const shareName = 'defaultPasswordAndExpirationNotEnforced' + setupData(shareContext.user, shareName) + createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => { + shareContext.url = shareUrl + cy.log(`Created share with URL: ${shareUrl}`) + }) + }) + + it('Checks if user can create share with password not enforced but expiration date enforced', () => { + const shareOptions : ShareOptions = { + alwaysAskForPassword: true, + enforcePassword: false, + defaultExpirationDateSet: true, + enforceExpirationDate: true, + } + applyShareOptions(shareOptions) + const shareName = 'noPasswordExpireEnforced' + setupData(shareContext.user, shareName) + createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => { + shareContext.url = shareUrl + cy.log(`Created share with URL: ${shareUrl}`) + }) + }) + + it('Checks if user can create share with password not enforced and expiration date has a default set', () => { + const shareOptions : ShareOptions = { + alwaysAskForPassword: true, + enforcePassword: false, + defaultExpirationDateSet: true, + enforceExpirationDate: false, + } + applyShareOptions(shareOptions) + const shareName = 'defaultExpireNoPasswordEnforced' + setupData(shareContext.user, shareName) + createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => { + shareContext.url = shareUrl + cy.log(`Created share with URL: ${shareUrl}`) + }) + }) + + it('Checks if user can create share with expiration date set and password not enforced', () => { + const shareOptions : ShareOptions = { + alwaysAskForPassword: true, + enforcePassword: false, + defaultExpirationDateSet: true, + } + applyShareOptions(shareOptions) + + const shareName = 'noPasswordExpireDefault' + setupData(shareContext.user, shareName) + createLinkShare(shareContext, shareName, shareOptions).then((shareUrl) => { + shareContext.url = shareUrl + cy.log(`Created share with URL: ${shareUrl}`) + }) + }) + + it('Checks if user can create share with password not enforced, expiration date not enforced, and no defaults set', () => { + applyShareOptions() + const shareName = 'noPasswordNoExpireNoDefaults' + setupData(shareContext.user, shareName) + createLinkShare(shareContext, shareName, null).then((shareUrl) => { + shareContext.url = shareUrl + cy.log(`Created share with URL: ${shareUrl}`) + }) + }) + +}) diff --git a/cypress/e2e/files_sharing/public-share/sidebar-tab.cy.ts b/cypress/e2e/files_sharing/public-share/sidebar-tab.cy.ts new file mode 100644 index 00000000000..6b026717fd8 --- /dev/null +++ b/cypress/e2e/files_sharing/public-share/sidebar-tab.cy.ts @@ -0,0 +1,45 @@ +/*! + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { User } from "@nextcloud/cypress" +import { createShare } from "./FilesSharingUtils" +import { createLinkShare, openLinkShareDetails } from "./PublicShareUtils" + +describe('files_sharing: sidebar tab', () => { + let alice: User + + beforeEach(() => { + cy.createRandomUser() + .then((user) => { + alice = user + cy.mkdir(user, '/test') + cy.login(user) + cy.visit('/apps/files') + }) + }) + + /** + * Regression tests of https://github.com/nextcloud/server/issues/53566 + * Where the ' char was shown as ' + */ + it('correctly lists shares by label with special characters', () => { + createLinkShare({ user: alice }, 'test') + openLinkShareDetails(0) + cy.findByRole('textbox', { name: /share label/i }) + .should('be.visible') + .type('Alice\' share') + + cy.intercept('PUT', '**/ocs/v2.php/apps/files_sharing/api/v1/shares/*').as('PUT') + cy.findByRole('button', { name: /update share/i }).click() + cy.wait('@PUT') + + // see the label is shown correctly + cy.findByRole('list', { name: /link shares/i }) + .findAllByRole('listitem') + .should('have.length', 1) + .first() + .should('contain.text', 'Share link (Alice\' share)') + }) +}) 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..f95115ee591 --- /dev/null +++ b/cypress/e2e/files_sharing/public-share/view_file-drop.cy.ts @@ -0,0 +1,172 @@ +/*! + * 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.get('[data-cy-files-sharing-file-drop]') + .contains(`Upload files to ${shareName}`) + .should('be.visible') + cy.get('[data-cy-files-sharing-file-drop]') + .contains('agree to the terms of service') + .should('be.visible') + 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..0e2d2edab6c --- /dev/null +++ b/cypress/e2e/files_sharing/public-share/view_view-only-no-download.cy.ts @@ -0,0 +1,100 @@ +/*! + * 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') + }) +}) 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..511a1caeb09 --- /dev/null +++ b/cypress/e2e/files_sharing/public-share/view_view-only.cy.ts @@ -0,0 +1,103 @@ +/*! + * 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(['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') + }) + + it('Only download action is actions available', () => { + getActionButtonForFile('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') + }) +}) |