Skip to content

Commit b9af883

Browse files
authored
Merge pull request #50626 from nextcloud/backport/50260/stable30
[stable30] chore : Comprehensive e2e testing for public sharing
2 parents 82470ce + feea4c3 commit b9af883

11 files changed

+387
-8
lines changed

apps/files_sharing/src/components/SharingEntryLink.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
:disabled="pendingEnforcedExpirationDate || saving"
8787
class="share-link-expiration-date-checkbox"
8888
@change="onDefaultExpirationDateEnabledChange">
89-
{{ config.enforcePasswordForPublicLink ? t('files_sharing', 'Enable link expiration (enforced)') : t('files_sharing', 'Enable link expiration') }}
89+
{{ config.isDefaultExpireDateEnforced ? t('files_sharing', 'Enable link expiration (enforced)') : t('files_sharing', 'Enable link expiration') }}
9090
</NcActionCheckbox>
9191

9292
<!-- expiration date -->
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
export type ShareOptions = {
7+
enforcePassword?: boolean
8+
enforceExpirationDate?: boolean
9+
alwaysAskForPassword?: boolean
10+
defaultExpirationDateSet?: boolean
11+
}
12+
13+
export const defaultShareOptions: ShareOptions = {
14+
enforcePassword: false,
15+
enforceExpirationDate: false,
16+
alwaysAskForPassword: false,
17+
defaultExpirationDateSet: false,
18+
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { ShareContext } from './setup-public-share.ts'
7+
import type { ShareOptions } from '../ShareOptionsType.ts'
8+
import { defaultShareOptions } from '../ShareOptionsType.ts'
9+
import { setupData, createShare } from './setup-public-share.ts'
10+
11+
describe('files_sharing: Before create checks', () => {
12+
13+
let shareContext: ShareContext
14+
15+
before(() => {
16+
// Setup data for the shared folder once before all tests
17+
cy.createRandomUser().then((randomUser) => {
18+
shareContext = {
19+
user: randomUser,
20+
}
21+
})
22+
})
23+
24+
afterEach(() => {
25+
cy.runOccCommand('config:app:delete core shareapi_enable_link_password_by_default')
26+
cy.runOccCommand('config:app:delete core shareapi_enforce_links_password')
27+
cy.runOccCommand('config:app:delete core shareapi_default_expire_date')
28+
cy.runOccCommand('config:app:delete core shareapi_enforce_expire_date')
29+
cy.runOccCommand('config:app:delete core shareapi_expire_after_n_days')
30+
})
31+
32+
const applyShareOptions = (options: ShareOptions = defaultShareOptions): void => {
33+
cy.runOccCommand(`config:app:set --value ${options.alwaysAskForPassword ? 'yes' : 'no'} core shareapi_enable_link_password_by_default`)
34+
cy.runOccCommand(`config:app:set --value ${options.enforcePassword ? 'yes' : 'no'} core shareapi_enforce_links_password`)
35+
cy.runOccCommand(`config:app:set --value ${options.enforceExpirationDate ? 'yes' : 'no'} core shareapi_enforce_expire_date`)
36+
cy.runOccCommand(`config:app:set --value ${options.defaultExpirationDateSet ? 'yes' : 'no'} core shareapi_default_expire_date`)
37+
if (options.defaultExpirationDateSet) {
38+
cy.runOccCommand('config:app:set --value 2 core shareapi_expire_after_n_days')
39+
}
40+
}
41+
42+
it('Checks if user can create share when both password and expiration date are enforced', () => {
43+
const shareOptions : ShareOptions = {
44+
alwaysAskForPassword: true,
45+
enforcePassword: true,
46+
enforceExpirationDate: true,
47+
defaultExpirationDateSet: true,
48+
}
49+
applyShareOptions(shareOptions)
50+
const shareName = 'passwordAndExpireEnforced'
51+
setupData(shareContext.user, shareName)
52+
createShare(shareContext, shareName, shareOptions).then((shareUrl) => {
53+
shareContext.url = shareUrl
54+
cy.log(`Created share with URL: ${shareUrl}`)
55+
})
56+
})
57+
58+
it('Checks if user can create share when password is enforced and expiration date has a default set', () => {
59+
const shareOptions : ShareOptions = {
60+
alwaysAskForPassword: true,
61+
enforcePassword: true,
62+
defaultExpirationDateSet: true,
63+
}
64+
applyShareOptions(shareOptions)
65+
const shareName = 'passwordEnforcedDefaultExpire'
66+
setupData(shareContext.user, shareName)
67+
createShare(shareContext, shareName, shareOptions).then((shareUrl) => {
68+
shareContext.url = shareUrl
69+
cy.log(`Created share with URL: ${shareUrl}`)
70+
})
71+
})
72+
73+
it('Checks if user can create share when password is optionally requested and expiration date is enforced', () => {
74+
const shareOptions : ShareOptions = {
75+
alwaysAskForPassword: true,
76+
defaultExpirationDateSet: true,
77+
enforceExpirationDate: true,
78+
}
79+
applyShareOptions(shareOptions)
80+
const shareName = 'defaultPasswordExpireEnforced'
81+
setupData(shareContext.user, shareName)
82+
createShare(shareContext, shareName, shareOptions).then((shareUrl) => {
83+
shareContext.url = shareUrl
84+
cy.log(`Created share with URL: ${shareUrl}`)
85+
})
86+
})
87+
88+
it('Checks if user can create share when password is optionally requested and expiration date have defaults set', () => {
89+
const shareOptions : ShareOptions = {
90+
alwaysAskForPassword: true,
91+
defaultExpirationDateSet: true,
92+
}
93+
applyShareOptions(shareOptions)
94+
const shareName = 'defaultPasswordAndExpire'
95+
setupData(shareContext.user, shareName)
96+
createShare(shareContext, shareName, shareOptions).then((shareUrl) => {
97+
shareContext.url = shareUrl
98+
cy.log(`Created share with URL: ${shareUrl}`)
99+
})
100+
})
101+
102+
it('Checks if user can create share with password enforced and expiration date set but not enforced', () => {
103+
const shareOptions : ShareOptions = {
104+
alwaysAskForPassword: true,
105+
enforcePassword: true,
106+
defaultExpirationDateSet: true,
107+
enforceExpirationDate: false,
108+
}
109+
applyShareOptions(shareOptions)
110+
const shareName = 'passwordEnforcedExpireSetNotEnforced'
111+
setupData(shareContext.user, shareName)
112+
createShare(shareContext, shareName, shareOptions).then((shareUrl) => {
113+
shareContext.url = shareUrl
114+
cy.log(`Created share with URL: ${shareUrl}`)
115+
})
116+
})
117+
118+
it('Checks if user can create a share when both password and expiration date have default values but are both not enforced', () => {
119+
const shareOptions : ShareOptions = {
120+
alwaysAskForPassword: true,
121+
enforcePassword: false,
122+
defaultExpirationDateSet: true,
123+
enforceExpirationDate: false,
124+
}
125+
applyShareOptions(shareOptions)
126+
const shareName = 'defaultPasswordAndExpirationNotEnforced'
127+
setupData(shareContext.user, shareName)
128+
createShare(shareContext, shareName, shareOptions).then((shareUrl) => {
129+
shareContext.url = shareUrl
130+
cy.log(`Created share with URL: ${shareUrl}`)
131+
})
132+
})
133+
134+
it('Checks if user can create share with password not enforced but expiration date enforced', () => {
135+
const shareOptions : ShareOptions = {
136+
alwaysAskForPassword: true,
137+
enforcePassword: false,
138+
defaultExpirationDateSet: true,
139+
enforceExpirationDate: true,
140+
}
141+
applyShareOptions(shareOptions)
142+
const shareName = 'noPasswordExpireEnforced'
143+
setupData(shareContext.user, shareName)
144+
createShare(shareContext, shareName, shareOptions).then((shareUrl) => {
145+
shareContext.url = shareUrl
146+
cy.log(`Created share with URL: ${shareUrl}`)
147+
})
148+
})
149+
150+
it('Checks if user can create share with password not enforced and expiration date has a default set', () => {
151+
const shareOptions : ShareOptions = {
152+
alwaysAskForPassword: true,
153+
enforcePassword: false,
154+
defaultExpirationDateSet: true,
155+
enforceExpirationDate: false,
156+
}
157+
applyShareOptions(shareOptions)
158+
const shareName = 'defaultExpireNoPasswordEnforced'
159+
setupData(shareContext.user, shareName)
160+
createShare(shareContext, shareName, shareOptions).then((shareUrl) => {
161+
shareContext.url = shareUrl
162+
cy.log(`Created share with URL: ${shareUrl}`)
163+
})
164+
})
165+
166+
it('Checks if user can create share with expiration date set and password not enforced', () => {
167+
const shareOptions : ShareOptions = {
168+
alwaysAskForPassword: true,
169+
enforcePassword: false,
170+
defaultExpirationDateSet: true,
171+
}
172+
applyShareOptions(shareOptions)
173+
174+
const shareName = 'noPasswordExpireDefault'
175+
setupData(shareContext.user, shareName)
176+
createShare(shareContext, shareName, shareOptions).then((shareUrl) => {
177+
shareContext.url = shareUrl
178+
cy.log(`Created share with URL: ${shareUrl}`)
179+
})
180+
})
181+
182+
it('Checks if user can create share with password not enforced, expiration date not enforced, and no defaults set', () => {
183+
applyShareOptions()
184+
const shareName = 'noPasswordNoExpireNoDefaults'
185+
setupData(shareContext.user, shareName)
186+
createShare(shareContext, shareName, null).then((shareUrl) => {
187+
shareContext.url = shareUrl
188+
cy.log(`Created share with URL: ${shareUrl}`)
189+
})
190+
})
191+
192+
})
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
import type { User } from '@nextcloud/cypress'
6+
import type { ShareOptions } from '../ShareOptionsType.ts'
7+
import { openSharingPanel } from '../FilesSharingUtils.ts'
8+
9+
export interface ShareContext {
10+
user: User
11+
url?: string
12+
}
13+
14+
const defaultShareContext: ShareContext = {
15+
user: {} as User,
16+
url: undefined,
17+
}
18+
19+
/**
20+
* Retrieves the URL of the share.
21+
* Throws an error if the share context is not initialized properly.
22+
*
23+
* @param context The current share context (defaults to `defaultShareContext` if not provided).
24+
* @return The share URL.
25+
* @throws Error if the share context has no URL.
26+
*/
27+
export function getShareUrl(context: ShareContext = defaultShareContext): string {
28+
if (!context.url) {
29+
throw new Error('You need to setup the share first!')
30+
}
31+
return context.url
32+
}
33+
34+
/**
35+
* Setup the available data
36+
* @param user The current share context
37+
* @param shareName The name of the shared folder
38+
*/
39+
export function setupData(user: User, shareName: string): void {
40+
cy.mkdir(user, `/${shareName}`)
41+
cy.mkdir(user, `/${shareName}/subfolder`)
42+
cy.uploadContent(user, new Blob(['<content>foo</content>']), 'text/plain', `/${shareName}/foo.txt`)
43+
cy.uploadContent(user, new Blob(['<content>bar</content>']), 'text/plain', `/${shareName}/subfolder/bar.txt`)
44+
}
45+
46+
/**
47+
* Check the password state based on enforcement and default presence.
48+
*
49+
* @param enforced Whether the password is enforced.
50+
* @param alwaysAskForPassword Wether the password should always be asked for.
51+
*/
52+
function checkPasswordState(enforced: boolean, alwaysAskForPassword: boolean) {
53+
if (enforced) {
54+
cy.contains('Password protection (enforced)').should('exist')
55+
} else if (alwaysAskForPassword) {
56+
cy.contains('Password protection').should('exist')
57+
}
58+
cy.contains('Enter a password')
59+
.should('exist')
60+
.and('not.be.disabled')
61+
}
62+
63+
/**
64+
* Check the expiration date state based on enforcement and default presence.
65+
*
66+
* @param enforced Whether the expiration date is enforced.
67+
* @param hasDefault Whether a default expiration date is set.
68+
*/
69+
function checkExpirationDateState(enforced: boolean, hasDefault: boolean) {
70+
if (enforced) {
71+
cy.contains('Enable link expiration (enforced)').should('exist')
72+
} else if (hasDefault) {
73+
cy.contains('Enable link expiration').should('exist')
74+
}
75+
cy.contains('Enter expiration date')
76+
.should('exist')
77+
.and('not.be.disabled')
78+
}
79+
80+
/**
81+
* Create a public link share
82+
* @param context The current share context
83+
* @param shareName The name of the shared folder
84+
* @param options The share options
85+
*/
86+
export function createShare(context: ShareContext, shareName: string, options: ShareOptions | null = null) {
87+
cy.login(context.user)
88+
cy.visit('/apps/files')
89+
openSharingPanel(shareName)
90+
91+
cy.intercept('POST', '**/ocs/v2.php/apps/files_sharing/api/v1/shares').as('createShare')
92+
cy.findByRole('button', { name: 'Create a new share link' }).click()
93+
// Conduct optional checks based on the provided options
94+
if (options) {
95+
cy.get('.sharing-entry__actions').should('be.visible') // Wait for the dialog to open
96+
checkPasswordState(options.enforcePassword ?? false, options.alwaysAskForPassword ?? false)
97+
checkExpirationDateState(options.enforceExpirationDate ?? false, options.defaultExpirationDateSet ?? false)
98+
cy.findByRole('button', { name: 'Create share' }).click()
99+
}
100+
101+
return cy.wait('@createShare')
102+
.should(({ response }) => {
103+
expect(response?.statusCode).to.eq(200)
104+
const url = response?.body?.ocs?.data?.url
105+
expect(url).to.match(/^https?:\/\//)
106+
context.url = url
107+
})
108+
.then(() => cy.wrap(context.url))
109+
}
110+
111+
/**
112+
* Adjust share permissions to be editable
113+
*/
114+
function adjustSharePermission(): void {
115+
cy.findByRole('list', { name: 'Link shares' })
116+
.findAllByRole('listitem')
117+
.first()
118+
.findByRole('button', { name: /Actions/i })
119+
.click()
120+
cy.findByRole('menuitem', { name: /Customize link/i }).click()
121+
122+
cy.get('[data-cy-files-sharing-share-permissions-bundle]').should('be.visible')
123+
cy.get('[data-cy-files-sharing-share-permissions-bundle="upload-edit"]').click()
124+
125+
cy.intercept('PUT', '**/ocs/v2.php/apps/files_sharing/api/v1/shares/*').as('updateShare')
126+
cy.findByRole('button', { name: 'Update share' }).click()
127+
cy.wait('@updateShare').its('response.statusCode').should('eq', 200)
128+
}
129+
130+
/**
131+
* Setup a public share and backup the state.
132+
* If the setup was already done in another run, the state will be restored.
133+
*
134+
* @param shareName The name of the shared folder
135+
* @return The URL of the share
136+
*/
137+
export function setupPublicShare(shareName = 'shared'): Cypress.Chainable<string> {
138+
139+
return cy.task('getVariable', { key: 'public-share-data' })
140+
.then((data) => {
141+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
142+
const { dataSnapshot, shareUrl } = data as any || {}
143+
if (dataSnapshot) {
144+
cy.restoreState(dataSnapshot)
145+
defaultShareContext.url = shareUrl
146+
return cy.wrap(shareUrl as string)
147+
} else {
148+
const shareData: Record<string, unknown> = {}
149+
return cy.createRandomUser()
150+
.then((user) => {
151+
defaultShareContext.user = user
152+
})
153+
.then(() => setupData(defaultShareContext.user, shareName))
154+
.then(() => createShare(defaultShareContext, shareName))
155+
.then((url) => {
156+
shareData.shareUrl = url
157+
})
158+
.then(() => adjustSharePermission())
159+
.then(() =>
160+
cy.saveState().then((snapshot) => {
161+
shareData.dataSnapshot = snapshot
162+
}),
163+
)
164+
.then(() => cy.task('setVariable', { key: 'public-share-data', value: shareData }))
165+
.then(() => cy.log(`Public share setup, URL: ${shareData.shareUrl}`))
166+
.then(() => cy.wrap(defaultShareContext.url))
167+
}
168+
})
169+
}

dist/7170-7170.js.map.license

Lines changed: 0 additions & 1 deletion
This file was deleted.

dist/7170-7170.js renamed to dist/7797-7797.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
File renamed without changes.

dist/7170-7170.js.map renamed to dist/7797-7797.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/7797-7797.js.map.license

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
7797-7797.js.license

0 commit comments

Comments
 (0)