aboutsummaryrefslogtreecommitdiffstats
path: root/cypress/e2e/files_sharing
diff options
context:
space:
mode:
Diffstat (limited to 'cypress/e2e/files_sharing')
-rw-r--r--cypress/e2e/files_sharing/FilesSharingUtils.ts199
-rw-r--r--cypress/e2e/files_sharing/ShareOptionsType.ts18
-rw-r--r--cypress/e2e/files_sharing/expiry-date.cy.ts128
-rw-r--r--cypress/e2e/files_sharing/file-request.cy.ts83
-rw-r--r--cypress/e2e/files_sharing/files-copy-move.cy.ts150
-rw-r--r--cypress/e2e/files_sharing/files-download.cy.ts102
-rw-r--r--cypress/e2e/files_sharing/files-shares-view.cy.ts59
-rw-r--r--cypress/e2e/files_sharing/filesSharingUtils.ts112
-rw-r--r--cypress/e2e/files_sharing/limit_to_same_group.cy.ts107
-rw-r--r--cypress/e2e/files_sharing/note-to-recipient.cy.ts92
-rw-r--r--cypress/e2e/files_sharing/public-share/PublicShareUtils.ts191
-rw-r--r--cypress/e2e/files_sharing/public-share/copy-move-files.cy.ts49
-rw-r--r--cypress/e2e/files_sharing/public-share/default-view.cy.ts102
-rw-r--r--cypress/e2e/files_sharing/public-share/download.cy.ts266
-rw-r--r--cypress/e2e/files_sharing/public-share/header-avatar.cy.ts193
-rw-r--r--cypress/e2e/files_sharing/public-share/header-menu.cy.ts199
-rw-r--r--cypress/e2e/files_sharing/public-share/rename-files.cy.ts32
-rw-r--r--cypress/e2e/files_sharing/public-share/required-before-create.cy.ts192
-rw-r--r--cypress/e2e/files_sharing/public-share/sidebar-tab.cy.ts45
-rw-r--r--cypress/e2e/files_sharing/public-share/view_file-drop.cy.ts172
-rw-r--r--cypress/e2e/files_sharing/public-share/view_view-only-no-download.cy.ts100
-rw-r--r--cypress/e2e/files_sharing/public-share/view_view-only.cy.ts103
-rw-r--r--cypress/e2e/files_sharing/share-status-action.cy.ts125
23 files changed, 2707 insertions, 112 deletions
diff --git a/cypress/e2e/files_sharing/FilesSharingUtils.ts b/cypress/e2e/files_sharing/FilesSharingUtils.ts
new file mode 100644
index 00000000000..c9b30bd576c
--- /dev/null
+++ b/cypress/e2e/files_sharing/FilesSharingUtils.ts
@@ -0,0 +1,199 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+/* eslint-disable jsdoc/require-jsdoc */
+import { triggerActionForFile } from '../files/FilesUtils'
+
+export interface ShareSetting {
+ read: boolean
+ update: boolean
+ delete: boolean
+ create: boolean
+ share: boolean
+ download: boolean
+ note: string
+ expiryDate: Date
+}
+
+export function createShare(fileName: string, username: string, shareSettings: Partial<ShareSetting> = {}) {
+ openSharingPanel(fileName)
+
+ cy.get('#app-sidebar-vue').within(() => {
+ cy.intercept({ times: 1, method: 'GET', url: '**/apps/files_sharing/api/v1/sharees?*' }).as('userSearch')
+ cy.findByRole('combobox', { name: /Search for internal recipients/i })
+ .type(`{selectAll}${username}`)
+ cy.wait('@userSearch')
+ })
+
+ cy.get(`[user="${username}"]`).click()
+
+ // HACK: Save the share and then update it, as permissions changes are currently not saved for new share.
+ cy.get('[data-cy-files-sharing-share-editor-action="save"]').click({ scrollBehavior: 'nearest' })
+ updateShare(fileName, 0, shareSettings)
+}
+
+export function openSharingDetails(index: number) {
+ cy.get('#app-sidebar-vue').within(() => {
+ cy.get('[data-cy-files-sharing-share-actions]').eq(index).click({ force: true })
+ cy.get('[data-cy-files-sharing-share-permissions-bundle="custom"]').click()
+ })
+}
+
+export function updateShare(fileName: string, index: number, shareSettings: Partial<ShareSetting> = {}) {
+ openSharingPanel(fileName)
+ openSharingDetails(index)
+
+ cy.intercept({ times: 1, method: 'PUT', url: '**/apps/files_sharing/api/v1/shares/*' }).as('updateShare')
+
+ cy.get('#app-sidebar-vue').within(() => {
+ if (shareSettings.download !== undefined) {
+ cy.get('[data-cy-files-sharing-share-permissions-checkbox="download"]').find('input').as('downloadCheckbox')
+ if (shareSettings.download) {
+ // Force:true because the checkbox is hidden by the pretty UI.
+ cy.get('@downloadCheckbox').check({ force: true, scrollBehavior: 'nearest' })
+ } else {
+ // Force:true because the checkbox is hidden by the pretty UI.
+ cy.get('@downloadCheckbox').uncheck({ force: true, scrollBehavior: 'nearest' })
+ }
+ }
+
+ if (shareSettings.read !== undefined) {
+ cy.get('[data-cy-files-sharing-share-permissions-checkbox="read"]').find('input').as('readCheckbox')
+ if (shareSettings.read) {
+ // Force:true because the checkbox is hidden by the pretty UI.
+ cy.get('@readCheckbox').check({ force: true, scrollBehavior: 'nearest' })
+ } else {
+ // Force:true because the checkbox is hidden by the pretty UI.
+ cy.get('@readCheckbox').uncheck({ force: true, scrollBehavior: 'nearest' })
+ }
+ }
+
+ if (shareSettings.update !== undefined) {
+ cy.get('[data-cy-files-sharing-share-permissions-checkbox="update"]').find('input').as('updateCheckbox')
+ if (shareSettings.update) {
+ // Force:true because the checkbox is hidden by the pretty UI.
+ cy.get('@updateCheckbox').check({ force: true, scrollBehavior: 'nearest' })
+ } else {
+ // Force:true because the checkbox is hidden by the pretty UI.
+ cy.get('@updateCheckbox').uncheck({ force: true, scrollBehavior: 'nearest' })
+ }
+ }
+
+ if (shareSettings.create !== undefined) {
+ cy.get('[data-cy-files-sharing-share-permissions-checkbox="create"]').find('input').as('createCheckbox')
+ if (shareSettings.create) {
+ // Force:true because the checkbox is hidden by the pretty UI.
+ cy.get('@createCheckbox').check({ force: true, scrollBehavior: 'nearest' })
+ } else {
+ // Force:true because the checkbox is hidden by the pretty UI.
+ cy.get('@createCheckbox').uncheck({ force: true, scrollBehavior: 'nearest' })
+ }
+ }
+
+ if (shareSettings.delete !== undefined) {
+ cy.get('[data-cy-files-sharing-share-permissions-checkbox="delete"]').find('input').as('deleteCheckbox')
+ if (shareSettings.delete) {
+ // Force:true because the checkbox is hidden by the pretty UI.
+ cy.get('@deleteCheckbox').check({ force: true, scrollBehavior: 'nearest' })
+ } else {
+ // Force:true because the checkbox is hidden by the pretty UI.
+ cy.get('@deleteCheckbox').uncheck({ force: true, scrollBehavior: 'nearest' })
+ }
+ }
+
+ if (shareSettings.note !== undefined) {
+ cy.findByRole('checkbox', { name: /note to recipient/i }).check({ force: true, scrollBehavior: 'nearest' })
+ cy.findByRole('textbox', { name: /note to recipient/i }).type(shareSettings.note)
+ }
+
+ if (shareSettings.expiryDate !== undefined) {
+ cy.findByRole('checkbox', { name: /expiration date/i })
+ .check({ force: true, scrollBehavior: 'nearest' })
+ cy.get('#share-date-picker')
+ .type(`${shareSettings.expiryDate.getFullYear()}-${String(shareSettings.expiryDate.getMonth() + 1).padStart(2, '0')}-${String(shareSettings.expiryDate.getDate()).padStart(2, '0')}`)
+ }
+
+ cy.get('[data-cy-files-sharing-share-editor-action="save"]').click({ scrollBehavior: 'nearest' })
+
+ cy.wait('@updateShare')
+ })
+ // close all toasts
+ cy.get('.toast-success').findAllByRole('button').click({ force: true, multiple: true })
+}
+
+export function openSharingPanel(fileName: string) {
+ triggerActionForFile(fileName, 'details')
+
+ cy.get('[data-cy-sidebar]')
+ .find('[aria-controls="tab-sharing"]')
+ .click()
+}
+
+type FileRequestOptions = {
+ label?: string
+ note?: string
+ password?: string
+ /* YYYY-MM-DD format */
+ expiration?: string
+}
+
+/**
+ * Create a file request for a folder
+ * @param path The path of the folder, leading slash is required
+ * @param options The options for the file request
+ */
+export const createFileRequest = (path: string, options: FileRequestOptions = {}) => {
+ if (!path.startsWith('/')) {
+ throw new Error('Path must start with a slash')
+ }
+
+ // Navigate to the folder
+ cy.visit('/apps/files/files?dir=' + path)
+
+ // Open the file request dialog
+ cy.get('[data-cy-upload-picker] .action-item__menutoggle').first().click()
+ cy.contains('.upload-picker__menu-entry button', 'Create file request').click()
+ cy.get('[data-cy-file-request-dialog]').should('be.visible')
+
+ // Check and fill the first page options
+ cy.get('[data-cy-file-request-dialog-fieldset="label"]').should('be.visible')
+ cy.get('[data-cy-file-request-dialog-fieldset="destination"]').should('be.visible')
+ cy.get('[data-cy-file-request-dialog-fieldset="note"]').should('be.visible')
+
+ cy.get('[data-cy-file-request-dialog-fieldset="destination"] input').should('contain.value', path)
+ if (options.label) {
+ cy.get('[data-cy-file-request-dialog-fieldset="label"] input').type(`{selectall}${options.label}`)
+ }
+ if (options.note) {
+ cy.get('[data-cy-file-request-dialog-fieldset="note"] textarea').type(`{selectall}${options.note}`)
+ }
+
+ // Go to the next page
+ cy.get('[data-cy-file-request-dialog-controls="next"]').click()
+ cy.get('[data-cy-file-request-dialog-fieldset="expiration"] input[type="checkbox"]').should('exist')
+ cy.get('[data-cy-file-request-dialog-fieldset="expiration"] input[type="date"]').should('not.exist')
+ cy.get('[data-cy-file-request-dialog-fieldset="password"] input[type="checkbox"]').should('exist')
+ cy.get('[data-cy-file-request-dialog-fieldset="password"] input[type="password"]').should('not.exist')
+ if (options.expiration) {
+ cy.get('[data-cy-file-request-dialog-fieldset="expiration"] input[type="checkbox"]').check({ force: true })
+ cy.get('[data-cy-file-request-dialog-fieldset="expiration"] input[type="date"]').type(`{selectall}${options.expiration}`)
+ }
+ if (options.password) {
+ cy.get('[data-cy-file-request-dialog-fieldset="password"] input[type="checkbox"]').check({ force: true })
+ cy.get('[data-cy-file-request-dialog-fieldset="password"] input[type="password"]').type(`{selectall}${options.password}`)
+ }
+
+ // Create the file request
+ cy.get('[data-cy-file-request-dialog-controls="next"]').click()
+
+ // Get the file request URL
+ cy.get('[data-cy-file-request-dialog-fieldset="link"]').then(($link) => {
+ const url = $link.val()
+ cy.log(`File request URL: ${url}`)
+ cy.wrap(url).as('fileRequestUrl')
+ })
+
+ // Close
+ cy.get('[data-cy-file-request-dialog-controls="finish"]').click()
+}
diff --git a/cypress/e2e/files_sharing/ShareOptionsType.ts b/cypress/e2e/files_sharing/ShareOptionsType.ts
new file mode 100644
index 00000000000..a6ce6922299
--- /dev/null
+++ b/cypress/e2e/files_sharing/ShareOptionsType.ts
@@ -0,0 +1,18 @@
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+export type ShareOptions = {
+ enforcePassword?: boolean
+ enforceExpirationDate?: boolean
+ alwaysAskForPassword?: boolean
+ defaultExpirationDateSet?: boolean
+}
+
+export const defaultShareOptions: ShareOptions = {
+ enforcePassword: false,
+ enforceExpirationDate: false,
+ alwaysAskForPassword: false,
+ defaultExpirationDateSet: false,
+}
diff --git a/cypress/e2e/files_sharing/expiry-date.cy.ts b/cypress/e2e/files_sharing/expiry-date.cy.ts
new file mode 100644
index 00000000000..f39a47309e2
--- /dev/null
+++ b/cypress/e2e/files_sharing/expiry-date.cy.ts
@@ -0,0 +1,128 @@
+/*!
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { User } from '@nextcloud/cypress'
+import { closeSidebar } from '../files/FilesUtils.ts'
+import { createShare, openSharingDetails, openSharingPanel, updateShare } from './FilesSharingUtils.ts'
+
+describe('files_sharing: Expiry date', () => {
+ const expectedDefaultDate = new Date(Date.now() + 2 * 24 * 60 * 60 * 1000)
+ const expectedDefaultDateString = `${expectedDefaultDate.getFullYear()}-${String(expectedDefaultDate.getMonth() + 1).padStart(2, '0')}-${String(expectedDefaultDate.getDate()).padStart(2, '0')}`
+ const fortnight = new Date(Date.now() + 14 * 24 * 60 * 60 * 1000)
+ const fortnightString = `${fortnight.getFullYear()}-${String(fortnight.getMonth() + 1).padStart(2, '0')}-${String(fortnight.getDate()).padStart(2, '0')}`
+
+ let alice: User
+ let bob: User
+
+ before(() => {
+ // Ensure we have the admin setting setup for default dates with 2 days in the future
+ cy.runOccCommand('config:app:set --value yes core shareapi_default_internal_expire_date')
+ cy.runOccCommand('config:app:set --value 2 core shareapi_internal_expire_after_n_days')
+
+ cy.createRandomUser().then((user) => {
+ alice = user
+ cy.login(alice)
+ })
+ cy.createRandomUser().then((user) => {
+ bob = user
+ })
+ })
+
+ after(() => {
+ cy.runOccCommand('config:app:delete core shareapi_default_internal_expire_date')
+ cy.runOccCommand('config:app:delete core shareapi_enforce_internal_expire_date')
+ cy.runOccCommand('config:app:delete core shareapi_internal_expire_after_n_days')
+ })
+
+ beforeEach(() => {
+ cy.runOccCommand('config:app:delete core shareapi_enforce_internal_expire_date')
+ })
+
+ it('See default expiry date is set and enforced', () => {
+ // Enforce the date
+ cy.runOccCommand('config:app:set --value yes core shareapi_enforce_internal_expire_date')
+ const dir = 'defaultExpiryDateEnforced'
+ prepareDirectory(dir)
+
+ validateExpiryDate(dir, expectedDefaultDateString)
+ cy.findByRole('checkbox', { name: /expiration date/i })
+ .should('be.checked')
+ .and('be.disabled')
+ })
+
+ it('See default expiry date is set also if not enforced', () => {
+ const dir = 'defaultExpiryDate'
+ prepareDirectory(dir)
+
+ validateExpiryDate(dir, expectedDefaultDateString)
+ cy.findByRole('checkbox', { name: /expiration date/i })
+ .should('be.checked')
+ .and('not.be.disabled')
+ .check({ force: true, scrollBehavior: 'nearest' })
+ })
+
+ it('Can set custom expiry date', () => {
+ const dir = 'customExpiryDate'
+ prepareDirectory(dir)
+ updateShare(dir, 0, { expiryDate: fortnight })
+ validateExpiryDate(dir, fortnightString)
+ })
+
+ it('Custom expiry date survives reload', () => {
+ const dir = 'customExpiryDateReload'
+ prepareDirectory(dir)
+ updateShare(dir, 0, { expiryDate: fortnight })
+ validateExpiryDate(dir, fortnightString)
+
+ cy.visit('/apps/files')
+ validateExpiryDate(dir, fortnightString)
+ })
+
+ /**
+ * Regression test for https://github.com/nextcloud/server/pull/50192
+ * Ensure that admin default settings do not always override the user set value.
+ */
+ it('Custom expiry date survives unrelated update', () => {
+ const dir = 'customExpiryUnrelatedChanges'
+ prepareDirectory(dir)
+ updateShare(dir, 0, { expiryDate: fortnight })
+ validateExpiryDate(dir, fortnightString)
+
+ closeSidebar()
+ updateShare(dir, 0, { note: 'Only note changed' })
+ validateExpiryDate(dir, fortnightString)
+
+ cy.visit('/apps/files')
+ validateExpiryDate(dir, fortnightString)
+ })
+
+ /**
+ * Prepare directory, login and share to bob
+ *
+ * @param name The directory name
+ */
+ function prepareDirectory(name: string) {
+ cy.mkdir(alice, `/${name}`)
+ cy.login(alice)
+ cy.visit('/apps/files')
+ createShare(name, bob.userId)
+ }
+
+ /**
+ * Validate expiry date on a share
+ *
+ * @param filename The filename to validate
+ * @param expectedDate The expected date in YYYY-MM-dd
+ */
+ function validateExpiryDate(filename: string, expectedDate: string) {
+ openSharingPanel(filename)
+ openSharingDetails(0)
+
+ cy.get('#share-date-picker')
+ .should('exist')
+ .and('have.value', expectedDate)
+ }
+
+})
diff --git a/cypress/e2e/files_sharing/file-request.cy.ts b/cypress/e2e/files_sharing/file-request.cy.ts
new file mode 100644
index 00000000000..578f72fa0b5
--- /dev/null
+++ b/cypress/e2e/files_sharing/file-request.cy.ts
@@ -0,0 +1,83 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import type { User } from '@nextcloud/cypress'
+import { createFolder, getRowForFile, navigateToFolder } from '../files/FilesUtils'
+import { createFileRequest } from './FilesSharingUtils'
+
+const enterGuestName = (name: string) => {
+ cy.findByRole('dialog', { name: /Upload files to/ })
+ .should('be.visible')
+ .within(() => {
+ cy.findByRole('textbox', { name: 'Name' })
+ .should('be.visible')
+
+ cy.findByRole('textbox', { name: 'Name' })
+ .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 = ''
+
+ it('Login with a user and create a file request', () => {
+ cy.createRandomUser().then((_user) => {
+ user = _user
+ cy.login(user)
+ })
+
+ cy.visit('/apps/files')
+ createFolder(folderName)
+
+ createFileRequest(`/${folderName}`)
+ cy.get('@fileRequestUrl').should('contain', '/s/').then((_url: string) => {
+ cy.logout()
+ url = _url
+ })
+ })
+
+ it('Open the file request as a guest', () => {
+ cy.visit(url)
+ enterGuestName('Guest')
+
+ // Check various elements on the page
+ 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('[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)
+ })
+
+ it('Check the uploaded file', () => {
+ cy.login(user)
+ cy.visit(`/apps/files/files?dir=/${folderName}`)
+ getRowForFile('Guest')
+ .should('be.visible')
+ navigateToFolder('Guest')
+ getRowForFile('file.txt').should('be.visible')
+ })
+})
diff --git a/cypress/e2e/files_sharing/files-copy-move.cy.ts b/cypress/e2e/files_sharing/files-copy-move.cy.ts
new file mode 100644
index 00000000000..6ad01cb2219
--- /dev/null
+++ b/cypress/e2e/files_sharing/files-copy-move.cy.ts
@@ -0,0 +1,150 @@
+/**
+ * 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.ts'
+import {
+ getRowForFile,
+ copyFile,
+ navigateToFolder,
+ triggerActionForFile,
+} from '../files/FilesUtils.ts'
+import { ACTION_COPY_MOVE } from '../../../apps/files/src/actions/moveOrCopyAction.ts'
+
+export const copyFileForbidden = (fileName: string, dirPath: string) => {
+ getRowForFile(fileName).should('be.visible')
+ triggerActionForFile(fileName, ACTION_COPY_MOVE)
+
+ cy.get('.file-picker').within(() => {
+ // intercept the copy so we can wait for it
+ cy.intercept('COPY', /\/(remote|public)\.php\/dav\/files\//).as('copyFile')
+
+ const directories = dirPath.split('/')
+ directories.forEach((directory) => {
+ // select the folder
+ cy.get(`[data-filename="${CSS.escape(directory)}"]`).should('be.visible').click()
+ })
+
+ // check copy button
+ cy.contains('button', `Copy to ${directories.at(-1)}`).should('be.disabled')
+ })
+}
+
+export const moveFileForbidden = (fileName: string, dirPath: string) => {
+ getRowForFile(fileName).should('be.visible')
+ triggerActionForFile(fileName, ACTION_COPY_MOVE)
+
+ cy.get('.file-picker').within(() => {
+ // intercept the copy so we can wait for it
+ cy.intercept('MOVE', /\/(remote|public)\.php\/dav\/files\//).as('moveFile')
+
+ // select home folder
+ cy.get('button[title="Home"]').should('be.visible').click()
+
+ const directories = dirPath.split('/')
+ directories.forEach((directory) => {
+ // select the folder
+ cy.get(`[data-filename="${directory}"]`).should('be.visible').click()
+ })
+
+ // click move
+ cy.contains('button', `Move to ${directories.at(-1)}`).should('not.exist')
+ })
+}
+
+describe('files_sharing: Move or copy files', { testIsolation: true }, () => {
+ let user: User
+ let sharee: User
+
+ beforeEach(() => {
+ cy.createRandomUser().then(($user) => {
+ user = $user
+ })
+ cy.createRandomUser().then(($user) => {
+ sharee = $user
+ })
+ })
+
+ it('can create a file in a shared folder', () => {
+ // share the folder
+ cy.mkdir(user, '/folder')
+ cy.login(user)
+ cy.visit('/apps/files')
+ createShare('folder', sharee.userId, { read: true, download: true })
+ cy.logout()
+
+ // Now for the sharee
+ cy.uploadContent(sharee, new Blob([]), 'text/plain', '/folder/file.txt')
+ cy.login(sharee)
+ // visit shared files view
+ cy.visit('/apps/files')
+ // see the shared folder
+ getRowForFile('folder').should('be.visible')
+ navigateToFolder('folder')
+ // Content of the shared folder
+ getRowForFile('file.txt').should('be.visible')
+ })
+
+ it('can copy a file to a shared folder', () => {
+ // share the folder
+ cy.mkdir(user, '/folder')
+ cy.login(user)
+ cy.visit('/apps/files')
+ createShare('folder', sharee.userId, { read: true, download: true })
+ cy.logout()
+
+ // Now for the sharee
+ cy.uploadContent(sharee, new Blob([]), 'text/plain', '/file.txt')
+ cy.login(sharee)
+ // visit shared files view
+ cy.visit('/apps/files')
+ // see the shared folder
+ getRowForFile('folder').should('be.visible')
+ // copy file to a shared folder
+ copyFile('file.txt', 'folder')
+ // click on the folder should open it in files
+ navigateToFolder('folder')
+ // Content of the shared folder
+ getRowForFile('file.txt').should('be.visible')
+ })
+
+ it('can not copy a file to a shared folder with no create permissions', () => {
+ // share the folder
+ cy.mkdir(user, '/folder')
+ cy.login(user)
+ cy.visit('/apps/files')
+ createShare('folder', sharee.userId, { read: true, download: true, create: false })
+ cy.logout()
+
+ // Now for the sharee
+ cy.uploadContent(sharee, new Blob([]), 'text/plain', '/file.txt')
+ cy.login(sharee)
+ // visit shared files view
+ cy.visit('/apps/files')
+ // see the shared folder
+ getRowForFile('folder').should('be.visible')
+ copyFileForbidden('file.txt', 'folder')
+ })
+
+ it('can not move a file from a shared folder with no delete permissions', () => {
+ // share the folder
+ cy.mkdir(user, '/folder')
+ cy.uploadContent(user, new Blob([]), 'text/plain', '/folder/file.txt')
+ cy.login(user)
+ cy.visit('/apps/files')
+ createShare('folder', sharee.userId, { read: true, download: true, delete: false })
+ cy.logout()
+
+ // Now for the sharee
+ cy.mkdir(sharee, '/folder-own')
+ cy.login(sharee)
+ // visit shared files view
+ cy.visit('/apps/files')
+ // see the shared folder
+ getRowForFile('folder').should('be.visible')
+ navigateToFolder('folder')
+ getRowForFile('file.txt').should('be.visible')
+ moveFileForbidden('file.txt', 'folder-own')
+ })
+})
diff --git a/cypress/e2e/files_sharing/files-download.cy.ts b/cypress/e2e/files_sharing/files-download.cy.ts
new file mode 100644
index 00000000000..97ea91b7647
--- /dev/null
+++ b/cypress/e2e/files_sharing/files-download.cy.ts
@@ -0,0 +1,102 @@
+/**
+ * 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.ts'
+import {
+ getActionButtonForFile,
+ getActionEntryForFile,
+ getRowForFile,
+} from '../files/FilesUtils.ts'
+
+describe('files_sharing: Download forbidden', { testIsolation: true }, () => {
+ let user: User
+ let sharee: User
+
+ beforeEach(() => {
+ cy.runOccCommand('config:app:set --value yes core shareapi_allow_view_without_download')
+ cy.createRandomUser().then(($user) => {
+ user = $user
+ })
+ cy.createRandomUser().then(($user) => {
+ sharee = $user
+ })
+ })
+
+ after(() => {
+ cy.runOccCommand('config:app:delete core shareapi_allow_view_without_download')
+ })
+
+ it('cannot download a folder if disabled', () => {
+ // share the folder
+ cy.mkdir(user, '/folder')
+ cy.login(user)
+ cy.visit('/apps/files')
+ createShare('folder', sharee.userId, { read: true, download: false })
+ cy.logout()
+
+ // Now for the sharee
+ cy.login(sharee)
+
+ // visit shared files view
+ cy.visit('/apps/files')
+ // see the shared folder
+ getActionButtonForFile('folder')
+ .should('be.visible')
+ // open the action menu
+ .click({ force: true })
+ // see no download action
+ getActionEntryForFile('folder', 'download')
+ .should('not.exist')
+
+ // Disable view without download option
+ cy.runOccCommand('config:app:set --value no core shareapi_allow_view_without_download')
+
+ // visit shared files view
+ cy.visit('/apps/files')
+ // see the shared folder
+ getRowForFile('folder').should('be.visible')
+ getActionButtonForFile('folder')
+ .should('be.visible')
+ // open the action menu
+ .click({ force: true })
+ getActionEntryForFile('folder', 'download').should('not.exist')
+ })
+
+ it('cannot download a file if disabled', () => {
+ // share the folder
+ cy.uploadContent(user, new Blob([]), 'text/plain', '/file.txt')
+ cy.login(user)
+ cy.visit('/apps/files')
+ createShare('file.txt', sharee.userId, { read: true, download: false })
+ cy.logout()
+
+ // Now for the sharee
+ cy.login(sharee)
+
+ // visit shared files view
+ cy.visit('/apps/files')
+ // see the shared folder
+ getActionButtonForFile('file.txt')
+ .should('be.visible')
+ // open the action menu
+ .click({ force: true })
+ // see no download action
+ getActionEntryForFile('file.txt', 'download')
+ .should('not.exist')
+
+ // Disable view without download option
+ cy.runOccCommand('config:app:set --value no core shareapi_allow_view_without_download')
+
+ // visit shared files view
+ cy.visit('/apps/files')
+ // see the shared folder
+ getRowForFile('file.txt').should('be.visible')
+ getActionButtonForFile('file.txt')
+ .should('be.visible')
+ // open the action menu
+ .click({ force: true })
+ getActionEntryForFile('file.txt', 'download').should('not.exist')
+ })
+})
diff --git a/cypress/e2e/files_sharing/files-shares-view.cy.ts b/cypress/e2e/files_sharing/files-shares-view.cy.ts
new file mode 100644
index 00000000000..12a67d9ee0f
--- /dev/null
+++ b/cypress/e2e/files_sharing/files-shares-view.cy.ts
@@ -0,0 +1,59 @@
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import type { User } from '@nextcloud/cypress'
+import { createShare } from './FilesSharingUtils.ts'
+import { getRowForFile } from '../files/FilesUtils.ts'
+
+describe('files_sharing: Files view', { testIsolation: true }, () => {
+ let user: User
+ let sharee: User
+
+ beforeEach(() => {
+ cy.createRandomUser().then(($user) => {
+ user = $user
+ })
+ cy.createRandomUser().then(($user) => {
+ sharee = $user
+ })
+ })
+
+ /**
+ * Regression test of https://github.com/nextcloud/server/issues/46108
+ */
+ it('opens a shared folder when clicking on it', () => {
+ cy.mkdir(user, '/folder')
+ cy.uploadContent(user, new Blob([]), 'text/plain', '/folder/file')
+ cy.login(user)
+ cy.visit('/apps/files')
+
+ // share the folder
+ createShare('folder', sharee.userId, { read: true, download: true })
+ // visit the own shares
+ cy.visit('/apps/files/sharingout')
+ // see the shared folder
+ getRowForFile('folder').should('be.visible')
+ // click on the folder should open it in files
+ getRowForFile('folder').findByRole('button', { name: /open in files/i }).click()
+ // See the URL has changed
+ cy.url().should('match', /apps\/files\/files\/.+dir=\/folder/)
+ // Content of the shared folder
+ getRowForFile('file').should('be.visible')
+
+ cy.logout()
+ // Now for the sharee
+ cy.login(sharee)
+
+ // visit shared files view
+ cy.visit('/apps/files/sharingin')
+ // see the shared folder
+ getRowForFile('folder').should('be.visible')
+ // click on the folder should open it in files
+ getRowForFile('folder').findByRole('button', { name: /open in files/i }).click()
+ // See the URL has changed
+ cy.url().should('match', /apps\/files\/files\/.+dir=\/folder/)
+ // Content of the shared folder
+ getRowForFile('file').should('be.visible')
+ })
+})
diff --git a/cypress/e2e/files_sharing/filesSharingUtils.ts b/cypress/e2e/files_sharing/filesSharingUtils.ts
deleted file mode 100644
index cb407153380..00000000000
--- a/cypress/e2e/files_sharing/filesSharingUtils.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-/* eslint-disable jsdoc/require-jsdoc */
-/**
- * @copyright Copyright (c) 2024 Louis Chemineau <louis@chmn.me>
- *
- * @author Louis Chemineau <louis@chmn.me>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-import { triggerActionForFile } from '../files/FilesUtils'
-
-export interface ShareSetting {
- read: boolean
- update: boolean
- delete: boolean
- share: boolean
- download: boolean
-}
-
-export function createShare(fileName: string, username: string, shareSettings: Partial<ShareSetting> = {}) {
- openSharingPanel(fileName)
-
- cy.get('#app-sidebar-vue').within(() => {
- cy.get('#sharing-search-input').clear()
- cy.intercept({ times: 1, method: 'GET', url: '**/apps/files_sharing/api/v1/sharees?*' }).as('userSearch')
- cy.get('#sharing-search-input').type(username)
- cy.wait('@userSearch')
- })
-
- cy.get(`[user="${username}"]`).click()
-
- // HACK: Save the share and then update it, as permissions changes are currently not saved for new share.
- cy.get('[data-cy-files-sharing-share-editor-action="save"]').click({ scrollBehavior: 'nearest' })
- updateShare(fileName, 0, shareSettings)
-}
-
-export function updateShare(fileName: string, index: number, shareSettings: Partial<ShareSetting> = {}) {
- openSharingPanel(fileName)
-
- cy.get('#app-sidebar-vue').within(() => {
- cy.get('[data-cy-files-sharing-share-actions]').eq(index).click()
- cy.get('[data-cy-files-sharing-share-permissions-bundle="custom"]').click()
-
- if (shareSettings.download !== undefined) {
- cy.get('[data-cy-files-sharing-share-permissions-checkbox="download"]').find('input').as('downloadCheckbox')
- if (shareSettings.download) {
- // Force:true because the checkbox is hidden by the pretty UI.
- cy.get('@downloadCheckbox').check({ force: true, scrollBehavior: 'nearest' })
- } else {
- // Force:true because the checkbox is hidden by the pretty UI.
- cy.get('@downloadCheckbox').uncheck({ force: true, scrollBehavior: 'nearest' })
- }
- }
-
- if (shareSettings.read !== undefined) {
- cy.get('[data-cy-files-sharing-share-permissions-checkbox="read"]').find('input').as('readCheckbox')
- if (shareSettings.read) {
- // Force:true because the checkbox is hidden by the pretty UI.
- cy.get('@readCheckbox').check({ force: true, scrollBehavior: 'nearest' })
- } else {
- // Force:true because the checkbox is hidden by the pretty UI.
- cy.get('@readCheckbox').uncheck({ force: true, scrollBehavior: 'nearest' })
- }
- }
-
- if (shareSettings.update !== undefined) {
- cy.get('[data-cy-files-sharing-share-permissions-checkbox="update"]').find('input').as('updateCheckbox')
- if (shareSettings.update) {
- // Force:true because the checkbox is hidden by the pretty UI.
- cy.get('@updateCheckbox').check({ force: true, scrollBehavior: 'nearest' })
- } else {
- // Force:true because the checkbox is hidden by the pretty UI.
- cy.get('@updateCheckbox').uncheck({ force: true, scrollBehavior: 'nearest' })
- }
- }
-
- if (shareSettings.delete !== undefined) {
- cy.get('[data-cy-files-sharing-share-permissions-checkbox="delete"]').find('input').as('deleteCheckbox')
- if (shareSettings.delete) {
- // Force:true because the checkbox is hidden by the pretty UI.
- cy.get('@deleteCheckbox').check({ force: true, scrollBehavior: 'nearest' })
- } else {
- // Force:true because the checkbox is hidden by the pretty UI.
- cy.get('@deleteCheckbox').uncheck({ force: true, scrollBehavior: 'nearest' })
- }
- }
-
- cy.get('[data-cy-files-sharing-share-editor-action="save"]').click({ scrollBehavior: 'nearest' })
- })
-}
-
-export function openSharingPanel(fileName: string) {
- triggerActionForFile(fileName, 'details')
-
- cy.get('#app-sidebar-vue')
- .get('[aria-controls="tab-sharing"]')
- .click()
-}
diff --git a/cypress/e2e/files_sharing/limit_to_same_group.cy.ts b/cypress/e2e/files_sharing/limit_to_same_group.cy.ts
new file mode 100644
index 00000000000..c95efa089ff
--- /dev/null
+++ b/cypress/e2e/files_sharing/limit_to_same_group.cy.ts
@@ -0,0 +1,107 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { User } from "@nextcloud/cypress"
+import { createShare } from "./FilesSharingUtils.ts"
+
+describe('Limit to sharing to people in the same group', () => {
+ let alice: User
+ let bob: User
+ let randomFileName1 = ''
+ let randomFileName2 = ''
+ let randomGroupName = ''
+ let randomGroupName2 = ''
+ let randomGroupName3 = ''
+
+ before(() => {
+ randomFileName1 = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + '.txt'
+ randomFileName2 = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + '.txt'
+ randomGroupName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10)
+ randomGroupName2 = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10)
+ randomGroupName3 = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10)
+
+ cy.runOccCommand('config:app:set core shareapi_only_share_with_group_members --value yes')
+
+ cy.createRandomUser()
+ .then(user => {
+ alice = user
+ cy.createRandomUser()
+ })
+ .then(user => {
+ bob = user
+
+ cy.runOccCommand(`group:add ${randomGroupName}`)
+ cy.runOccCommand(`group:add ${randomGroupName2}`)
+ cy.runOccCommand(`group:add ${randomGroupName3}`)
+ cy.runOccCommand(`group:adduser ${randomGroupName} ${alice.userId}`)
+ cy.runOccCommand(`group:adduser ${randomGroupName} ${bob.userId}`)
+ cy.runOccCommand(`group:adduser ${randomGroupName2} ${alice.userId}`)
+ cy.runOccCommand(`group:adduser ${randomGroupName2} ${bob.userId}`)
+ cy.runOccCommand(`group:adduser ${randomGroupName3} ${bob.userId}`)
+
+ cy.uploadContent(alice, new Blob(['share to bob'], { type: 'text/plain' }), 'text/plain', `/${randomFileName1}`)
+ cy.uploadContent(bob, new Blob(['share by bob'], { type: 'text/plain' }), 'text/plain', `/${randomFileName2}`)
+
+ cy.login(alice)
+ cy.visit('/apps/files')
+ createShare(randomFileName1, bob.userId)
+ cy.login(bob)
+ cy.visit('/apps/files')
+ createShare(randomFileName2, alice.userId)
+ })
+ })
+
+ after(() => {
+ cy.runOccCommand('config:app:set core shareapi_only_share_with_group_members --value no')
+ })
+
+ it('Alice can see the shared file', () => {
+ cy.login(alice)
+ cy.visit('/apps/files')
+ cy.get(`[data-cy-files-list] [data-cy-files-list-row-name="${randomFileName2}"]`).should('exist')
+ })
+
+ it('Bob can see the shared file', () => {
+ cy.login(alice)
+ cy.visit('/apps/files')
+ cy.get(`[data-cy-files-list] [data-cy-files-list-row-name="${randomFileName1}"]`).should('exist')
+ })
+
+ context('Bob is removed from the first group', () => {
+ before(() => {
+ cy.runOccCommand(`group:removeuser ${randomGroupName} ${bob.userId}`)
+ })
+
+ it('Alice can see the shared file', () => {
+ cy.login(alice)
+ cy.visit('/apps/files')
+ cy.get(`[data-cy-files-list] [data-cy-files-list-row-name="${randomFileName2}"]`).should('exist')
+ })
+
+ it('Bob can see the shared file', () => {
+ cy.login(alice)
+ cy.visit('/apps/files')
+ cy.get(`[data-cy-files-list] [data-cy-files-list-row-name="${randomFileName1}"]`).should('exist')
+ })
+ })
+
+ context('Bob is removed from the second group', () => {
+ before(() => {
+ cy.runOccCommand(`group:removeuser ${randomGroupName2} ${bob.userId}`)
+ })
+
+ it('Alice cannot see the shared file', () => {
+ cy.login(alice)
+ cy.visit('/apps/files')
+ cy.get(`[data-cy-files-list] [data-cy-files-list-row-name="${randomFileName2}"]`).should('not.exist')
+ })
+
+ it('Bob cannot see the shared file', () => {
+ cy.login(alice)
+ cy.visit('/apps/files')
+ cy.get(`[data-cy-files-list] [data-cy-files-list-row-name="${randomFileName1}"]`).should('not.exist')
+ })
+ })
+})
diff --git a/cypress/e2e/files_sharing/note-to-recipient.cy.ts b/cypress/e2e/files_sharing/note-to-recipient.cy.ts
new file mode 100644
index 00000000000..08fee587d9a
--- /dev/null
+++ b/cypress/e2e/files_sharing/note-to-recipient.cy.ts
@@ -0,0 +1,92 @@
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import type { User } from '@nextcloud/cypress'
+import { createShare, openSharingPanel } from './FilesSharingUtils.ts'
+import { navigateToFolder } from '../files/FilesUtils.ts'
+
+describe('files_sharing: Note to recipient', { testIsolation: true }, () => {
+ let user: User
+ let sharee: User
+
+ beforeEach(() => {
+ cy.createRandomUser().then(($user) => {
+ user = $user
+ })
+ cy.createRandomUser().then(($user) => {
+ sharee = $user
+ })
+ })
+
+ it('displays the note to the sharee', () => {
+ cy.mkdir(user, '/folder')
+ cy.uploadContent(user, new Blob([]), 'text/plain', '/folder/file')
+ cy.login(user)
+ cy.visit('/apps/files')
+
+ // share the folder
+ createShare('folder', sharee.userId, { read: true, download: true, note: 'Hello, this is the note.' })
+
+ cy.logout()
+ // Now for the sharee
+ cy.login(sharee)
+
+ // visit shared files view
+ cy.visit('/apps/files')
+ navigateToFolder('folder')
+ cy.get('.note-to-recipient')
+ .should('be.visible')
+ .and('contain.text', 'Hello, this is the note.')
+ })
+
+ it('displays the note to the sharee even if the file list is empty', () => {
+ cy.mkdir(user, '/folder')
+ cy.login(user)
+ cy.visit('/apps/files')
+
+ // share the folder
+ createShare('folder', sharee.userId, { read: true, download: true, note: 'Hello, this is the note.' })
+
+ cy.logout()
+ // Now for the sharee
+ cy.login(sharee)
+
+ // visit shared files view
+ cy.visit('/apps/files')
+ navigateToFolder('folder')
+ cy.get('.note-to-recipient')
+ .should('be.visible')
+ .and('contain.text', 'Hello, this is the note.')
+ })
+
+ /**
+ * Regression test for https://github.com/nextcloud/server/issues/46188
+ */
+ it('shows an existing note when editing a share', () => {
+ cy.mkdir(user, '/folder')
+ cy.login(user)
+ cy.visit('/apps/files')
+
+ // share the folder
+ createShare('folder', sharee.userId, { read: true, download: true, note: 'Hello, this is the note.' })
+
+ // reload just to be sure
+ cy.visit('/apps/files')
+
+ // open the sharing tab
+ openSharingPanel('folder')
+
+ cy.get('[data-cy-sidebar]').within(() => {
+ // Open the share
+ cy.get('[data-cy-files-sharing-share-actions]').first().click({ force: true })
+
+ cy.findByRole('checkbox', { name: /note to recipient/i })
+ .and('be.checked')
+ cy.findByRole('textbox', { name: /note to recipient/i })
+ .should('be.visible')
+ .and('have.value', 'Hello, this is the note.')
+ })
+ })
+
+})
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 &#39;
+ */
+ 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')
+ })
+})
diff --git a/cypress/e2e/files_sharing/share-status-action.cy.ts b/cypress/e2e/files_sharing/share-status-action.cy.ts
new file mode 100644
index 00000000000..f02ec676573
--- /dev/null
+++ b/cypress/e2e/files_sharing/share-status-action.cy.ts
@@ -0,0 +1,125 @@
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import type { User } from '@nextcloud/cypress'
+import { createShare } from './FilesSharingUtils.ts'
+import { closeSidebar, enableGridMode, getActionButtonForFile, getInlineActionEntryForFile, getRowForFile } from '../files/FilesUtils.ts'
+
+describe('files_sharing: Sharing status action', { testIsolation: true }, () => {
+ /**
+ * Regression test of https://github.com/nextcloud/server/issues/45723
+ */
+ it('No "shared" tag when user ID is purely numerical but there are no shares', () => {
+ const user = {
+ language: 'en',
+ password: 'test1234',
+ userId: String(Math.floor(Math.random() * 1000)),
+ } as User
+ cy.createUser(user)
+ cy.mkdir(user, '/folder')
+ cy.login(user)
+
+ cy.visit('/apps/files')
+
+ getRowForFile('folder')
+ .should('be.visible')
+ .find('[data-cy-files-list-row-actions]')
+ .findByRole('button', { name: 'Shared' })
+ .should('not.exist')
+ })
+
+ it('Render quick option for sharing', () => {
+ cy.createRandomUser().then((user) => {
+ cy.mkdir(user, '/folder')
+ cy.login(user)
+
+ cy.visit('/apps/files')
+ })
+
+ getRowForFile('folder')
+ .should('be.visible')
+ .find('[data-cy-files-list-row-actions]')
+ .findByRole('button', { name: /Sharing options/ })
+ .should('be.visible')
+ .click()
+
+ // check the click opened the sidebar
+ cy.get('[data-cy-sidebar]')
+ .should('be.visible')
+ // and ensure the sharing tab is selected
+ .findByRole('tab', { name: 'Sharing', selected: true })
+ .should('exist')
+ })
+
+ describe('Sharing inline status action handling', () => {
+ let user: User
+ let sharee: User
+
+ before(() => {
+ cy.createRandomUser().then(($user) => {
+ sharee = $user
+ })
+ cy.createRandomUser().then(($user) => {
+ user = $user
+ cy.mkdir(user, '/folder')
+ cy.login(user)
+
+ cy.visit('/apps/files')
+ getRowForFile('folder').should('be.visible')
+
+ createShare('folder', sharee.userId)
+ closeSidebar()
+ })
+ cy.logout()
+ })
+
+ it('Render inline status action for sharer', () => {
+ cy.login(user)
+ cy.visit('/apps/files')
+
+ getInlineActionEntryForFile('folder', 'sharing-status')
+ .should('have.attr', 'aria-label', `Shared with ${sharee.userId}`)
+ .should('have.attr', 'title', `Shared with ${sharee.userId}`)
+ .should('be.visible')
+ })
+
+ it('Render status action in gridview for sharer', () => {
+ cy.login(user)
+ cy.visit('/apps/files')
+ enableGridMode()
+
+ getRowForFile('folder')
+ .should('be.visible')
+ getActionButtonForFile('folder')
+ .click()
+ cy.findByRole('menu')
+ .findByRole('menuitem', { name: /shared with/i })
+ .should('be.visible')
+ })
+
+ it('Render inline status action for sharee', () => {
+ cy.login(sharee)
+ cy.visit('/apps/files')
+
+ getInlineActionEntryForFile('folder', 'sharing-status')
+ .should('have.attr', 'aria-label', `Shared by ${user.userId}`)
+ .should('be.visible')
+ })
+
+ it('Render status action in grid view for sharee', () => {
+ cy.login(sharee)
+ cy.visit('/apps/files')
+
+ enableGridMode()
+
+ getRowForFile('folder')
+ .should('be.visible')
+ getActionButtonForFile('folder')
+ .click()
+ cy.findByRole('menu')
+ .findByRole('menuitem', { name: `Shared by ${user.userId}` })
+ .should('be.visible')
+ })
+ })
+})