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