From 2cd7b1bb82900644903d1778c17b2b1c89489e41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Mon, 11 May 2026 10:47:24 +0200 Subject: [PATCH] test: Add E2E test for searching with restricted ACL read permissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- cypress/e2e/groupfolders.cy.ts | 93 +++++++++++++++++++++++++++++++ cypress/e2e/unifiedSearchUtils.ts | 73 ++++++++++++++++++++++++ cypress/support/commands.ts | 3 +- 3 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 cypress/e2e/unifiedSearchUtils.ts diff --git a/cypress/e2e/groupfolders.cy.ts b/cypress/e2e/groupfolders.cy.ts index d9fa31504..fa7ea55cc 100644 --- a/cypress/e2e/groupfolders.cy.ts +++ b/cypress/e2e/groupfolders.cy.ts @@ -28,6 +28,12 @@ import { PERMISSION_READ, PERMISSION_WRITE, } from './groupfoldersUtils.ts' +import { + openUnifiedSearch, + searchCanLoadMoreResults, + searchFor, + searchHasResult, +} from './unifiedSearchUtils.ts' import { randHash } from '../utils/index.js' import { triggerActionForFile } from './files/filesUtils.ts' @@ -323,3 +329,90 @@ describe('Groupfolders ACLs and trashbin behavior', () => { }) }) + +describe('Groupfolders ACLs and unified search behavior', () => { + let user1: User + let user2: User + let managerUser: User + let groupFolderId: string + let groupName: string + let groupFolderName: string + + beforeEach(() => { + if (groupFolderId) { + deleteGroupFolder(groupFolderId) + } + groupName = `test_group_${randHash()}` + groupFolderName = `test_group_folder_${randHash()}` + + cy.createRandomUser() + .then(_user => { + user1 = _user + }) + cy.createRandomUser() + .then(_user => { + user2 = _user + }) + cy.createRandomUser() + .then(_user => { + managerUser = _user + + createGroup(groupName) + .then(() => { + addUserToGroup(groupName, user1.userId) + addUserToGroup(groupName, user2.userId) + addUserToGroup(groupName, managerUser.userId) + createGroupFolder(groupFolderName, groupName, [PERMISSION_READ, PERMISSION_WRITE, PERMISSION_DELETE]) + .then(_groupFolderId => { + groupFolderId = _groupFolderId + enableACLPermissions(groupFolderId) + addACLManagerUser(groupFolderId, managerUser.userId) + }) + }) + }) + }) + + it('Search for files in groupfolders with restricted read permissions', () => { + // Create two subfolders and twelve files alterning between subfolders + cy.login(managerUser) + cy.mkdir(managerUser, `/${groupFolderName}/subfolder1`) + cy.mkdir(managerUser, `/${groupFolderName}/subfolder2`) + // Use incremental mtimes to have a specific order in the results + const mtime = Date.now() / 1000 + for (let i = 0; i < 12; i = i + 2) { + cy.uploadContent(managerUser, new Blob([i]), 'text/plain', `/${groupFolderName}/subfolder1/test${i}.txt`, mtime + i) + cy.uploadContent(managerUser, new Blob([i + 1]), 'text/plain', `/${groupFolderName}/subfolder2/test${i + 1}.txt`, mtime + i + 1) + } + + // Set ACL permissions + setACLPermissions(groupFolderId, '/subfolder1', [`+${PERMISSION_READ}`], undefined, user1.userId) + setACLPermissions(groupFolderId, '/subfolder1', [`+${PERMISSION_READ}`], undefined, user2.userId) + setACLPermissions(groupFolderId, '/subfolder2', [`+${PERMISSION_READ}`], undefined, user1.userId) + setACLPermissions(groupFolderId, '/subfolder2', [`-${PERMISSION_READ}`], undefined, user2.userId) + + // user1 can find files in both subfolders + cy.login(user1) + cy.visit('/apps/files') + openUnifiedSearch() + searchFor('test') + searchHasResult('Files', `test11.txt in ${groupFolderName}/subfolder2`) + searchHasResult('Files', `test10.txt in ${groupFolderName}/subfolder1`) + searchHasResult('Files', `test9.txt in ${groupFolderName}/subfolder2`) + searchHasResult('Files', `test8.txt in ${groupFolderName}/subfolder1`) + searchHasResult('Files', `test7.txt in ${groupFolderName}/subfolder2`) + searchCanLoadMoreResults('Files') + + // user2 can find files only in subfolder1 + cy.login(user2) + cy.visit('/apps/files') + openUnifiedSearch() + searchFor('test') + searchHasResult('Files', `test10.txt in ${groupFolderName}/subfolder1`) + searchHasResult('Files', `test8.txt in ${groupFolderName}/subfolder1`) + searchHasResult('Files', `test6.txt in ${groupFolderName}/subfolder1`) + searchHasResult('Files', `test4.txt in ${groupFolderName}/subfolder1`) + searchHasResult('Files', `test2.txt in ${groupFolderName}/subfolder1`) + searchCanLoadMoreResults('Files') + }) + +}) diff --git a/cypress/e2e/unifiedSearchUtils.ts b/cypress/e2e/unifiedSearchUtils.ts new file mode 100644 index 000000000..c9f37f6d5 --- /dev/null +++ b/cypress/e2e/unifiedSearchUtils.ts @@ -0,0 +1,73 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/** + * Get the unified search modal (if open) + */ +export function getUnifiedSearchModal() { + return cy.get('[role="dialog"][id="unified-search"]') +} + +/** + * Open the unified search modal + */ +export function openUnifiedSearch() { + cy.get('button[aria-label="Unified search"]').click({ force: true }) + // wait for it to be open + getUnifiedSearchModal().should('be.visible') +} + +/** + * Searchs for the given string in the unified search modal + * + * @param string term the term to search for + */ +export function searchFor(term: string) { + getUnifiedSearchModal().find('[data-cy-unified-search-input]').type(term) +} + +/** + * Get search results main element + */ +export function getUnifiedSearchResults() { + return getUnifiedSearchModal().find('[class="unified-search-modal__results"]') +} + +/** + * Get search results list for a specific section + * + * @param string section the section + */ +export function getUnifiedSearchResultsForSection(section: string) { + return getUnifiedSearchResults().contains('[class="result-title"]', section).next('ul') +} + +/** + * Get search results footer for a specific section + * + * @param string section the section + */ +export function getUnifiedSearchResultsFooterForSection(section: string) { + return getUnifiedSearchResults().contains('[class="result-title"]', section).siblings('[class="result-footer"]').first() +} + +/** + * Checks that the given result is found in the given section + * + * @param string section the section + * @param string result the result in the section + */ +export function searchHasResult(section: string, result: string) { + getUnifiedSearchResultsForSection(section).contains(result).should('be.visible') +} + +/** + * Checks that more results can be loaded for the given section + * + * @param string section the section + */ +export function searchCanLoadMoreResults(section: string) { + getUnifiedSearchResultsFooterForSection(section).contains('Load more results').should('be.visible') +} diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 72d9eb6e8..ca820eeff 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -92,7 +92,7 @@ Cypress.Commands.add('uploadFile', (user, fixture = 'image.jpg', mimeType = 'ima * @param {string} mimeType e.g. image/png * @param {string} target the target of the file relative to the user root */ -Cypress.Commands.add('uploadContent', (user, blob, mimeType, target) => { +Cypress.Commands.add('uploadContent', (user, blob, mimeType, target, mtime?) => { cy.clearCookies() .then({ timeout: 8000 }, async () => { const fileName = basename(target) @@ -108,6 +108,7 @@ Cypress.Commands.add('uploadContent', (user, blob, mimeType, target) => { data: file, headers: { 'Content-Type': mimeType, + 'X-OC-MTime': mtime ? `${mtime}` : undefined, }, auth: { username: user.userId,