Merge pull request #43768 from nextcloud/artonge/tests/add_tests_for_versions_actions
commit
455a209b9c
@ -1,290 +0,0 @@ |
||||
<!-- |
||||
- @copyright Copyright (c) 2022 Louis Chmn <louis@chmn.me> |
||||
- |
||||
- @author Louis Chmn <louis@chmn.me> |
||||
- |
||||
- @license GNU AGPL version 3 or any later version |
||||
- |
||||
- 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/>. |
||||
- |
||||
--> |
||||
|
||||
<template> |
||||
<li> |
||||
<ul> |
||||
<!-- file --> |
||||
<NcActionCheckbox v-if="!isFolder" |
||||
:checked="shareHasPermissions(atomicPermissions.UPDATE)" |
||||
@update:checked="toggleSharePermissions(atomicPermissions.UPDATE)"> |
||||
{{ t('files_sharing', 'Allow editing') }} |
||||
</NcActionCheckbox> |
||||
|
||||
<!-- folder --> |
||||
<template v-if="isFolder && fileHasCreatePermission && config.isPublicUploadEnabled"> |
||||
<template v-if="!showCustomPermissionsForm"> |
||||
<NcActionRadio :checked="sharePermissionEqual(bundledPermissions.READ_ONLY)" |
||||
:value="bundledPermissions.READ_ONLY" |
||||
:name="randomFormName" |
||||
@change="setSharePermissions(bundledPermissions.READ_ONLY)"> |
||||
{{ t('files_sharing', 'Read only') }} |
||||
</NcActionRadio> |
||||
|
||||
<NcActionRadio :checked="sharePermissionEqual(bundledPermissions.UPLOAD_AND_UPDATE)" |
||||
:value="bundledPermissions.UPLOAD_AND_UPDATE" |
||||
:name="randomFormName" |
||||
@change="setSharePermissions(bundledPermissions.UPLOAD_AND_UPDATE)"> |
||||
{{ t('files_sharing', 'Allow upload and editing') }} |
||||
</NcActionRadio> |
||||
<NcActionRadio :checked="sharePermissionEqual(bundledPermissions.FILE_DROP)" |
||||
:value="bundledPermissions.FILE_DROP" |
||||
:name="randomFormName" |
||||
class="sharing-entry__action--public-upload" |
||||
@change="setSharePermissions(bundledPermissions.FILE_DROP)"> |
||||
{{ t('files_sharing', 'File drop (upload only)') }} |
||||
</NcActionRadio> |
||||
|
||||
<!-- custom permissions button --> |
||||
<NcActionButton :title="t('files_sharing', 'Custom permissions')" |
||||
@click="showCustomPermissionsForm = true"> |
||||
<template #icon> |
||||
<Tune /> |
||||
</template> |
||||
{{ sharePermissionsIsBundle ? "" : sharePermissionsSummary }} |
||||
</NcActionButton> |
||||
</template> |
||||
|
||||
<!-- custom permissions --> |
||||
<span v-else :class="{error: !sharePermissionsSetIsValid}"> |
||||
<NcActionCheckbox :checked="shareHasPermissions(atomicPermissions.READ)" |
||||
:disabled="!canToggleSharePermissions(atomicPermissions.READ)" |
||||
@update:checked="toggleSharePermissions(atomicPermissions.READ)"> |
||||
{{ t('files_sharing', 'Read') }} |
||||
</NcActionCheckbox> |
||||
<NcActionCheckbox :checked="shareHasPermissions(atomicPermissions.CREATE)" |
||||
:disabled="!canToggleSharePermissions(atomicPermissions.CREATE)" |
||||
@update:checked="toggleSharePermissions(atomicPermissions.CREATE)"> |
||||
{{ t('files_sharing', 'Upload') }} |
||||
</NcActionCheckbox> |
||||
<NcActionCheckbox :checked="shareHasPermissions(atomicPermissions.UPDATE)" |
||||
:disabled="!canToggleSharePermissions(atomicPermissions.UPDATE)" |
||||
@update:checked="toggleSharePermissions(atomicPermissions.UPDATE)"> |
||||
{{ t('files_sharing', 'Edit') }} |
||||
</NcActionCheckbox> |
||||
<NcActionCheckbox :checked="shareHasPermissions(atomicPermissions.DELETE)" |
||||
:disabled="!canToggleSharePermissions(atomicPermissions.DELETE)" |
||||
@update:checked="toggleSharePermissions(atomicPermissions.DELETE)"> |
||||
{{ t('files_sharing', 'Delete') }} |
||||
</NcActionCheckbox> |
||||
|
||||
<NcActionButton @click="showCustomPermissionsForm = false"> |
||||
<template #icon> |
||||
<ChevronLeft /> |
||||
</template> |
||||
{{ t('files_sharing', 'Bundled permissions') }} |
||||
</NcActionButton> |
||||
</span> |
||||
</template> |
||||
</ul> |
||||
</li> |
||||
</template> |
||||
|
||||
<script> |
||||
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' |
||||
import NcActionRadio from '@nextcloud/vue/dist/Components/NcActionRadio.js' |
||||
import NcActionCheckbox from '@nextcloud/vue/dist/Components/NcActionCheckbox.js' |
||||
|
||||
import SharesMixin from '../mixins/SharesMixin.js' |
||||
import { |
||||
ATOMIC_PERMISSIONS, |
||||
BUNDLED_PERMISSIONS, |
||||
hasPermissions, |
||||
permissionsSetIsValid, |
||||
togglePermissions, |
||||
canTogglePermissions, |
||||
} from '../lib/SharePermissionsToolBox.js' |
||||
|
||||
import Tune from 'vue-material-design-icons/Tune.vue' |
||||
import ChevronLeft from 'vue-material-design-icons/ChevronLeft.vue' |
||||
|
||||
export default { |
||||
name: 'SharePermissionsEditor', |
||||
|
||||
components: { |
||||
NcActionButton, |
||||
NcActionCheckbox, |
||||
NcActionRadio, |
||||
Tune, |
||||
ChevronLeft, |
||||
}, |
||||
|
||||
mixins: [SharesMixin], |
||||
|
||||
data() { |
||||
return { |
||||
randomFormName: Math.random().toString(27).substring(2), |
||||
|
||||
showCustomPermissionsForm: false, |
||||
|
||||
atomicPermissions: ATOMIC_PERMISSIONS, |
||||
bundledPermissions: BUNDLED_PERMISSIONS, |
||||
} |
||||
}, |
||||
|
||||
computed: { |
||||
/** |
||||
* Return the summary of custom checked permissions. |
||||
* |
||||
* @return {string} |
||||
*/ |
||||
sharePermissionsSummary() { |
||||
return Object.values(this.atomicPermissions) |
||||
.filter(permission => this.shareHasPermissions(permission)) |
||||
.map(permission => { |
||||
switch (permission) { |
||||
case this.atomicPermissions.CREATE: |
||||
return this.t('files_sharing', 'Upload') |
||||
case this.atomicPermissions.READ: |
||||
return this.t('files_sharing', 'Read') |
||||
case this.atomicPermissions.UPDATE: |
||||
return this.t('files_sharing', 'Edit') |
||||
case this.atomicPermissions.DELETE: |
||||
return this.t('files_sharing', 'Delete') |
||||
default: |
||||
return null |
||||
} |
||||
}) |
||||
.filter(permissionLabel => permissionLabel !== null) |
||||
.join(', ') |
||||
}, |
||||
|
||||
/** |
||||
* Return whether the share's permission is a bundle. |
||||
* |
||||
* @return {boolean} |
||||
*/ |
||||
sharePermissionsIsBundle() { |
||||
return Object.values(BUNDLED_PERMISSIONS) |
||||
.map(bundle => this.sharePermissionEqual(bundle)) |
||||
.filter(isBundle => isBundle) |
||||
.length > 0 |
||||
}, |
||||
|
||||
/** |
||||
* Return whether the share's permission is valid. |
||||
* |
||||
* @return {boolean} |
||||
*/ |
||||
sharePermissionsSetIsValid() { |
||||
return permissionsSetIsValid(this.share.permissions) |
||||
}, |
||||
|
||||
/** |
||||
* Is the current share a folder ? |
||||
* TODO: move to a proper FileInfo model? |
||||
* |
||||
* @return {boolean} |
||||
*/ |
||||
isFolder() { |
||||
return this.fileInfo.type === 'dir' |
||||
}, |
||||
|
||||
/** |
||||
* Does the current file/folder have create permissions. |
||||
* TODO: move to a proper FileInfo model? |
||||
* |
||||
* @return {boolean} |
||||
*/ |
||||
fileHasCreatePermission() { |
||||
return !!(this.fileInfo.permissions & ATOMIC_PERMISSIONS.CREATE) |
||||
}, |
||||
}, |
||||
|
||||
mounted() { |
||||
// Show the Custom Permissions view on open if the permissions set is not a bundle. |
||||
this.showCustomPermissionsForm = !this.sharePermissionsIsBundle |
||||
}, |
||||
|
||||
methods: { |
||||
/** |
||||
* Return whether the share has the exact given permissions. |
||||
* |
||||
* @param {number} permissions - the permissions to check. |
||||
* |
||||
* @return {boolean} |
||||
*/ |
||||
sharePermissionEqual(permissions) { |
||||
// We use the share's permission without PERMISSION_SHARE as it is not relevant here. |
||||
return (this.share.permissions & ~ATOMIC_PERMISSIONS.SHARE) === permissions |
||||
}, |
||||
|
||||
/** |
||||
* Return whether the share has the given permissions. |
||||
* |
||||
* @param {number} permissions - the permissions to check. |
||||
* |
||||
* @return {boolean} |
||||
*/ |
||||
shareHasPermissions(permissions) { |
||||
return hasPermissions(this.share.permissions, permissions) |
||||
}, |
||||
|
||||
/** |
||||
* Set the share permissions to the given permissions. |
||||
* |
||||
* @param {number} permissions - the permissions to set. |
||||
* |
||||
* @return {void} |
||||
*/ |
||||
setSharePermissions(permissions) { |
||||
this.share.permissions = permissions |
||||
this.queueUpdate('permissions') |
||||
}, |
||||
|
||||
/** |
||||
* Return whether some given permissions can be toggled. |
||||
* |
||||
* @param {number} permissionsToToggle - the permissions to toggle. |
||||
* |
||||
* @return {boolean} |
||||
*/ |
||||
canToggleSharePermissions(permissionsToToggle) { |
||||
return canTogglePermissions(this.share.permissions, permissionsToToggle) |
||||
}, |
||||
|
||||
/** |
||||
* Toggle a given permission. |
||||
* |
||||
* @param {number} permissions - the permissions to toggle. |
||||
* |
||||
* @return {void} |
||||
*/ |
||||
toggleSharePermissions(permissions) { |
||||
this.share.permissions = togglePermissions(this.share.permissions, permissions) |
||||
|
||||
if (!permissionsSetIsValid(this.share.permissions)) { |
||||
return |
||||
} |
||||
|
||||
this.queueUpdate('permissions') |
||||
}, |
||||
}, |
||||
} |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
.error { |
||||
::v-deep .action-checkbox__label:before { |
||||
border: 1px solid var(--color-error); |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,104 @@ |
||||
/* 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) { |
||||
cy.get('@downloadCheckbox').check({ force: true, scrollBehavior: 'nearest' }) |
||||
} else { |
||||
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) { |
||||
cy.get('@readCheckbox').check({ force: true, scrollBehavior: 'nearest' }) |
||||
} else { |
||||
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) { |
||||
cy.get('@updateCheckbox').check({ force: true, scrollBehavior: 'nearest' }) |
||||
} else { |
||||
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) { |
||||
cy.get('@deleteCheckbox').check({ force: true, scrollBehavior: 'nearest' }) |
||||
} else { |
||||
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() |
||||
} |
@ -0,0 +1,110 @@ |
||||
/** |
||||
* @copyright Copyright (c) 2024 Louis Chmn <louis@chmn.me> |
||||
* |
||||
* @author Louis Chmn <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 type { User } from '@nextcloud/cypress' |
||||
import { doesNotHaveAction, openVersionsPanel, setupTestSharedFileFromUser, uploadThreeVersions, deleteVersion } from './filesVersionsUtils' |
||||
import { navigateToFolder, getRowForFile } from '../files/FilesUtils' |
||||
|
||||
describe('Versions restoration', () => { |
||||
const folderName = 'shared_folder' |
||||
const randomFileName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + '.txt' |
||||
const randomFilePath = `/${folderName}/${randomFileName}` |
||||
let user: User |
||||
let versionCount = 0 |
||||
|
||||
before(() => { |
||||
cy.createRandomUser() |
||||
.then((_user) => { |
||||
user = _user |
||||
cy.mkdir(user, `/${folderName}`) |
||||
uploadThreeVersions(user, randomFilePath) |
||||
uploadThreeVersions(user, randomFilePath) |
||||
versionCount = 6 |
||||
cy.login(user) |
||||
cy.visit('/apps/files') |
||||
navigateToFolder(folderName) |
||||
openVersionsPanel(randomFilePath) |
||||
}) |
||||
}) |
||||
|
||||
it('Delete initial version', () => { |
||||
cy.get('[data-files-versions-version]').should('have.length', versionCount) |
||||
deleteVersion(2) |
||||
versionCount-- |
||||
cy.get('[data-files-versions-version]').should('have.length', versionCount) |
||||
}) |
||||
|
||||
context('Delete versions of shared file', () => { |
||||
it('Works with delete permission', () => { |
||||
setupTestSharedFileFromUser(user, folderName, { delete: true }) |
||||
navigateToFolder(folderName) |
||||
openVersionsPanel(randomFilePath) |
||||
|
||||
cy.get('[data-files-versions-version]').should('have.length', versionCount) |
||||
deleteVersion(2) |
||||
versionCount-- |
||||
cy.get('[data-files-versions-version]').should('have.length', versionCount) |
||||
}) |
||||
|
||||
it('Does not work without delete permission', () => { |
||||
setupTestSharedFileFromUser(user, folderName, { delete: false }) |
||||
navigateToFolder(folderName) |
||||
openVersionsPanel(randomFilePath) |
||||
|
||||
doesNotHaveAction(0, 'delete') |
||||
doesNotHaveAction(1, 'delete') |
||||
doesNotHaveAction(2, 'delete') |
||||
}) |
||||
|
||||
it('Does not work without delete permission through direct API access', () => { |
||||
let hostname: string |
||||
let fileId: string|undefined |
||||
let versionId: string|undefined |
||||
|
||||
setupTestSharedFileFromUser(user, folderName, { delete: false }) |
||||
.then(recipient => { |
||||
navigateToFolder(folderName) |
||||
openVersionsPanel(randomFilePath) |
||||
|
||||
cy.url().then(url => { hostname = new URL(url).hostname }) |
||||
getRowForFile(randomFileName).invoke('attr', 'data-cy-files-list-row-fileid').then(_fileId => { fileId = _fileId }) |
||||
cy.get('[data-files-versions-version]').eq(1).invoke('attr', 'data-files-versions-version').then(_versionId => { versionId = _versionId }) |
||||
|
||||
cy.then(() => { |
||||
cy.logout() |
||||
cy.request({ |
||||
method: 'DELETE', |
||||
auth: { user: recipient.userId, pass: recipient.password }, |
||||
headers: { |
||||
cookie: '', |
||||
}, |
||||
url: `http://${hostname}/remote.php/dav/versions/${recipient.userId}/versions/${fileId}/${versionId}`, |
||||
failOnStatusCode: false, |
||||
}) |
||||
.then(({ status }) => { |
||||
expect(status).to.equal(403) |
||||
}) |
||||
}) |
||||
}) |
||||
}) |
||||
}) |
||||
}) |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in new issue