diff --git a/apps/files_sharing/src/components/SharingEntryLink.vue b/apps/files_sharing/src/components/SharingEntryLink.vue index ab5e049e187de..de2551518628c 100644 --- a/apps/files_sharing/src/components/SharingEntryLink.vue +++ b/apps/files_sharing/src/components/SharingEntryLink.vue @@ -87,7 +87,7 @@ :disabled="pendingEnforcedExpirationDate || saving" class="share-link-expiration-date-checkbox" @change="onDefaultExpirationDateEnabledChange"> - {{ config.enforcePasswordForPublicLink ? t('files_sharing', 'Enable link expiration (enforced)') : t('files_sharing', 'Enable link expiration') }} + {{ config.isDefaultExpireDateEnforced ? t('files_sharing', 'Enable link expiration (enforced)') : t('files_sharing', 'Enable link expiration') }} diff --git a/cypress/e2e/files_sharing/ShareOptionsType.ts b/cypress/e2e/files_sharing/ShareOptionsType.ts new file mode 100644 index 0000000000000..a6ce69222995d --- /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/public-share/required-before-create.cy.ts b/cypress/e2e/files_sharing/public-share/required-before-create.cy.ts new file mode 100644 index 0000000000000..d0222f087e1d4 --- /dev/null +++ b/cypress/e2e/files_sharing/public-share/required-before-create.cy.ts @@ -0,0 +1,187 @@ +/*! + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { ShareContext } from './setup-public-share.ts' +import type { ShareOptions } from '../ShareOptionsType.ts' +import { setupData, createShare } from './setup-public-share.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): 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, shareName) + createShare(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, shareName) + createShare(shareContext, shareName).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, + enforceExpirationDate: true, + } + applyShareOptions(shareOptions) + const shareName = 'defaultPasswordExpireEnforced' + setupData(shareContext, shareName) + createShare(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, shareName) + createShare(shareContext, shareName).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, shareName) + createShare(shareContext, shareName, shareOptions).then((shareUrl) => { + shareContext.url = shareUrl + cy.log(`Created share with URL: ${shareUrl}`) + }) + }) + + it('Checks if user can create share with both password and expiration date not enforced, but defaults set', () => { + const shareOptions : ShareOptions = { + enforcePassword: false, + enforceExpirationDate: false, + defaultExpirationDateSet: true, + alwaysAskForPassword: true, + } + const shareName = 'defaultPasswordExpireNotEnforced' + setupData(shareContext, shareName) + createShare(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, shareName) + createShare(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 = { + enforcePassword: false, + defaultExpirationDateSet: true, + } + applyShareOptions(shareOptions) + const shareName = 'defaultExpireNoPasswordEnforced' + setupData(shareContext, shareName) + createShare(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, shareName) + createShare(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, shareName) + createShare(shareContext, shareName, null).then((shareUrl) => { + shareContext.url = shareUrl + cy.log(`Created share with URL: ${shareUrl}`) + }) + }) + +}) diff --git a/cypress/e2e/files_sharing/public-share/setup-public-share.ts b/cypress/e2e/files_sharing/public-share/setup-public-share.ts index 5e23357a8215c..8f3bc303208e4 100644 --- a/cypress/e2e/files_sharing/public-share/setup-public-share.ts +++ b/cypress/e2e/files_sharing/public-share/setup-public-share.ts @@ -3,91 +3,141 @@ * 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' -let user: User -let url: string +export interface ShareContext { + user: User + url?: string +} + +const defaultShareContext: ShareContext = { + user: {} as User, + url: undefined, +} /** - * URL of the share + * 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() { - if (url === undefined) { +export function getShareUrl(context: ShareContext = defaultShareContext): string { + if (!context.url) { throw new Error('You need to setup the share first!') } - return url + return context.url } /** * Setup the available data + * @param user The current share context * @param shareName The name of the shared folder */ -function setupData(shareName: string) { +export function setupData(user: User, shareName: string): void { cy.mkdir(user, `/${shareName}`) cy.mkdir(user, `/${shareName}/subfolder`) cy.uploadContent(user, new Blob(['foo']), 'text/plain', `/${shareName}/foo.txt`) cy.uploadContent(user, new Blob(['bar']), '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') + cy.contains('Enter a password') + .should('exist') + .and('not.be.disabled') + } 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') +} + /** * Create a public link share + * @param context The current share context * @param shareName The name of the shared folder + * @param options The share options */ -function createShare(shareName: string) { - cy.login(user) - // open the files app +export function createShare(context: ShareContext, shareName: string, options: ShareOptions | null = null) { + cy.login(context.user) 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() + 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() + } - // extract the link return cy.wait('@createShare') .should(({ response }) => { - const { ocs } = response!.body - url = ocs?.data.url - expect(url).to.match(/^http:\/\//) + expect(response?.statusCode).to.eq(200) + const url = response?.body?.ocs?.data?.url + expect(url).to.match(/^https?:\/\//) + context.url = url }) - .then(() => cy.wrap(url)) + .then(() => cy.wrap(context.url)) } /** * Adjust share permissions to be editable */ -function adjustSharePermission() { - // Update the share to be a file drop +function adjustSharePermission(): void { 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.findByRole('menuitem', { name: /Customize link/i }).click() + + cy.get('[data-cy-files-sharing-share-permissions-bundle]').should('be.visible') + cy.get('[data-cy-files-sharing-share-permissions-bundle="upload-edit"]').click() - // Enable upload-edit - cy.get('[data-cy-files-sharing-share-permissions-bundle]') - .should('be.visible') - cy.get('[data-cy-files-sharing-share-permissions-bundle="upload-edit"]') - .click() - // save changes cy.intercept('PUT', '**/ocs/v2.php/apps/files_sharing/api/v1/shares/*').as('updateShare') - cy.findByRole('button', { name: 'Update share' }) - .click() - cy.wait('@updateShare') + 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(): Cypress.Chainable { - const shareName = 'shared' +export function setupPublicShare(shareName = 'shared'): Cypress.Chainable { return cy.task('getVariable', { key: 'public-share-data' }) .then((data) => { @@ -95,20 +145,28 @@ export function setupPublicShare(): Cypress.Chainable { const { dataSnapshot, shareUrl } = data as any || {} if (dataSnapshot) { cy.restoreState(dataSnapshot) - url = shareUrl + defaultShareContext.url = shareUrl return cy.wrap(shareUrl as string) } else { const shareData: Record = {} return cy.createRandomUser() - .then(($user) => { user = $user }) - .then(() => setupData(shareName)) - .then(() => createShare(shareName)) - .then((value) => { shareData.shareUrl = value }) + .then((user) => { + defaultShareContext.user = user + }) + .then(() => setupData(defaultShareContext.user, shareName)) + .then(() => createShare(defaultShareContext, shareName)) + .then((url) => { + shareData.shareUrl = url + }) .then(() => adjustSharePermission()) - .then(() => cy.saveState().then((value) => { shareData.dataSnapshot = value })) + .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(url)) + .then(() => cy.wrap(defaultShareContext.url)) } }) }