From bb592972e5a43c5dedcb0001d234c0903e7d0702 Mon Sep 17 00:00:00 2001 From: Louis Chemineau Date: Wed, 13 Mar 2024 13:48:42 +0100 Subject: [PATCH 1/2] chore(files_versions): Use new metadata API for versions As of: https://github.com/nextcloud/server/pull/44049 Server equivalent: https://github.com/nextcloud/server/pull/44175 Signed-off-by: Louis Chemineau --- lib/AppInfo/Application.php | 1 + lib/Versions/GroupVersion.php | 4 +- lib/Versions/GroupVersionEntity.php | 30 ++++++++----- lib/Versions/VersionsBackend.php | 69 +++++++++++++++++++---------- tests/stub.phpstub | 36 +++++++++++++-- 5 files changed, 102 insertions(+), 38 deletions(-) diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index a06bde607..ef002ea6e 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -158,6 +158,7 @@ public function register(IRegistrationContext $context): void { $c->get(LoggerInterface::class), $c->get(GroupVersionsMapper::class), $c->get(IMimeTypeLoader::class), + $c->get(IUserSession::class), ); }); diff --git a/lib/Versions/GroupVersion.php b/lib/Versions/GroupVersion.php index 4715c6181..22745e77f 100644 --- a/lib/Versions/GroupVersion.php +++ b/lib/Versions/GroupVersion.php @@ -40,11 +40,11 @@ public function __construct( FileInfo $sourceFileInfo, IVersionBackend $backend, IUser $user, - string $label, + array $metadata, private File $versionFile, private int $folderId, ) { - parent::__construct($timestamp, $revisionId, $name, $size, $mimetype, $path, $sourceFileInfo, $backend, $user, $label); + parent::__construct($timestamp, $revisionId, $name, $size, $mimetype, $path, $sourceFileInfo, $backend, $user, $metadata); } public function getVersionFile(): File { diff --git a/lib/Versions/GroupVersionEntity.php b/lib/Versions/GroupVersionEntity.php index 6284e18ba..d46c9a65a 100644 --- a/lib/Versions/GroupVersionEntity.php +++ b/lib/Versions/GroupVersionEntity.php @@ -70,23 +70,33 @@ public function jsonSerialize(): array { ]; } - public function getLabel(): string { - return $this->getDecodedMetadata()['label'] ?? ''; + public function getDecodedMetadata(): array { + return json_decode($this->metadata ?? '', true, 512, JSON_THROW_ON_ERROR) ?? []; } - public function setLabel(string $label): void { - $metadata = $this->getDecodedMetadata(); - $metadata['label'] = $label; - $this->setDecodedMetadata($metadata); + public function setDecodedMetadata(array $value): void { + $this->metadata = json_encode($value, JSON_THROW_ON_ERROR); $this->markFieldUpdated('metadata'); } - public function getDecodedMetadata(): array { - return json_decode($this->metadata ?? '', true, 512, JSON_THROW_ON_ERROR) ?? []; + /** + * @abstract given a key, return the value associated with the key in the metadata column + * if nothing is found, we return an empty string + * @param string $key key associated with the value + */ + public function getMetadataValue(string $key): ?string { + return $this->getDecodedMetadata()[$key] ?? null; } - public function setDecodedMetadata(array $value): void { - $this->metadata = json_encode($value, JSON_THROW_ON_ERROR); + /** + * @abstract sets a key value pair in the metadata column + * @param string $key key associated with the value + * @param string $value value associated with the key + */ + public function setMetadataValue(string $key, string $value): void { + $metadata = $this->getDecodedMetadata(); + $metadata[$key] = $value; + $this->setDecodedMetadata($metadata); $this->markFieldUpdated('metadata'); } } diff --git a/lib/Versions/VersionsBackend.php b/lib/Versions/VersionsBackend.php index 1297e291b..fcb793396 100644 --- a/lib/Versions/VersionsBackend.php +++ b/lib/Versions/VersionsBackend.php @@ -23,8 +23,9 @@ namespace OCA\GroupFolders\Versions; +use OCA\DAV\Connector\Sabre\Exception\Forbidden; use OCA\Files_Versions\Versions\IDeletableVersionBackend; -use OCA\Files_Versions\Versions\INameableVersionBackend; +use OCA\Files_Versions\Versions\IMetadataVersionBackend; use OCA\Files_Versions\Versions\INeedSyncVersionBackend; use OCA\Files_Versions\Versions\IVersion; use OCA\Files_Versions\Versions\IVersionBackend; @@ -41,9 +42,10 @@ use OCP\Files\NotFoundException; use OCP\Files\Storage\IStorage; use OCP\IUser; +use OCP\IUserSession; use Psr\Log\LoggerInterface; -class VersionsBackend implements IVersionBackend, INameableVersionBackend, IDeletableVersionBackend, INeedSyncVersionBackend { +class VersionsBackend implements IVersionBackend, IMetadataVersionBackend, IDeletableVersionBackend, INeedSyncVersionBackend { public function __construct( private IRootFolder $rootFolder, private Folder $appFolder, @@ -52,6 +54,7 @@ public function __construct( private LoggerInterface $logger, private GroupVersionsMapper $groupVersionsMapper, private IMimeTypeLoader $mimeTypeLoader, + private IUserSession $userSession, ) { } @@ -165,7 +168,7 @@ function (GroupVersionEntity $versionEntity) use ($versionsFolder, $mountPoint, $fileInfo, $this, $user, - $versionEntity->getLabel(), + $versionEntity->getDecodedMetadata(), $versionFile, $folderId, ); @@ -206,21 +209,27 @@ public function createVersion(IUser $user, FileInfo $file) { } public function rollback(IVersion $version): void { - if ($version instanceof GroupVersion) { - $this->createVersion($version->getUser(), $version->getSourceFile()); + if (!($version instanceof GroupVersion)) { + throw new \LogicException('Trying to restore a version from a file not in a group folder'); + } - /** @var GroupMountPoint $targetMount */ - $targetMount = $version->getSourceFile()->getMountPoint(); - $targetCache = $targetMount->getStorage()->getCache(); - $versionMount = $version->getVersionFile()->getMountPoint(); - $versionCache = $versionMount->getStorage()->getCache(); + if (!$this->currentUserHasPermissions($version->getSourceFile(), \OCP\Constants::PERMISSION_UPDATE)) { + throw new Forbidden('You cannot restore this version because you do not have update permissions on the source file.'); + } - $targetInternalPath = $version->getSourceFile()->getInternalPath(); - $versionInternalPath = $version->getVersionFile()->getInternalPath(); + $this->createVersion($version->getUser(), $version->getSourceFile()); - $targetMount->getStorage()->copyFromStorage($versionMount->getStorage(), $versionInternalPath, $targetInternalPath); - $versionMount->getStorage()->getCache()->copyFromCache($targetCache, $versionCache->get($versionInternalPath), $targetMount->getSourcePath() . '/' . $targetInternalPath); - } + /** @var GroupMountPoint $targetMount */ + $targetMount = $version->getSourceFile()->getMountPoint(); + $targetCache = $targetMount->getStorage()->getCache(); + $versionMount = $version->getVersionFile()->getMountPoint(); + $versionCache = $versionMount->getStorage()->getCache(); + + $targetInternalPath = $version->getSourceFile()->getInternalPath(); + $versionInternalPath = $version->getVersionFile()->getInternalPath(); + + $targetMount->getStorage()->copyFromStorage($versionMount->getStorage(), $versionInternalPath, $targetInternalPath); + $versionMount->getStorage()->getCache()->copyFromCache($targetCache, $versionCache->get($versionInternalPath), $targetMount->getSourcePath() . '/' . $targetInternalPath); } public function read(IVersion $version) { @@ -236,6 +245,7 @@ public function getVersionFile(IUser $user, FileInfo $sourceFile, $revision): Fi if (!($mount instanceof GroupMountPoint)) { throw new \LogicException('Trying to getVersionFile from a file not in a mounted group folder'); } + try { /** @var Folder $versionsFolder */ $versionsFolder = $this->getVersionsFolder($mount->getFolderId())->get((string)$sourceFile->getId()); @@ -297,19 +307,22 @@ private function getVersionsFolder(int $folderId): Folder { } } - public function setVersionLabel(IVersion $version, string $label): void { - $versionEntity = $this->groupVersionsMapper->findVersionForFileId( - $version->getSourceFile()->getId(), - $version->getTimestamp(), - ); - if (trim($label) === '') { - $label = null; + public function setMetadataValue(Node $node, int $revision, string $key, string $value): void { + if (!$this->currentUserHasPermissions($node, \OCP\Constants::PERMISSION_UPDATE)) { + throw new Forbidden('You cannot update the version\'s metadata because you do not have update permissions on the source file.'); } - $versionEntity->setLabel($label ?? ''); + + $versionEntity = $this->groupVersionsMapper->findVersionForFileId($node->getId(), $revision); + + $versionEntity->setMetadataValue($key, $value); $this->groupVersionsMapper->update($versionEntity); } public function deleteVersion(IVersion $version): void { + if (!$this->currentUserHasPermissions($version->getSourceFile(), \OCP\Constants::PERMISSION_DELETE)) { + throw new Forbidden('You cannot delete this version because you do not have delete permissions on the source file.'); + } + $sourceFile = $version->getSourceFile(); $mount = $sourceFile->getMountPoint(); @@ -359,4 +372,14 @@ public function updateVersionEntity(File $sourceFile, int $revision, array $prop public function deleteVersionsEntity(File $file): void { $this->groupVersionsMapper->deleteAllVersionsForFileId($file->getId()); } + + private function currentUserHasPermissions(FileInfo $sourceFile, int $permissions): bool { + $currentUserId = $this->userSession->getUser()?->getUID(); + + if ($currentUserId === null) { + throw new NotFoundException("No user logged in"); + } + + return ($sourceFile->getPermissions() & $permissions) === $permissions; + } } diff --git a/tests/stub.phpstub b/tests/stub.phpstub index 688833009..6676c3348 100644 --- a/tests/stub.phpstub +++ b/tests/stub.phpstub @@ -288,6 +288,8 @@ namespace OCA\Files_Versions\Versions { use OCP\Files\NotFoundException; use OCP\Files\Storage\IStorage; use OCP\IUser; + use OCP\Files\Node; + /** * @since 15.0.0 @@ -351,8 +353,8 @@ namespace OCA\Files_Versions\Versions { public function getVersionFile(\OCP\IUser $user, \OCP\Files\FileInfo $sourceFile, $revision) : \OCP\Files\File; } - interface INameableVersionBackend { - public function setVersionLabel(IVersion $version, string $label): void; + interface IMetadataVersionBackend { + public function setMetadataValue(Node $node, int $revision, string $key, string $value): void; } interface IDeletableVersionBackend { @@ -399,7 +401,7 @@ namespace OCA\Files_Versions\Versions { FileInfo $sourceFileInfo, IVersionBackend $backend, IUser $user, - string $label = '' + array $metadata = [], ) { } @@ -438,6 +440,10 @@ namespace OCA\Files_Versions\Versions { public function getUser(): IUser { throw new \Exception('stub'); } + + public function getMetadataValue(string $key): ?string { + return $this->metadata[$key] ?? null; + } } } @@ -1583,3 +1589,27 @@ namespace OCA\Circles\Events { public function getCircle(): Circle {} } } + + +namespace OCA\DAV\Connector\Sabre\Exception { + class Forbidden extends \Sabre\DAV\Exception\Forbidden { + public const NS_OWNCLOUD = 'http://owncloud.org/ns'; + + /** + * @param string $message + * @param bool $retry + * @param \Exception $previous + */ + public function __construct($message, $retry = false, \Exception $previous = null) {} + + /** + * This method allows the exception to include additional information + * into the WebDAV error response + * + * @param \Sabre\DAV\Server $server + * @param \DOMElement $errorNode + * @return void + */ + public function serialize(\Sabre\DAV\Server $server, \DOMElement $errorNode) {} + } +} \ No newline at end of file From 7ae22237847ea7f7a1cb1768cb7b487a5c4af123 Mon Sep 17 00:00:00 2001 From: Louis Chemineau Date: Wed, 13 Mar 2024 15:29:39 +0100 Subject: [PATCH 2/2] test(files_versions): Add tests for files versions Signed-off-by: Louis Chemineau --- cypress/e2e/files/filesUtils.ts | 63 ++++++++++++ .../e2e/files_versions/filesVersionsUtils.ts | 99 +++++++++++++++++++ .../e2e/files_versions/version_creation.cy.ts | 65 ++++++++++++ .../e2e/files_versions/version_deletion.cy.ts | 63 ++++++++++++ .../e2e/files_versions/version_download.cy.ts | 63 ++++++++++++ .../e2e/files_versions/version_naming.cy.ts | 75 ++++++++++++++ .../files_versions/version_restoration.cy.ts | 77 +++++++++++++++ cypress/e2e/groupfoldersUtils.ts | 28 +++--- 8 files changed, 519 insertions(+), 14 deletions(-) create mode 100644 cypress/e2e/files/filesUtils.ts create mode 100644 cypress/e2e/files_versions/filesVersionsUtils.ts create mode 100644 cypress/e2e/files_versions/version_creation.cy.ts create mode 100644 cypress/e2e/files_versions/version_deletion.cy.ts create mode 100644 cypress/e2e/files_versions/version_download.cy.ts create mode 100644 cypress/e2e/files_versions/version_naming.cy.ts create mode 100644 cypress/e2e/files_versions/version_restoration.cy.ts diff --git a/cypress/e2e/files/filesUtils.ts b/cypress/e2e/files/filesUtils.ts new file mode 100644 index 000000000..96bff2f85 --- /dev/null +++ b/cypress/e2e/files/filesUtils.ts @@ -0,0 +1,63 @@ +/** + * @copyright Copyright (c) 2024 Ferdinand Thiessen + * + * @author Ferdinand Thiessen + * + * @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 . + * + */ + +export const getRowForFile = (filename: string) => cy.get(`[data-cy-files-list-row-name="${CSS.escape(filename)}"]`) + +export const getActionsForFile = (filename: string) => getRowForFile(filename).find('[data-cy-files-list-row-actions]') + +export const getActionButtonForFile = (filename: string) => getActionsForFile(filename).find('button[aria-label="Actions"]') + +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 navigateToFolder = (folderName: string) => { + getRowForFile(folderName).should('be.visible').find('[data-cy-files-list-row-name-link]').click() +} diff --git a/cypress/e2e/files_versions/filesVersionsUtils.ts b/cypress/e2e/files_versions/filesVersionsUtils.ts new file mode 100644 index 000000000..bef0cc28d --- /dev/null +++ b/cypress/e2e/files_versions/filesVersionsUtils.ts @@ -0,0 +1,99 @@ +/* eslint-disable jsdoc/require-jsdoc */ +/** + * @copyright Copyright (c) 2022 Louis Chemineau + * + * @author Louis Chemineau + * + * @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 . + * + */ + +import type { User } from '@nextcloud/cypress' +import path from 'path' + +export const uploadThreeVersions = (user: User, fileName: string) => { + // A new version will not be created if the changes occur + // within less than one second of each other. + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.uploadContent(user, new Blob(['v1'], { type: 'text/plain' }), 'text/plain', `/${fileName}`) + .wait(1100) + .uploadContent(user, new Blob(['v2'], { type: 'text/plain' }), 'text/plain', `/${fileName}`) + .wait(1100) + .uploadContent(user, new Blob(['v3'], { type: 'text/plain' }), 'text/plain', `/${fileName}`) + cy.login(user) +} + +export function openVersionsPanel(fileName: string) { + // Detect the versions list fetch + cy.intercept('PROPFIND', '**/dav/versions/*/versions/**').as('getVersions') + + // Open the versions tab + cy.window().then(win => { + win.OCA.Files.Sidebar.setActiveTab('version_vue') + win.OCA.Files.Sidebar.open(`/${fileName}`) + }) + + // Wait for the versions list to be fetched + cy.wait('@getVersions') + cy.get('#tab-version_vue').should('be.visible', { timeout: 10000 }) +} + +export function toggleVersionMenu(index: number) { + cy.get('#tab-version_vue [data-files-versions-version]') + .eq(index) + .find('button') + .click() +} + +export function triggerVersionAction(index: number, actionName: string) { + toggleVersionMenu(index) + cy.get(`[data-cy-files-versions-version-action="${actionName}"]`).filter(':visible').click() +} + +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 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') + + triggerVersionAction(index, 'download') + + return cy.readFile(path.join(downloadsFolder, filename)) + .then((versionContent) => expect(versionContent).to.equal(expectedContent)) + .then(() => cy.exec(`rm ${downloadsFolder}/${filename}`)) +} diff --git a/cypress/e2e/files_versions/version_creation.cy.ts b/cypress/e2e/files_versions/version_creation.cy.ts new file mode 100644 index 000000000..03282ae59 --- /dev/null +++ b/cypress/e2e/files_versions/version_creation.cy.ts @@ -0,0 +1,65 @@ +/** + * @copyright Copyright (c) 2022 Louis Chmn + * + * @author Louis Chmn + * + * @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 . + * + */ + +import type { User } from '@nextcloud/cypress' + +import { PERMISSION_DELETE, PERMISSION_READ, PERMISSION_WRITE, addUserToGroup, createGroup, createGroupFolder } from '../groupfoldersUtils' +import { openVersionsPanel, uploadThreeVersions } from './filesVersionsUtils' +import { navigateToFolder } from '../files/filesUtils' + +describe('Versions creation', () => { + let randomGroupName: string + let randomGroupFolderName: string + let randomFileName: string + let randomFilePath: string + let user1: User + + before(() => { + randomGroupName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + randomGroupFolderName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + randomFileName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + '.txt' + randomFilePath = `${randomGroupFolderName}/${randomFileName}` + + cy.createRandomUser().then(_user => { user1 = _user }) + createGroup(randomGroupName) + + cy.then(() => { + addUserToGroup(randomGroupName, user1.userId) + createGroupFolder(randomGroupFolderName, randomGroupName, [PERMISSION_READ, PERMISSION_WRITE, PERMISSION_DELETE]) + + uploadThreeVersions(user1, randomFilePath) + cy.login(user1) + }) + + cy.visit('/apps/files') + navigateToFolder(randomGroupFolderName) + openVersionsPanel(randomFilePath) + }) + + it('Opens the versions panel and sees the versions', () => { + 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') + }) + }) +}) diff --git a/cypress/e2e/files_versions/version_deletion.cy.ts b/cypress/e2e/files_versions/version_deletion.cy.ts new file mode 100644 index 000000000..9c7804d9c --- /dev/null +++ b/cypress/e2e/files_versions/version_deletion.cy.ts @@ -0,0 +1,63 @@ +/** + * @copyright Copyright (c) 2024 Louis Chmn + * + * @author Louis Chmn + * + * @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 . + * + */ + +import type { User } from '@nextcloud/cypress' + +import { openVersionsPanel, uploadThreeVersions, deleteVersion } from './filesVersionsUtils' +import { navigateToFolder } from '../files/filesUtils' +import { PERMISSION_DELETE, PERMISSION_READ, PERMISSION_WRITE, addUserToGroup, createGroup, createGroupFolder } from '../groupfoldersUtils' + +describe('Versions restoration', () => { + let randomGroupName: string + let randomGroupFolderName: string + let randomFileName: string + let randomFilePath: string + let user1: User + + before(() => { + randomGroupName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + randomGroupFolderName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + randomFileName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + '.txt' + randomFilePath = `${randomGroupFolderName}/${randomFileName}` + + cy.createRandomUser().then(_user => { user1 = _user }) + createGroup(randomGroupName) + + cy.then(() => { + addUserToGroup(randomGroupName, user1.userId) + createGroupFolder(randomGroupFolderName, randomGroupName, [PERMISSION_READ, PERMISSION_WRITE, PERMISSION_DELETE]) + + uploadThreeVersions(user1, randomFilePath) + cy.login(user1) + }) + + cy.visit('/apps/files') + navigateToFolder(randomGroupFolderName) + openVersionsPanel(randomFilePath) + }) + + it('Delete initial version', () => { + cy.get('[data-files-versions-version]').should('have.length', 3) + deleteVersion(2) + cy.get('[data-files-versions-version]').should('have.length', 2) + }) +}) diff --git a/cypress/e2e/files_versions/version_download.cy.ts b/cypress/e2e/files_versions/version_download.cy.ts new file mode 100644 index 000000000..fe9019b53 --- /dev/null +++ b/cypress/e2e/files_versions/version_download.cy.ts @@ -0,0 +1,63 @@ +/** + * @copyright Copyright (c) 2022 Louis Chmn + * + * @author Louis Chmn + * + * @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 . + * + */ + +import type { User } from '@nextcloud/cypress' + +import { assertVersionContent, openVersionsPanel, uploadThreeVersions } from './filesVersionsUtils' +import { PERMISSION_DELETE, PERMISSION_READ, PERMISSION_WRITE, addUserToGroup, createGroup, createGroupFolder } from '../groupfoldersUtils' +import { navigateToFolder } from '../files/filesUtils' + +describe('Versions download', () => { + let randomGroupName: string + let randomGroupFolderName: string + let randomFileName: string + let randomFilePath: string + let user1: User + + before(() => { + randomGroupName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + randomGroupFolderName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + randomFileName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + '.txt' + randomFilePath = `${randomGroupFolderName}/${randomFileName}` + + cy.createRandomUser().then(_user => { user1 = _user }) + createGroup(randomGroupName) + + cy.then(() => { + addUserToGroup(randomGroupName, user1.userId) + createGroupFolder(randomGroupFolderName, randomGroupName, [PERMISSION_READ, PERMISSION_WRITE, PERMISSION_DELETE]) + + uploadThreeVersions(user1, randomFilePath) + cy.login(user1) + }) + + cy.visit('/apps/files') + navigateToFolder(randomGroupFolderName) + openVersionsPanel(randomFilePath) + }) + + it('Download versions and assert their content', () => { + assertVersionContent(randomFileName, 0, 'v3') + assertVersionContent(randomFileName, 1, 'v2') + assertVersionContent(randomFileName, 2, 'v1') + }) +}) diff --git a/cypress/e2e/files_versions/version_naming.cy.ts b/cypress/e2e/files_versions/version_naming.cy.ts new file mode 100644 index 000000000..fec1a154a --- /dev/null +++ b/cypress/e2e/files_versions/version_naming.cy.ts @@ -0,0 +1,75 @@ +/** + * @copyright Copyright (c) 2022 Louis Chmn + * + * @author Louis Chmn + * + * @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 . + * + */ + +import type { User } from '@nextcloud/cypress' + +import { nameVersion, openVersionsPanel, uploadThreeVersions } from './filesVersionsUtils' +import { PERMISSION_DELETE, PERMISSION_READ, PERMISSION_WRITE, addUserToGroup, createGroup, createGroupFolder } from '../groupfoldersUtils' +import { navigateToFolder } from '../files/filesUtils' + +describe('Versions naming', () => { + let randomGroupName: string + let randomGroupFolderName: string + let randomFileName: string + let randomFilePath: string + let user1: User + + before(() => { + randomGroupName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + randomGroupFolderName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + randomFileName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + '.txt' + randomFilePath = `${randomGroupFolderName}/${randomFileName}` + + cy.createRandomUser().then(_user => { user1 = _user }) + createGroup(randomGroupName) + + cy.then(() => { + addUserToGroup(randomGroupName, user1.userId) + createGroupFolder(randomGroupFolderName, randomGroupName, [PERMISSION_READ, PERMISSION_WRITE, PERMISSION_DELETE]) + + uploadThreeVersions(user1, randomFilePath) + cy.login(user1) + }) + + cy.visit('/apps/files') + navigateToFolder(randomGroupFolderName) + openVersionsPanel(randomFilePath) + }) + + 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') + }) + + nameVersion(1, 'v2') + cy.get('#tab-version_vue').within(() => { + cy.get('[data-files-versions-version]').eq(1).contains('v2') + }) + + nameVersion(0, 'v3') + cy.get('#tab-version_vue').within(() => { + cy.get('[data-files-versions-version]').eq(0).contains('v3 (Current version)') + }) + }) +}) diff --git a/cypress/e2e/files_versions/version_restoration.cy.ts b/cypress/e2e/files_versions/version_restoration.cy.ts new file mode 100644 index 000000000..ae2e68a50 --- /dev/null +++ b/cypress/e2e/files_versions/version_restoration.cy.ts @@ -0,0 +1,77 @@ +/** + * @copyright Copyright (c) 2022 Louis Chmn + * + * @author Louis Chmn + * + * @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 . + * + */ + +import type { User } from '@nextcloud/cypress' + +import { assertVersionContent, doesNotHaveAction, openVersionsPanel, restoreVersion, uploadThreeVersions } from './filesVersionsUtils' +import { PERMISSION_DELETE, PERMISSION_READ, PERMISSION_WRITE, addUserToGroup, createGroup, createGroupFolder } from '../groupfoldersUtils' +import { navigateToFolder } from '../files/filesUtils' + +describe('Versions restoration', () => { + let randomGroupName: string + let randomGroupFolderName: string + let randomFileName: string + let randomFilePath: string + let user1: User + + before(() => { + randomGroupName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + randomGroupFolderName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + randomFileName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + '.txt' + randomFilePath = `${randomGroupFolderName}/${randomFileName}` + + cy.createRandomUser().then(_user => { user1 = _user }) + createGroup(randomGroupName) + + cy.then(() => { + addUserToGroup(randomGroupName, user1.userId) + createGroupFolder(randomGroupFolderName, randomGroupName, [PERMISSION_READ, PERMISSION_WRITE, PERMISSION_DELETE]) + + uploadThreeVersions(user1, randomFilePath) + cy.login(user1) + }) + + cy.visit('/apps/files') + navigateToFolder(randomGroupFolderName) + openVersionsPanel(randomFilePath) + }) + + 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') + 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') + }) +}) diff --git a/cypress/e2e/groupfoldersUtils.ts b/cypress/e2e/groupfoldersUtils.ts index bff57883f..090064fbe 100644 --- a/cypress/e2e/groupfoldersUtils.ts +++ b/cypress/e2e/groupfoldersUtils.ts @@ -76,25 +76,25 @@ export function deleteGroupFolder(groupFolderId: string) { export function fileOrFolderExists(name: string) { // Make sure file list is loaded first - cy.get(`[data-cy-files-list-tfoot],[data-cy-files-content-empty]`).should('be.visible') + cy.get('[data-cy-files-list-tfoot],[data-cy-files-content-empty]').should('be.visible') cy.get(`[data-cy-files-list] [data-cy-files-list-row-name="${name}"]`).should('be.visible') } export function fileOrFolderDoesNotExist(name: string) { // Make sure file list is loaded first - cy.get(`[data-cy-files-list-tfoot],[data-cy-files-content-empty]`).should('be.visible') + cy.get('[data-cy-files-list-tfoot],[data-cy-files-content-empty]').should('be.visible') cy.get(`[data-cy-files-list] [data-cy-files-list-row-name="${name}"]`).should('not.exist') } export function fileOrFolderExistsInTrashbin(name: string) { // Make sure file list is loaded first - cy.get(`[data-cy-files-list-tfoot],[data-cy-files-content-empty]`).should('be.visible') + cy.get('[data-cy-files-list-tfoot],[data-cy-files-content-empty]').should('be.visible') cy.get(`[data-cy-files-list] [data-cy-files-list-row-name^="${name}.d"]`).should('be.visible') } export function fileOrFolderDoesNotExistInTrashbin(name: string) { // Make sure file list is loaded first - cy.get(`[data-cy-files-list-tfoot],[data-cy-files-content-empty]`).should('be.visible') + cy.get('[data-cy-files-list-tfoot],[data-cy-files-content-empty]').should('be.visible') cy.get(`[data-cy-files-list] [data-cy-files-list-row-name^="${name}.d"]`).should('not.exist') } @@ -113,9 +113,9 @@ export function enterFolderInTrashbin(name: string) { export function deleteFolder(name: string) { cy.intercept({ times: 1, method: 'DELETE', url: `**/dav/files/**/${name}` }).as('delete') cy.get(`[data-cy-files-list] [data-cy-files-list-row-name="${name}"] [data-cy-files-list-row-actions]`).click() - cy.get(`[data-cy-files-list] [data-cy-files-list-row-action="delete"]`).should('be.visible') - cy.get(`[data-cy-files-list] [data-cy-files-list-row-action="delete"]`).scrollIntoView() - cy.get(`[data-cy-files-list] [data-cy-files-list-row-action="delete"]`).click() + cy.get('[data-cy-files-list] [data-cy-files-list-row-action="delete"]').should('be.visible') + cy.get('[data-cy-files-list] [data-cy-files-list-row-action="delete"]').scrollIntoView() + cy.get('[data-cy-files-list] [data-cy-files-list-row-action="delete"]').click() cy.wait('@delete').its('response.statusCode').should('eq', 204) } @@ -123,15 +123,15 @@ export function deleteFile(name: string) { cy.intercept({ times: 1, method: 'DELETE', url: `**/dav/files/**/${name}` }).as('delete') // For files wait for preview to load and release lock cy.get(`[data-cy-files-list] [data-cy-files-list-row-name="${name}"] .files-list__row-icon img`) - .should('be.visible') - .and(($img) => { + .should('be.visible') + .and(($img) => { // "naturalWidth" and "naturalHeight" are set when the image loads - expect($img[0].naturalWidth, 'image has natural width').to.be.greaterThan(0) - }) + expect($img[0].naturalWidth, 'image has natural width').to.be.greaterThan(0) + }) cy.get(`[data-cy-files-list] [data-cy-files-list-row-name="${name}"] [data-cy-files-list-row-actions]`).click() - cy.get(`[data-cy-files-list] [data-cy-files-list-row-action="delete"]`).should('be.visible') - cy.get(`[data-cy-files-list] [data-cy-files-list-row-action="delete"]`).scrollIntoView() - cy.get(`[data-cy-files-list] [data-cy-files-list-row-action="delete"]`).click() + cy.get('[data-cy-files-list] [data-cy-files-list-row-action="delete"]').should('be.visible') + cy.get('[data-cy-files-list] [data-cy-files-list-row-action="delete"]').scrollIntoView() + cy.get('[data-cy-files-list] [data-cy-files-list-row-action="delete"]').click() cy.wait('@delete').its('response.statusCode').should('eq', 204) }