Merge pull request #43768 from nextcloud/artonge/tests/add_tests_for_versions_actions

pull/43875/head
John Molakvoæ 9 months ago committed by GitHub
commit 455a209b9c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      .gitignore
  2. 10
      apps/dav/lib/Server.php
  3. 290
      apps/files_sharing/src/components/SharePermissionsEditor.vue
  4. 1
      apps/files_sharing/src/components/SharingEntry.vue
  5. 35
      apps/files_sharing/src/views/SharingDetailsTab.vue
  6. 13
      apps/files_versions/src/components/Version.vue
  7. 58
      cypress/e2e/files/FilesUtils.ts
  8. 81
      cypress/e2e/files/files_copy-move.cy.ts
  9. 104
      cypress/e2e/files_sharing/filesSharingUtils.ts
  10. 66
      cypress/e2e/files_versions/filesVersionsUtils.ts
  11. 110
      cypress/e2e/files_versions/version_deletion.cy.ts
  12. 62
      cypress/e2e/files_versions/version_download.cy.ts
  13. 96
      cypress/e2e/files_versions/version_naming.cy.ts
  14. 85
      cypress/e2e/files_versions/version_restoration.cy.ts
  15. 3
      dist/1758-1758.js
  16. 1
      dist/1758-1758.js.map
  17. 3
      dist/4889-4889.js
  18. 0
      dist/4889-4889.js.LICENSE.txt
  19. 1
      dist/4889-4889.js.map
  20. 4
      dist/files_sharing-files_sharing_tab.js
  21. 2
      dist/files_sharing-files_sharing_tab.js.map
  22. 4
      dist/files_versions-files_versions.js
  23. 2
      dist/files_versions-files_versions.js.map

1
.gitignore vendored

@ -173,3 +173,4 @@ core/js/mimetypelist.js
# Tests - cypress
cypress/snapshots
cypress/videos
cypress/downloads

@ -241,11 +241,6 @@ class Server {
$this->server->addPlugin(new FakeLockerPlugin());
}
// Allow view-only plugin for webdav requests
$this->server->addPlugin(new ViewOnlyPlugin(
\OC::$server->getUserFolder(),
));
if (BrowserErrorPagePlugin::isBrowserRequest($request)) {
$this->server->addPlugin(new BrowserErrorPagePlugin());
}
@ -255,6 +250,11 @@ class Server {
// wait with registering these until auth is handled and the filesystem is setup
$this->server->on('beforeMethod:*', function () use ($root, $lazySearchBackend, $logger) {
// Allow view-only plugin for webdav requests
$this->server->addPlugin(new ViewOnlyPlugin(
\OC::$server->getUserFolder(),
));
// custom properties plugin must be the last one
$userSession = \OC::$server->getUserSession();
$user = $userSession->getUser();

@ -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>

@ -46,6 +46,7 @@
@open-sharing-details="openShareDetailsForCustomSettings(share)" />
</div>
<NcButton class="sharing-entry__action"
data-cy-files-sharing-share-actions
:aria-label="t('files_sharing', 'Open Sharing Details')"
type="tertiary"
@click="openSharingDetails(share)">

@ -19,6 +19,7 @@
<div ref="quickPermissions" class="sharingTabDetailsView__quick-permissions">
<div>
<NcCheckboxRadioSwitch :button-variant="true"
data-cy-files-sharing-share-permissions-bundle="read-only"
:checked.sync="sharingPermission"
:value="bundledPermissions.READ_ONLY.toString()"
name="sharing_permission_radio"
@ -31,6 +32,7 @@
</template>
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :button-variant="true"
data-cy-files-sharing-share-permissions-bundle="upload-edit"
:checked.sync="sharingPermission"
:value="bundledPermissions.ALL.toString()"
name="sharing_permission_radio"
@ -48,6 +50,7 @@
</template>
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch v-if="allowsFileDrop"
data-cy-files-sharing-share-permissions-bundle="file-drop"
:button-variant="true"
:checked.sync="sharingPermission"
:value="bundledPermissions.FILE_DROP.toString()"
@ -62,6 +65,7 @@
</template>
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :button-variant="true"
data-cy-files-sharing-share-permissions-bundle="custom"
:checked.sync="sharingPermission"
:value="'custom'"
name="sharing_permission_radio"
@ -145,7 +149,10 @@
@update:checked="queueUpdate('hideDownload')">
{{ t('files_sharing', 'Hide download') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch v-if="!isPublicShare" :disabled="!canSetDownload" :checked.sync="canDownload">
<NcCheckboxRadioSwitch v-if="!isPublicShare"
:disabled="!canSetDownload"
:checked.sync="canDownload"
data-cy-files-sharing-share-permissions-checkbox="download">
{{ t('files_sharing', 'Allow download') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :checked.sync="writeNoteToRecipientIsChecked">
@ -162,21 +169,30 @@
</NcCheckboxRadioSwitch>
<section v-if="setCustomPermissions" class="custom-permissions-group">
<NcCheckboxRadioSwitch :disabled="!allowsFileDrop && share.type === SHARE_TYPES.SHARE_TYPE_LINK"
:checked.sync="hasRead">
:checked.sync="hasRead"
data-cy-files-sharing-share-permissions-checkbox="read">
{{ t('files_sharing', 'Read') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch v-if="isFolder" :disabled="!canSetCreate" :checked.sync="canCreate">
<NcCheckboxRadioSwitch v-if="isFolder"
:disabled="!canSetCreate"
:checked.sync="canCreate"
data-cy-files-sharing-share-permissions-checkbox="create">
{{ t('files_sharing', 'Create') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :disabled="!canSetEdit" :checked.sync="canEdit">
<NcCheckboxRadioSwitch :disabled="!canSetEdit"
:checked.sync="canEdit"
data-cy-files-sharing-share-permissions-checkbox="update">
{{ t('files_sharing', 'Edit') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch v-if="config.isResharingAllowed && share.type !== SHARE_TYPES.SHARE_TYPE_LINK"
:disabled="!canSetReshare"
:checked.sync="canReshare">
:checked.sync="canReshare"
data-cy-files-sharing-share-permissions-checkbox="share">
{{ t('files_sharing', 'Share') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :disabled="!canSetDelete" :checked.sync="canDelete">
<NcCheckboxRadioSwitch :disabled="!canSetDelete"
:checked.sync="canDelete"
data-cy-files-sharing-share-permissions-checkbox="delete">
{{ t('files_sharing', 'Delete') }}
</NcCheckboxRadioSwitch>
</section>
@ -199,10 +215,13 @@
<div class="sharingTabDetailsView__footer">
<div class="button-group">
<NcButton @click="$emit('close-sharing-details')">
<NcButton data-cy-files-sharing-share-editor-action="cancel"
@click="$emit('close-sharing-details')">
{{ t('files_sharing', 'Cancel') }}
</NcButton>
<NcButton type="primary" @click="saveShare">
<NcButton type="primary"
data-cy-files-sharing-share-editor-action="save"
@click="saveShare">
{{ shareButtonText }}
<template v-if="creating" #icon>
<NcLoadingIcon />

@ -19,7 +19,7 @@
<NcListItem class="version"
:name="versionLabel"
:force-display-actions="true"
data-files-versions-version
:data-files-versions-version="version.fileVersion"
@click="click">
<!-- Icon -->
<template #icon>
@ -52,6 +52,7 @@
<!-- Actions -->
<template #actions>
<NcActionButton v-if="enableLabeling && hasUpdatePermissions"
data-cy-files-versions-version-action="label"
:close-after-click="true"
@click="labelUpdate">
<template #icon>
@ -60,6 +61,7 @@
{{ version.label === '' ? t('files_versions', 'Name this version') : t('files_versions', 'Edit version name') }}
</NcActionButton>
<NcActionButton v-if="!isCurrent && canView && canCompare"
data-cy-files-versions-version-action="compare"
:close-after-click="true"
@click="compareVersion">
<template #icon>
@ -68,6 +70,7 @@
{{ t('files_versions', 'Compare to current version') }}
</NcActionButton>
<NcActionButton v-if="!isCurrent && hasUpdatePermissions"
data-cy-files-versions-version-action="restore"
:close-after-click="true"
@click="restoreVersion">
<template #icon>
@ -76,6 +79,7 @@
{{ t('files_versions', 'Restore version') }}
</NcActionButton>
<NcActionLink v-if="isDownloadable"
data-cy-files-versions-version-action="download"
:href="downloadURL"
:close-after-click="true"
:download="downloadURL">
@ -85,6 +89,7 @@
{{ t('files_versions', 'Download version') }}
</NcActionLink>
<NcActionButton v-if="!isCurrent && enableDeletion && hasDeletePermissions"
data-cy-files-versions-version-action="delete"
:close-after-click="true"
@click="deleteVersion">
<template #icon>
@ -266,7 +271,11 @@ export default defineComponent({
this.$emit('restore', this.version)
},
deleteVersion() {
async deleteVersion() {
// Let @nc-vue properly remove the popover before we delete the version.
// This prevents @nc-vue from throwing a error.
await this.$nextTick()
await this.$nextTick()
this.$emit('delete', this.version)
},

@ -30,3 +30,61 @@ export const triggerActionForFile = (filename: string, actionId: string) => {
getActionButtonForFile(filename).click()
cy.get(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"] > button`).should('exist').click()
}
export const moveFile = (fileName: string, dirName: string) => {
getRowForFile(fileName).should('be.visible')
triggerActionForFile(fileName, 'move-copy')
cy.get('.file-picker').within(() => {
// intercept the copy so we can wait for it
cy.intercept('MOVE', /\/remote.php\/dav\/files\//).as('moveFile')
if (dirName === '/') {
// select home folder
cy.get('button[title="Home"]').should('be.visible').click()
// click move
cy.contains('button', 'Move').should('be.visible').click()
} else if (dirName === '.') {
// click move
cy.contains('button', 'Copy').should('be.visible').click()
} else {
// select the folder
cy.get(`[data-filename="${dirName}"]`).should('be.visible').click()
// click move
cy.contains('button', `Move to ${dirName}`).should('be.visible').click()
}
cy.wait('@moveFile')
})
}
export const copyFile = (fileName: string, dirName: string) => {
getRowForFile(fileName).should('be.visible')
triggerActionForFile(fileName, 'move-copy')
cy.get('.file-picker').within(() => {
// intercept the copy so we can wait for it
cy.intercept('COPY', /\/remote.php\/dav\/files\//).as('copyFile')
if (dirName === '/') {
// select home folder
cy.get('button[title="Home"]').should('be.visible').click()
// click copy
cy.contains('button', 'Copy').should('be.visible').click()
} else if (dirName === '.') {
// click copy
cy.contains('button', 'Copy').should('be.visible').click()
} else {
// select folder
cy.get(`[data-filename="${dirName}"]`).should('be.visible').click()
// click copy
cy.contains('button', `Copy to ${dirName}`).should('be.visible').click()
}
cy.wait('@copyFile')
})
}
export const navigateToFolder = (folderName: string) => {
getRowForFile(folderName).should('be.visible').find('[data-cy-files-list-row-name-link]').click()
}

@ -20,7 +20,7 @@
*
*/
import { getRowForFile, triggerActionForFile } from './FilesUtils.ts'
import { getRowForFile, moveFile, copyFile, navigateToFolder } from './FilesUtils.ts'
describe('Files: Move or copy files', { testIsolation: true }, () => {
let currentUser
@ -42,22 +42,9 @@ describe('Files: Move or copy files', { testIsolation: true }, () => {
cy.login(currentUser)
cy.visit('/apps/files')
// intercept the copy so we can wait for it
cy.intercept('COPY', /\/remote.php\/dav\/files\//).as('copyFile')
copyFile('original.txt', 'new-folder')
// Open actions and trigger copy-move action
getRowForFile('original.txt').should('be.visible')
triggerActionForFile('original.txt', 'move-copy')
// select new folder
cy.get('.file-picker [data-filename="new-folder"]').should('be.visible').click()
// click copy
cy.get('.file-picker').contains('button', 'Copy to new-folder').should('be.visible').click()
// wait for copy to finish
cy.wait('@copyFile')
getRowForFile('new-folder').find('[data-cy-files-list-row-name-link]').click()
navigateToFolder('new-folder')
cy.url().should('contain', 'dir=/new-folder')
getRowForFile('original.txt').should('be.visible')
@ -70,24 +57,14 @@ describe('Files: Move or copy files', { testIsolation: true }, () => {
cy.login(currentUser)
cy.visit('/apps/files')
// intercept the copy so we can wait for it
cy.intercept('MOVE', /\/remote.php\/dav\/files\//).as('moveFile')
getRowForFile('original.txt').should('be.visible')
triggerActionForFile('original.txt', 'move-copy')
// select new folder
cy.get('.file-picker [data-filename="new-folder"]').should('be.visible').click()
// click copy
cy.get('.file-picker').contains('button', 'Move to new-folder').should('be.visible').click()
moveFile('original.txt', 'new-folder')
cy.wait('@moveFile')
// wait until visible again
getRowForFile('new-folder').should('be.visible')
// original should be moved -> not exist anymore
getRowForFile('original.txt').should('not.exist')
getRowForFile('new-folder').should('be.visible').find('[data-cy-files-list-row-name-link]').click()
navigateToFolder('new-folder')
cy.url().should('contain', 'dir=/new-folder')
getRowForFile('original.txt').should('be.visible')
@ -101,24 +78,14 @@ describe('Files: Move or copy files', { testIsolation: true }, () => {
cy.login(currentUser)
cy.visit('/apps/files')
// intercept the copy so we can wait for it
cy.intercept('MOVE', /\/remote.php\/dav\/files\//).as('moveFile')
getRowForFile('original').should('be.visible')
triggerActionForFile('original', 'move-copy')
// select new folder
cy.get('.file-picker [data-filename="original folder"]').should('be.visible').click()
// click copy
cy.get('.file-picker').contains('button', 'Move to original folder').should('be.visible').click()
moveFile('original', 'original folder')
cy.wait('@moveFile')
// wait until visible again
getRowForFile('original folder').should('be.visible')
// original should be moved -> not exist anymore
getRowForFile('original').should('not.exist')
getRowForFile('original folder').should('be.visible').find('[data-cy-files-list-row-name-link]').click()
navigateToFolder('original folder')
cy.url().should('contain', 'dir=/original%20folder')
getRowForFile('original').should('be.visible')
@ -131,21 +98,11 @@ describe('Files: Move or copy files', { testIsolation: true }, () => {
cy.login(currentUser)
cy.visit('/apps/files')
// intercept the copy so we can wait for it
cy.intercept('MOVE', /\/remote.php\/dav\/files\//).as('moveFile')
getRowForFile('new-folder').should('be.visible').find('[data-cy-files-list-row-name-link]').click()
navigateToFolder('new-folder')
cy.url().should('contain', 'dir=/new-folder')
getRowForFile('original.txt').should('be.visible')
triggerActionForFile('original.txt', 'move-copy')
// select new folder
cy.get('.file-picker button[title="Home"]').should('be.visible').click()
// click move
cy.get('.file-picker').contains('button', 'Move').should('be.visible').click()
moveFile('original.txt', '/')
cy.wait('@moveFile')
// wait until visible again
cy.get('main').contains('No files in here').should('be.visible')
@ -162,16 +119,8 @@ describe('Files: Move or copy files', { testIsolation: true }, () => {
cy.login(currentUser)
cy.visit('/apps/files')
// intercept the copy so we can wait for it
cy.intercept('COPY', /\/remote.php\/dav\/files\//).as('copyFile')
copyFile('original.txt', '.')
getRowForFile('original.txt').should('be.visible')
triggerActionForFile('original.txt', 'move-copy')
// click copy
cy.get('.file-picker').contains('button', 'Copy').should('be.visible').click()
cy.wait('@copyFile')
getRowForFile('original.txt').should('be.visible')
getRowForFile('original (copy).txt').should('be.visible')
})
@ -182,16 +131,8 @@ describe('Files: Move or copy files', { testIsolation: true }, () => {
cy.login(currentUser)
cy.visit('/apps/files')
// intercept the copy so we can wait for it
cy.intercept('COPY', /\/remote.php\/dav\/files\//).as('copyFile')
getRowForFile('original.txt').should('be.visible')
triggerActionForFile('original.txt', 'move-copy')
// click copy
cy.get('.file-picker').contains('button', 'Copy').should('be.visible').click()
copyFile('original.txt', '.')
cy.wait('@copyFile')
getRowForFile('original.txt').should('be.visible')
getRowForFile('original (copy 2).txt').should('be.visible')
})

@ -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()
}

@ -1,3 +1,4 @@
/* eslint-disable jsdoc/require-jsdoc */
/**
* @copyright Copyright (c) 2022 Louis Chemineau <louis@chmn.me>
*
@ -22,6 +23,7 @@
import type { User } from '@nextcloud/cypress'
import path from 'path'
import { createShare, type ShareSetting } from '../files_sharing/filesSharingUtils'
export const uploadThreeVersions = (user: User, fileName: string) => {
// A new version will not be created if the changes occur
@ -35,7 +37,7 @@ export const uploadThreeVersions = (user: User, fileName: string) => {
cy.login(user)
}
export const openVersionsPanel = (fileName: string) =>{
export function openVersionsPanel(fileName: string) {
// Detect the versions list fetch
cy.intercept('PROPFIND', '**/dav/versions/*/versions/**').as('getVersions')
@ -50,35 +52,61 @@ export const openVersionsPanel = (fileName: string) =>{
cy.get('#tab-version_vue').should('be.visible', { timeout: 10000 })
}
export const openVersionMenu = (index: number) => {
cy.get('#tab-version_vue').within(() => {
cy.get('[data-files-versions-version]')
.eq(index).within(() => {
cy.get('.action-item__menutoggle').filter(':visible')
.click()
})
})
export function toggleVersionMenu(index: number) {
cy.get('#tab-version_vue [data-files-versions-version]')
.eq(index)
.find('button')
.click()
}
export const clickPopperAction = (actionName: string) => {
cy.get('.v-popper__popper').filter(':visible')
.contains(actionName)
.click()
export function triggerVersionAction(index: number, actionName: string) {
toggleVersionMenu(index)
cy.get(`[data-cy-files-versions-version-action="${actionName}"]`).filter(':visible').click()
}
export const nameVersion = (index: number, name: string) => {
openVersionMenu(index)
clickPopperAction('Name this version')
export function nameVersion(index: number, name: string) {
cy.intercept('PROPPATCH', '**/dav/versions/*/versions/**').as('labelVersion')
triggerVersionAction(index, 'label')
cy.get(':focused').type(`${name}{enter}`)
cy.wait('@labelVersion')
}
export const assertVersionContent = (filename: string, index: number, expectedContent: string) => {
export function restoreVersion(index: number) {
cy.intercept('MOVE', '**/dav/versions/*/versions/**').as('restoreVersion')
triggerVersionAction(index, 'restore')
cy.wait('@restoreVersion')
}
export function deleteVersion(index: number) {
cy.intercept('DELETE', '**/dav/versions/*/versions/**').as('deleteVersion')
triggerVersionAction(index, 'delete')
cy.wait('@deleteVersion')
}
export function doesNotHaveAction(index: number, actionName: string) {
toggleVersionMenu(index)
cy.get(`[data-cy-files-versions-version-action="${actionName}"]`).should('not.exist')
toggleVersionMenu(index)
}
export function assertVersionContent(filename: string, index: number, expectedContent: string) {
const downloadsFolder = Cypress.config('downloadsFolder')
openVersionMenu(index)
clickPopperAction('Download version')
triggerVersionAction(index, 'download')
return cy.readFile(path.join(downloadsFolder, filename))
.then((versionContent) => expect(versionContent).to.equal(expectedContent))
.then(() => cy.exec(`rm ${downloadsFolder}/${filename}`))
}
export function setupTestSharedFileFromUser(owner: User, randomFileName: string, shareOptions: Partial<ShareSetting>) {
return cy.createRandomUser()
.then((recipient) => {
cy.login(owner)
cy.visit('/apps/files')
createShare(randomFileName, recipient.userId, shareOptions)
cy.login(recipient)
cy.visit('/apps/files')
return cy.wrap(recipient)
})
}

@ -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)
})
})
})
})
})
})

@ -20,16 +20,20 @@
*
*/
import { assertVersionContent, openVersionsPanel, uploadThreeVersions } from './filesVersionsUtils'
import { assertVersionContent, doesNotHaveAction, openVersionsPanel, setupTestSharedFileFromUser, uploadThreeVersions } from './filesVersionsUtils'
import type { User } from '@nextcloud/cypress'
import { getRowForFile } from '../files/FilesUtils'
describe('Versions download', () => {
let randomFileName = ''
let user: User
before(() => {
randomFileName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + '.txt'
cy.createRandomUser()
.then((user) => {
.then((_user) => {
user = _user
uploadThreeVersions(user, randomFileName)
cy.login(user)
cy.visit('/apps/files')
@ -37,9 +41,61 @@ describe('Versions download', () => {
})
})
it('Download versions and assert there content', () => {
it('Download versions and assert their content', () => {
assertVersionContent(randomFileName, 0, 'v3')
assertVersionContent(randomFileName, 1, 'v2')
assertVersionContent(randomFileName, 2, 'v1')
})
context('Download versions of shared file', () => {
it('Works with download permission', () => {
setupTestSharedFileFromUser(user, randomFileName, { download: true })
openVersionsPanel(randomFileName)
assertVersionContent(randomFileName, 0, 'v3')
assertVersionContent(randomFileName, 1, 'v2')
assertVersionContent(randomFileName, 2, 'v1')
})
it('Does not show action without download permission', () => {
setupTestSharedFileFromUser(user, randomFileName, { download: false })
openVersionsPanel(randomFileName)
cy.get('[data-files-versions-version]').eq(0).find('.action-item__menutoggle').should('not.exist')
cy.get('[data-files-versions-version]').eq(0).get('[data-cy-version-action="download"]').should('not.exist')
doesNotHaveAction(1, 'download')
doesNotHaveAction(2, 'download')
})
it('Does not work without download permission through direct API access', () => {
let hostname: string
let fileId: string|undefined
let versionId: string|undefined
setupTestSharedFileFromUser(user, randomFileName, { download: false })
.then(recipient => {
openVersionsPanel(randomFileName)
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({
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)
})
})
})
})
})
})

@ -20,16 +20,20 @@
*
*/
import { nameVersion, openVersionsPanel, uploadThreeVersions } from './filesVersionsUtils'
import type { User } from '@nextcloud/cypress'
import { nameVersion, openVersionsPanel, uploadThreeVersions, doesNotHaveAction, setupTestSharedFileFromUser } from './filesVersionsUtils'
import { getRowForFile } from '../files/FilesUtils'
describe('Versions naming', () => {
let randomFileName = ''
let user: User
before(() => {
randomFileName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + '.txt'
cy.createRandomUser()
.then((user) => {
.then((_user) => {
user = _user
uploadThreeVersions(user, randomFileName)
cy.login(user)
cy.visit('/apps/files')
@ -37,25 +41,103 @@ describe('Versions naming', () => {
})
})
it('Names the initial version as v1', () => {
it('Names the versions', () => {
nameVersion(2, 'v1')
cy.get('#tab-version_vue').within(() => {
cy.get('[data-files-versions-version]').eq(2).contains('v1')
cy.get('[data-files-versions-version]').eq(2).contains('Initial version').should('not.exist')
})
})
it('Names the second version as v2', () => {
nameVersion(1, 'v2')
cy.get('#tab-version_vue').within(() => {
cy.get('[data-files-versions-version]').eq(1).contains('v2')
})
})
it('Names the current version as v3', () => {
nameVersion(0, 'v3')
cy.get('#tab-version_vue').within(() => {
cy.get('[data-files-versions-version]').eq(0).contains('v3 (Current version)')
})
})
context('Name versions of shared file', () => {
context('with edit permission', () => {
before(() => {
setupTestSharedFileFromUser(user, randomFileName, { update: true })
openVersionsPanel(randomFileName)
})
it('Names the versions', () => {
nameVersion(2, 'v1 - shared')
cy.get('#tab-version_vue').within(() => {
cy.get('[data-files-versions-version]').eq(2).contains('v1 - shared')
cy.get('[data-files-versions-version]').eq(2).contains('Initial version').should('not.exist')
})
nameVersion(1, 'v2 - shared')
cy.get('#tab-version_vue').within(() => {
cy.get('[data-files-versions-version]').eq(1).contains('v2 - shared')
})
nameVersion(0, 'v3 - shared')
cy.get('#tab-version_vue').within(() => {
cy.get('[data-files-versions-version]').eq(0).contains('v3 - shared (Current version)')
})
})
})
context('without edit permission', () => {
it('Does not show action', () => {
setupTestSharedFileFromUser(user, randomFileName, { update: false })
openVersionsPanel(randomFileName)
cy.get('[data-files-versions-version]').eq(0).find('.action-item__menutoggle').should('not.exist')
cy.get('[data-files-versions-version]').eq(0).get('[data-cy-version-action="label"]').should('not.exist')
doesNotHaveAction(1, 'label')
doesNotHaveAction(2, 'label')
})
it('Does not work without update permission through direct API access', () => {
let hostname: string
let fileId: string|undefined
let versionId: string|undefined
setupTestSharedFileFromUser(user, randomFileName, { update: false })
.then(recipient => {
openVersionsPanel(randomFileName)
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: 'PROPPATCH',
auth: { user: recipient.userId, pass: recipient.password },
headers: {
cookie: '',
},
body: `<?xml version="1.0"?>
<d:propertyupdate xmlns:d="DAV:"
xmlns:oc="http://owncloud.org/ns"
xmlns:nc="http://nextcloud.org/ns"
xmlns:ocs="http://open-collaboration-services.org/ns">
<d:set>
<d:prop>
<nc:version-label>not authorized labeling</nc:version-label>
</d:prop>
</d:set>
</d:propertyupdate>`,
url: `http://${hostname}/remote.php/dav/versions/${recipient.userId}/versions/${fileId}/${versionId}`,
failOnStatusCode: false,
})
.then(({ status }) => {
expect(status).to.equal(403)
})
})
})
})
})
})
})

@ -20,23 +20,20 @@
*
*/
import { assertVersionContent, clickPopperAction, openVersionMenu, openVersionsPanel, uploadThreeVersions } from './filesVersionsUtils'
const restoreVersion = (index: number) => {
cy.intercept('MOVE', '**/dav/versions/*/versions/**').as('restoreVersion')
openVersionMenu(index)
clickPopperAction('Restore version')
cy.wait('@restoreVersion')
}
import type { User } from '@nextcloud/cypress'
import { assertVersionContent, doesNotHaveAction, openVersionsPanel, setupTestSharedFileFromUser, restoreVersion, uploadThreeVersions } from './filesVersionsUtils'
import { getRowForFile } from '../files/FilesUtils'
describe('Versions restoration', () => {
let randomFileName = ''
let user: User
before(() => {
randomFileName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + '.txt'
cy.createRandomUser()
.then((user) => {
.then((_user) => {
user = _user
uploadThreeVersions(user, randomFileName)
cy.login(user)
cy.visit('/apps/files')
@ -44,8 +41,13 @@ describe('Versions restoration', () => {
})
})
it('Current version does not have restore action', () => {
doesNotHaveAction(0, 'restore')
})
it('Restores initial version', () => {
restoreVersion(2)
cy.get('#tab-version_vue').within(() => {
cy.get('[data-files-versions-version]').should('have.length', 3)
cy.get('[data-files-versions-version]').eq(0).contains('Current version')
@ -58,4 +60,69 @@ describe('Versions restoration', () => {
assertVersionContent(randomFileName, 1, 'v3')
assertVersionContent(randomFileName, 2, 'v2')
})
context('Restore versions of shared file', () => {
it('Works with update permission', () => {
setupTestSharedFileFromUser(user, randomFileName, { update: true })
openVersionsPanel(randomFileName)
it('Restores initial version', () => {
restoreVersion(2)
cy.get('#tab-version_vue').within(() => {
cy.get('[data-files-versions-version]').should('have.length', 3)
cy.get('[data-files-versions-version]').eq(0).contains('Current version')
cy.get('[data-files-versions-version]').eq(2).contains('Initial version').should('not.exist')
})
})
it('Downloads versions and assert there content', () => {
assertVersionContent(randomFileName, 0, 'v1')
assertVersionContent(randomFileName, 1, 'v3')
assertVersionContent(randomFileName, 2, 'v2')
})
})
it('Does not show action without delete permission', () => {
setupTestSharedFileFromUser(user, randomFileName, { update: false })
openVersionsPanel(randomFileName)
cy.get('[data-files-versions-version]').eq(0).find('.action-item__menutoggle').should('not.exist')
cy.get('[data-files-versions-version]').eq(0).get('[data-cy-version-action="restore"]').should('not.exist')
doesNotHaveAction(1, 'restore')
doesNotHaveAction(2, 'restore')
})
it('Does not work without update permission through direct API access', () => {
let hostname: string
let fileId: string|undefined
let versionId: string|undefined
setupTestSharedFileFromUser(user, randomFileName, { update: false })
.then(recipient => {
openVersionsPanel(randomFileName)
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: 'MOVE',
auth: { user: recipient.userId, pass: recipient.password },
headers: {
cookie: '',
Destination: 'https://nextcloud_server1.test/remote.php/dav/versions/admin/restore/target',
},
url: `http://${hostname}/remote.php/dav/versions/${recipient.userId}/versions/${fileId}/${versionId}`,
failOnStatusCode: false,
})
.then(({ status }) => {
expect(status).to.equal(403)
})
})
})
})
})
})

3
dist/1758-1758.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3
dist/4889-4889.js vendored

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…
Cancel
Save