diff options
Diffstat (limited to 'cypress/e2e/settings')
-rw-r--r-- | cypress/e2e/settings/access-levels.cy.ts | 65 | ||||
-rw-r--r-- | cypress/e2e/settings/apps.cy.ts | 156 | ||||
-rw-r--r-- | cypress/e2e/settings/personal-info.cy.ts | 448 | ||||
-rw-r--r-- | cypress/e2e/settings/users-group-admin.cy.ts | 186 | ||||
-rw-r--r-- | cypress/e2e/settings/users.cy.ts | 129 | ||||
-rw-r--r-- | cypress/e2e/settings/usersUtils.ts | 90 | ||||
-rw-r--r-- | cypress/e2e/settings/users_columns.cy.ts | 94 | ||||
-rw-r--r-- | cypress/e2e/settings/users_disable.cy.ts | 79 | ||||
-rw-r--r-- | cypress/e2e/settings/users_groups.cy.ts | 291 | ||||
-rw-r--r-- | cypress/e2e/settings/users_manager.cy.ts | 121 | ||||
-rw-r--r-- | cypress/e2e/settings/users_modify.cy.ts | 225 |
11 files changed, 1884 insertions, 0 deletions
diff --git a/cypress/e2e/settings/access-levels.cy.ts b/cypress/e2e/settings/access-levels.cy.ts new file mode 100644 index 00000000000..4bf0cbc1832 --- /dev/null +++ b/cypress/e2e/settings/access-levels.cy.ts @@ -0,0 +1,65 @@ +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { User } from '@nextcloud/cypress' +import { clearState, getNextcloudUserMenu, getNextcloudUserMenuToggle } from '../../support/commonUtils' + +const admin = new User('admin', 'admin') + +describe('Settings: Ensure only administrator can see the administration settings section', { testIsolation: true }, () => { + beforeEach(() => { + clearState() + }) + + it('Regular users cannot see admin-level items on the Settings page', () => { + // Given I am logged in + cy.createRandomUser().then(($user) => { + cy.login($user) + cy.visit('/') + }) + + // I open the settings menu + getNextcloudUserMenuToggle().click() + // I navigate to the settings panel + getNextcloudUserMenu() + .findByRole('link', { name: /settings/i }) + .click() + cy.url().should('match', /\/settings\/user$/) + + cy.get('#app-navigation').should('be.visible').within(() => { + // I see the personal section is NOT shown + cy.get('#app-navigation-caption-personal').should('not.exist') + // I see the admin section is NOT shown + cy.get('#app-navigation-caption-administration').should('not.exist') + + // I see that the "Personal info" entry in the settings panel is shown + cy.get('[data-section-id="personal-info"]').should('exist').and('be.visible') + }) + }) + + it('Admin users can see admin-level items on the Settings page', () => { + // Given I am logged in + cy.login(admin) + cy.visit('/') + + // I open the settings menu + getNextcloudUserMenuToggle().click() + // I navigate to the settings panel + getNextcloudUserMenu() + .findByRole('link', { name: /Personal settings/i }) + .click() + cy.url().should('match', /\/settings\/user$/) + + cy.get('#app-navigation').should('be.visible').within(() => { + // I see the personal section is shown + cy.get('#app-navigation-caption-personal').should('be.visible') + // I see the admin section is shown + cy.get('#app-navigation-caption-administration').should('be.visible') + + // I see that the "Personal info" entry in the settings panel is shown + cy.get('[data-section-id="personal-info"]').should('exist').and('be.visible') + }) + }) +}) diff --git a/cypress/e2e/settings/apps.cy.ts b/cypress/e2e/settings/apps.cy.ts new file mode 100644 index 00000000000..0df073271ef --- /dev/null +++ b/cypress/e2e/settings/apps.cy.ts @@ -0,0 +1,156 @@ +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { User } from '@nextcloud/cypress' +import { handlePasswordConfirmation } from './usersUtils' + +const admin = new User('admin', 'admin') + +describe('Settings: App management', { testIsolation: true }, () => { + beforeEach(() => { + // disable QA if already enabled + cy.runOccCommand('app:disable -n testing') + // enable notification if already disabled + cy.runOccCommand('app:enable -n updatenotification') + + // I am logged in as the admin + cy.login(admin) + + // Intercept the apps list request + cy.intercept('GET', '*/settings/apps/list').as('fetchAppsList') + + // I open the Apps management + cy.visit('/settings/apps/installed') + + // Wait for the apps list to load + cy.wait('@fetchAppsList') + }) + + it('Can enable an installed app', () => { + cy.get('#apps-list').should('exist') + // Wait for the app list to load + .contains('tr', 'QA testing', { timeout: 10000 }) + .should('exist') + // I enable the "QA testing" app + .contains('button', 'Enable') + .click({ force: true }) + + handlePasswordConfirmation(admin.password) + + // Wait until we see the disable button for the app + cy.get('#apps-list').should('exist') + .contains('tr', 'QA testing') + .should('exist') + // I see the disable button for the app + .contains('button', 'Disable', { timeout: 10000 }) + + // Change to enabled apps view + cy.get('#app-category-enabled a').click({ force: true }) + cy.url().should('match', /settings\/apps\/enabled$/) + // I see that the "QA testing" app has been enabled + cy.get('#apps-list').contains('tr', 'QA testing') + }) + + it('Can disable an installed app', () => { + cy.get('#apps-list') + .should('exist') + // Wait for the app list to load + .contains('tr', 'Update notification', { timeout: 10000 }) + .should('exist') + // I disable the "Update notification" app + .contains('button', 'Disable') + .click({ force: true }) + + handlePasswordConfirmation(admin.password) + + // Wait until we see the disable button for the app + cy.get('#apps-list').should('exist') + .contains('tr', 'Update notification') + .should('exist') + // I see the enable button for the app + .contains('button', 'Enable', { timeout: 10000 }) + + // Change to disabled apps view + cy.get('#app-category-disabled a').click({ force: true }) + cy.url().should('match', /settings\/apps\/disabled$/) + // I see that the "Update notification" app has been disabled + cy.get('#apps-list').contains('tr', 'Update notification') + }) + + it('Browse enabled apps', () => { + // When I open the "Active apps" section + cy.get('#app-category-enabled a') + .should('contain', 'Active apps') + .click({ force: true }) + // Then I see that the current section is "Active apps" + cy.url().should('match', /settings\/apps\/enabled$/) + cy.get('#app-category-enabled').find('.active').should('exist') + // I see that there are only enabled apps + cy.get('#apps-list') + .should('exist') + .find('tr button') + .each(($action) => { + cy.wrap($action).should('not.contain', 'Enable') + }) + }) + + it('Browse disabled apps', () => { + // When I open the "Active apps" section + cy.get('#app-category-disabled a') + .should('contain', 'Disabled apps') + .click({ force: true }) + // Then I see that the current section is "Active apps" + cy.url().should('match', /settings\/apps\/disabled$/) + cy.get('#app-category-disabled').find('.active').should('exist') + // I see that there are only disabled apps + cy.get('#apps-list') + .should('exist') + .find('tr button') + .each(($action) => { + cy.wrap($action).should('not.contain', 'Disable') + }) + }) + + it('Browse app bundles', () => { + // When I open the "App bundles" section + cy.get('#app-category-your-bundles a') + .should('contain', 'App bundles') + .click({ force: true }) + // Then I see that the current section is "App bundles" + cy.url().should('match', /settings\/apps\/app-bundles$/) + cy.get('#app-category-your-bundles').find('.active').should('exist') + // I see the app bundles + cy.get('#apps-list').contains('tr', 'Enterprise bundle') + cy.get('#apps-list').contains('tr', 'Education bundle') + // I see that the "Enterprise bundle" is disabled + cy.get('#apps-list').contains('tr', 'Enterprise bundle').contains('button', 'Download and enable all') + }) + + it('View app details', () => { + // When I click on the "QA testing" app + cy.get('#apps-list').contains('a', 'QA testing').click({ force: true }) + // I see that the app details are shown + cy.get('#app-sidebar-vue') + .should('be.visible') + .find('.app-sidebar-header__info') + .should('contain', 'QA testing') + cy.get('#app-sidebar-vue').contains('a', 'View in store').should('exist') + cy.get('#app-sidebar-vue').find('input[type="button"][value="Enable"]').should('be.visible') + cy.get('#app-sidebar-vue').find('input[type="button"][value="Remove"]').should('be.visible') + cy.get('#app-sidebar-vue').contains(/Version \d+\.\d+\.\d+/).should('be.visible') + }) + + /* + * TODO: Improve testing with app store as external API + * The following scenarios require the files_antivirus and calendar app + * being present in the app store with support for the current server version + * Ideally we would have either a dummy app store endpoint with some test apps + * or even an app store instance running somewhere to properly test this. + * This is also a requirement to properly test updates of apps + */ + // TODO: View app details for app store apps + // TODO: Install an app from the app store + // TODO: Show section from app store +}) diff --git a/cypress/e2e/settings/personal-info.cy.ts b/cypress/e2e/settings/personal-info.cy.ts new file mode 100644 index 00000000000..8d4b4bb606a --- /dev/null +++ b/cypress/e2e/settings/personal-info.cy.ts @@ -0,0 +1,448 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { User } from '@nextcloud/cypress' +import { handlePasswordConfirmation } from './usersUtils.ts' + +let user: User + +enum Visibility { + Private = 'Private', + Local = 'Local', + Federated = 'Federated', + Public = 'Published' +} + +const ALL_VISIBILITIES = [Visibility.Public, Visibility.Private, Visibility.Local, Visibility.Federated] + +/** + * Get the input connected to a specific label + * @param label The content of the label + */ +const inputForLabel = (label: string) => cy.contains('label', label).then((el) => cy.get(`#${el.attr('for')}`)) + +/** + * Get the property visibility button + * @param property The property to which to look for the button + */ +const getVisibilityButton = (property: string) => cy.get(`button[aria-label*="Change scope level of ${property.toLowerCase()}"`) + +/** + * Validate a specifiy visibility is set for a property + * @param property The property + * @param active The active visibility + */ +const validateActiveVisibility = (property: string, active: Visibility) => { + getVisibilityButton(property) + .should('have.attr', 'aria-label') + .and('match', new RegExp(`current scope is ${active}`, 'i')) + getVisibilityButton(property) + .click() + cy.get('ul[role="menu"]') + .contains('button', active) + .should('have.attr', 'aria-checked', 'true') + + // close menu + getVisibilityButton(property) + .click() +} + +/** + * Set a specific visibility for a property + * @param property The property + * @param active The visibility to set + */ +const setActiveVisibility = (property: string, active: Visibility) => { + getVisibilityButton(property) + .click() + cy.get('ul[role="menu"]') + .contains('button', active) + .click({ force: true }) + handlePasswordConfirmation(user.password) + + cy.wait('@submitSetting') +} + +/** + * Helper to check that setting all visibilities on a property is possible + * @param property The property to test + * @param defaultVisibility The default visibility of that property + * @param allowedVisibility Visibility that is allowed and need to be checked + */ +const checkSettingsVisibility = (property: string, defaultVisibility: Visibility = Visibility.Local, allowedVisibility: Visibility[] = ALL_VISIBILITIES) => { + getVisibilityButton(property) + .scrollIntoView() + + validateActiveVisibility(property, defaultVisibility) + + allowedVisibility.forEach((active) => { + setActiveVisibility(property, active) + + cy.reload() + getVisibilityButton(property).scrollIntoView() + + validateActiveVisibility(property, active) + }) + + // TODO: Fix this in vue library then enable this test again + /* // Test that not allowed options are disabled + ALL_VISIBILITIES.filter((v) => !allowedVisibility.includes(v)).forEach((disabled) => { + getVisibilityButton(property) + .click() + cy.get('ul[role="dialog"') + .contains('button', disabled) + .should('exist') + .and('have.attr', 'disabled', 'true') + }) */ +} + +const genericProperties = [ + ['Location', 'Berlin'], + ['X (formerly Twitter)', 'nextclouders'], + ['Fediverse', 'nextcloud@mastodon.xyz'], +] +const nonfederatedProperties = ['Organisation', 'Role', 'Headline', 'About'] + +describe('Settings: Change personal information', { testIsolation: true }, () => { + let snapshot: string = '' + + before(() => { + // make sure the fediverse check does not do http requests + cy.runOccCommand('config:system:set has_internet_connection --type bool --value false') + // ensure we can set locale and language + cy.runOccCommand('config:system:delete force_language') + cy.runOccCommand('config:system:delete force_locale') + cy.createRandomUser().then(($user) => { + user = $user + cy.modifyUser(user, 'language', 'en') + cy.modifyUser(user, 'locale', 'en_US') + + // Make sure the user is logged in at least once + // before the snapshot is taken to speed up the tests + cy.login(user) + cy.visit('/settings/user') + + cy.saveState().then(($snapshot) => { + snapshot = $snapshot + }) + }) + }) + + after(() => { + cy.runOccCommand('config:system:delete has_internet_connection') + + cy.runOccCommand('config:system:set force_language --value en') + cy.runOccCommand('config:system:set force_locale --value en_US') + }) + + beforeEach(() => { + cy.login(user) + cy.visit('/settings/user') + cy.intercept('PUT', /ocs\/v2.php\/cloud\/users\//).as('submitSetting') + }) + + afterEach(() => { + cy.restoreState(snapshot) + }) + + it('Can dis- and enable the profile', () => { + cy.visit(`/u/${user.userId}`) + cy.contains('h2', user.userId).should('be.visible') + + cy.visit('/settings/user') + cy.contains('Enable profile').click() + handlePasswordConfirmation(user.password) + cy.wait('@submitSetting') + + cy.visit(`/u/${user.userId}`, { failOnStatusCode: false }) + cy.contains('h2', 'Profile not found').should('be.visible') + + cy.visit('/settings/user') + cy.contains('Enable profile').click() + handlePasswordConfirmation(user.password) + cy.wait('@submitSetting') + + cy.visit(`/u/${user.userId}`, { failOnStatusCode: false }) + cy.contains('h2', user.userId).should('be.visible') + }) + + it('Can change language', () => { + cy.intercept('GET', /settings\/user/).as('reload') + inputForLabel('Language').scrollIntoView() + inputForLabel('Language').type('Ned') + cy.contains('li[role="option"]', 'Nederlands') + .click() + cy.wait('@reload') + + // expect language changed + inputForLabel('Taal').scrollIntoView() + cy.contains('section', 'Help met vertalen') + }) + + it('Can change locale', () => { + cy.intercept('GET', /settings\/user/).as('reload') + cy.clock(new Date(2024, 0, 10)) + + // Default is US + cy.contains('section', '01/10/2024') + + inputForLabel('Locale').scrollIntoView() + inputForLabel('Locale').type('German') + cy.contains('li[role="option"]', 'German (Germany') + .click() + cy.wait('@reload') + + // expect locale changed + inputForLabel('Locale').scrollIntoView() + cy.contains('section', '10.01.2024') + }) + + it('Can set primary email and change its visibility', () => { + cy.contains('label', 'Email').scrollIntoView() + // Check invalid input + inputForLabel('Email').type('foo bar') + inputForLabel('Email').then(($el) => expect(($el.get(0) as HTMLInputElement).checkValidity()).to.be.false) + // handle valid input + inputForLabel('Email').type('{selectAll}hello@example.com') + handlePasswordConfirmation(user.password) + + cy.wait('@submitSetting') + cy.reload() + inputForLabel('Email').should('have.value', 'hello@example.com') + + checkSettingsVisibility( + 'Email', + Visibility.Federated, + // It is not possible to set it as private + ALL_VISIBILITIES.filter((v) => v !== Visibility.Private), + ) + + // check it is visible on the profile + cy.visit(`/u/${user.userId}`) + cy.contains('a', 'hello@example.com').should('be.visible').and('have.attr', 'href', 'mailto:hello@example.com') + }) + + it('Can delete primary email', () => { + cy.contains('label', 'Email').scrollIntoView() + inputForLabel('Email').type('{selectAll}hello@example.com') + handlePasswordConfirmation(user.password) + cy.wait('@submitSetting') + + // check after reload + cy.reload() + inputForLabel('Email').should('have.value', 'hello@example.com') + + // delete email + cy.get('button[aria-label="Remove primary email"]').click({ force: true }) + cy.wait('@submitSetting') + + // check after reload + cy.reload() + inputForLabel('Email').should('have.value', '') + }) + + it('Can set and delete additional emails', () => { + cy.get('button[aria-label="Add additional email"]').should('be.disabled') + // we need a primary email first + cy.contains('label', 'Email').scrollIntoView() + inputForLabel('Email').type('{selectAll}primary@example.com') + handlePasswordConfirmation(user.password) + cy.wait('@submitSetting') + + // add new email + cy.get('button[aria-label="Add additional email"]') + .click() + + // without any value we should not be able to add a second additional + cy.get('button[aria-label="Add additional email"]').should('be.disabled') + + // fill the first additional + inputForLabel('Additional email address 1') + .type('1@example.com') + handlePasswordConfirmation(user.password) + cy.wait('@submitSetting') + + // add second additional email + cy.get('button[aria-label="Add additional email"]') + .click() + + // fill the second additional + inputForLabel('Additional email address 2') + .type('2@example.com') + handlePasswordConfirmation(user.password) + cy.wait('@submitSetting') + + // check the content is saved + cy.reload() + inputForLabel('Additional email address 1') + .should('have.value', '1@example.com') + inputForLabel('Additional email address 2') + .should('have.value', '2@example.com') + + // delete the first + cy.get('button[aria-label="Options for additional email address 1"]') + .click({ force: true }) + cy.contains('button[role="menuitem"]', 'Delete email') + .click({ force: true }) + handlePasswordConfirmation(user.password) + + cy.reload() + inputForLabel('Additional email address 1') + .should('have.value', '2@example.com') + }) + + it('Can set Full name and change its visibility', () => { + cy.contains('label', 'Full name').scrollIntoView() + // handle valid input + inputForLabel('Full name').type('{selectAll}Jane Doe') + handlePasswordConfirmation(user.password) + + cy.wait('@submitSetting') + cy.reload() + inputForLabel('Full name').should('have.value', 'Jane Doe') + + checkSettingsVisibility( + 'Full name', + Visibility.Federated, + // It is not possible to set it as private + ALL_VISIBILITIES.filter((v) => v !== Visibility.Private), + ) + + // check it is visible on the profile + cy.visit(`/u/${user.userId}`) + cy.contains('h2', 'Jane Doe').should('be.visible') + }) + + it('Can set Phone number and its visibility', () => { + cy.contains('label', 'Phone number').scrollIntoView() + // Check invalid input + inputForLabel('Phone number').type('foo bar') + inputForLabel('Phone number').should('have.attr', 'class').and('contain', '--error') + // handle valid input + inputForLabel('Phone number').type('{selectAll}+49 89 721010 99701') + inputForLabel('Phone number').should('have.attr', 'class').and('not.contain', '--error') + handlePasswordConfirmation(user.password) + + cy.wait('@submitSetting') + cy.reload() + inputForLabel('Phone number').should('have.value', '+498972101099701') + + checkSettingsVisibility('Phone number') + + // check it is visible on the profile + cy.visit(`/u/${user.userId}`) + cy.get('a[href="tel:+498972101099701"]').should('be.visible') + }) + + it('Can set phone number with phone region', () => { + cy.contains('label', 'Phone number').scrollIntoView() + inputForLabel('Phone number').type('{selectAll}0 40 428990') + inputForLabel('Phone number').should('have.attr', 'class').and('contain', '--error') + + cy.runOccCommand('config:system:set default_phone_region --value DE') + cy.reload() + + cy.contains('label', 'Phone number').scrollIntoView() + inputForLabel('Phone number').type('{selectAll}0 40 428990') + handlePasswordConfirmation(user.password) + + cy.wait('@submitSetting') + cy.reload() + inputForLabel('Phone number').should('have.value', '+4940428990') + }) + + it('Can reset phone number', () => { + cy.contains('label', 'Phone number').scrollIntoView() + inputForLabel('Phone number').type('{selectAll}+49 40 428990') + handlePasswordConfirmation(user.password) + + cy.wait('@submitSetting') + cy.reload() + inputForLabel('Phone number').should('have.value', '+4940428990') + + inputForLabel('Phone number').clear() + handlePasswordConfirmation(user.password) + + cy.wait('@submitSetting') + cy.reload() + inputForLabel('Phone number').should('have.value', '') + }) + + it('Can reset social media property', () => { + cy.contains('label', 'Fediverse').scrollIntoView() + inputForLabel('Fediverse').type('{selectAll}@nextcloud@mastodon.social') + handlePasswordConfirmation(user.password) + + cy.wait('@submitSetting') + cy.reload() + inputForLabel('Fediverse').should('have.value', 'nextcloud@mastodon.social') + + inputForLabel('Fediverse').clear() + handlePasswordConfirmation(user.password) + + cy.wait('@submitSetting') + cy.reload() + inputForLabel('Fediverse').should('have.value', '') + }) + + it('Can set Website and change its visibility', () => { + cy.contains('label', 'Website').scrollIntoView() + // Check invalid input + inputForLabel('Website').type('foo bar') + inputForLabel('Website').then(($el) => expect(($el.get(0) as HTMLInputElement).checkValidity()).to.be.false) + // handle valid input + inputForLabel('Website').type('{selectAll}http://example.com') + handlePasswordConfirmation(user.password) + + cy.wait('@submitSetting') + cy.reload() + inputForLabel('Website').should('have.value', 'http://example.com') + + checkSettingsVisibility('Website') + + // check it is visible on the profile + cy.visit(`/u/${user.userId}`) + cy.contains('http://example.com').should('be.visible') + }) + + // Check generic properties that allow any visibility and any value + genericProperties.forEach(([property, value]) => { + it(`Can set ${property} and change its visibility`, () => { + cy.contains('label', property).scrollIntoView() + inputForLabel(property).type(value) + handlePasswordConfirmation(user.password) + + cy.wait('@submitSetting') + cy.reload() + inputForLabel(property).should('have.value', value) + + checkSettingsVisibility(property) + + // check it is visible on the profile + cy.visit(`/u/${user.userId}`) + cy.contains(value).should('be.visible') + }) + }) + + // Check non federated properties - those where we need special configuration and only support local visibility + nonfederatedProperties.forEach((property) => { + it(`Can set ${property} and change its visibility`, () => { + const uniqueValue = `${property.toUpperCase()} ${property.toLowerCase()}` + cy.contains('label', property).scrollIntoView() + inputForLabel(property).type(uniqueValue) + handlePasswordConfirmation(user.password) + + cy.wait('@submitSetting') + cy.reload() + inputForLabel(property).should('have.value', uniqueValue) + + checkSettingsVisibility(property, Visibility.Local, [Visibility.Private, Visibility.Local]) + + // check it is visible on the profile + cy.visit(`/u/${user.userId}`) + cy.contains(uniqueValue).should('be.visible') + }) + }) +}) diff --git a/cypress/e2e/settings/users-group-admin.cy.ts b/cypress/e2e/settings/users-group-admin.cy.ts new file mode 100644 index 00000000000..5b5dcfd33a8 --- /dev/null +++ b/cypress/e2e/settings/users-group-admin.cy.ts @@ -0,0 +1,186 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +/// <reference types="cypress-if" /> +import { User } from '@nextcloud/cypress' +import { getUserListRow, handlePasswordConfirmation } from './usersUtils' +// eslint-disable-next-line n/no-extraneous-import +import randomString from 'crypto-random-string' + +const admin = new User('admin', 'admin') +const john = new User('john', '123456') + +/** + * Make a user subadmin of a group. + * + * @param user - The user to make subadmin + * @param group - The group the user should be subadmin of + */ +function makeSubAdmin(user: User, group: string): void { + cy.request({ + url: `${Cypress.config('baseUrl')!.replace('/index.php', '')}/ocs/v2.php/cloud/users/${user.userId}/subadmins`, + method: 'POST', + auth: { + user: admin.userId, + password: admin.userId, + }, + headers: { + 'OCS-ApiRequest': 'true', + }, + body: { + groupid: group, + }, + }) +} + +describe('Settings: Create accounts as a group admin', function() { + + let subadmin: User + let group: string + + beforeEach(() => { + group = randomString(7) + cy.deleteUser(john) + cy.createRandomUser().then((user) => { + subadmin = user + cy.runOccCommand(`group:add '${group}'`) + cy.runOccCommand(`group:adduser '${group}' '${subadmin.userId}'`) + makeSubAdmin(subadmin, group) + }) + }) + + it('Can create a user with prefilled single group', () => { + cy.login(subadmin) + // open the User settings + cy.visit('/settings/users') + + // open the New user modal + cy.get('button#new-user-button').click() + + cy.get('form[data-test="form"]').within(() => { + // see that the correct group is preselected + cy.contains('[data-test="groups"] .vs__selected', group).should('be.visible') + // see that the username is "" + cy.get('input[data-test="username"]').should('exist').and('have.value', '') + // set the username to john + cy.get('input[data-test="username"]').type(john.userId) + // see that the username is john + cy.get('input[data-test="username"]').should('have.value', john.userId) + // see that the password is "" + cy.get('input[type="password"]').should('exist').and('have.value', '') + // set the password to 123456 + cy.get('input[type="password"]').type(john.password) + // see that the password is 123456 + cy.get('input[type="password"]').should('have.value', john.password) + }) + + cy.get('form[data-test="form"]').parents('[role="dialog"]').within(() => { + // submit the new user form + cy.get('button[type="submit"]').click({ force: true }) + }) + + // Make sure no confirmation modal is shown + handlePasswordConfirmation(admin.password) + + // see that the created user is in the list + getUserListRow(john.userId) + // see that the list of users contains the user john + .contains(john.userId).should('exist') + }) + + it('Can create a new user when member of multiple groups', () => { + const group2 = randomString(7) + cy.runOccCommand(`group:add '${group2}'`) + cy.runOccCommand(`group:adduser '${group2}' '${subadmin.userId}'`) + makeSubAdmin(subadmin, group2) + + cy.login(subadmin) + // open the User settings + cy.visit('/settings/users') + + // open the New user modal + cy.get('button#new-user-button').click() + + cy.get('form[data-test="form"]').within(() => { + // see that no group is pre-selected + cy.get('[data-test="groups"] .vs__selected').should('not.exist') + // see both groups are available + cy.findByRole('combobox', { name: /member of the following groups/i }) + .should('be.visible') + .click() + // can select both groups + cy.document().its('body') + .findByRole('listbox', { name: 'Options' }) + .should('be.visible') + .as('options') + .findAllByRole('option') + .should('have.length', 2) + .get('@options') + .findByRole('option', { name: group }) + .should('be.visible') + .get('@options') + .findByRole('option', { name: group2 }) + .should('be.visible') + .click() + // see group is selected + cy.contains('[data-test="groups"] .vs__selected', group2).should('be.visible') + + // see that the username is "" + cy.get('input[data-test="username"]').should('exist').and('have.value', '') + // set the username to john + cy.get('input[data-test="username"]').type(john.userId) + // see that the username is john + cy.get('input[data-test="username"]').should('have.value', john.userId) + // see that the password is "" + cy.get('input[type="password"]').should('exist').and('have.value', '') + // set the password to 123456 + cy.get('input[type="password"]').type(john.password) + // see that the password is 123456 + cy.get('input[type="password"]').should('have.value', john.password) + }) + + cy.get('form[data-test="form"]').parents('[role="dialog"]').within(() => { + // submit the new user form + cy.get('button[type="submit"]').click({ force: true }) + }) + + // Make sure no confirmation modal is shown + handlePasswordConfirmation(admin.password) + + // see that the created user is in the list + getUserListRow(john.userId) + // see that the list of users contains the user john + .contains(john.userId).should('exist') + }) + + it('Only sees groups they are subadmin of', () => { + const group2 = randomString(7) + cy.runOccCommand(`group:add '${group2}'`) + cy.runOccCommand(`group:adduser '${group2}' '${subadmin.userId}'`) + // not a subadmin! + + cy.login(subadmin) + // open the User settings + cy.visit('/settings/users') + + // open the New user modal + cy.get('button#new-user-button').click() + + cy.get('form[data-test="form"]').within(() => { + // see that the subadmin group is pre-selected + cy.contains('[data-test="groups"] .vs__selected', group).should('be.visible') + // see only the subadmin group is available + cy.findByRole('combobox', { name: /member of the following groups/i }) + .should('be.visible') + .click() + // can select both groups + cy.document().its('body') + .findByRole('listbox', { name: 'Options' }) + .should('be.visible') + .as('options') + .findAllByRole('option') + .should('have.length', 1) + }) + }) +}) diff --git a/cypress/e2e/settings/users.cy.ts b/cypress/e2e/settings/users.cy.ts new file mode 100644 index 00000000000..5b8726e92ca --- /dev/null +++ b/cypress/e2e/settings/users.cy.ts @@ -0,0 +1,129 @@ +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +/// <reference types="cypress-if" /> +import { User } from '@nextcloud/cypress' +import { getUserListRow, handlePasswordConfirmation } from './usersUtils' + +const admin = new User('admin', 'admin') +const john = new User('john', '123456') + +describe('Settings: Create and delete accounts', function() { + beforeEach(function() { + cy.listUsers().then((users) => { + if ((users as string[]).includes(john.userId)) { + // ensure created user is deleted + cy.deleteUser(john) + } + }) + cy.login(admin) + // open the User settings + cy.visit('/settings/users') + }) + + it('Can create a user', function() { + // open the New user modal + cy.get('button#new-user-button').click() + + cy.get('form[data-test="form"]').within(() => { + // see that the username is "" + cy.get('input[data-test="username"]').should('exist').and('have.value', '') + // set the username to john + cy.get('input[data-test="username"]').type(john.userId) + // see that the username is john + cy.get('input[data-test="username"]').should('have.value', john.userId) + // see that the password is "" + cy.get('input[type="password"]').should('exist').and('have.value', '') + // set the password to 123456 + cy.get('input[type="password"]').type(john.password) + // see that the password is 123456 + cy.get('input[type="password"]').should('have.value', john.password) + }) + + cy.get('form[data-test="form"]').parents('[role="dialog"]').within(() => { + // submit the new user form + cy.get('button[type="submit"]').click({ force: true }) + }) + + // Make sure no confirmation modal is shown + handlePasswordConfirmation(admin.password) + + // see that the created user is in the list + getUserListRow(john.userId) + // see that the list of users contains the user john + .contains(john.userId).should('exist') + }) + + it('Can create a user with additional field data', function() { + // open the New user modal + cy.get('button#new-user-button').click() + + cy.get('form[data-test="form"]').within(() => { + // set the username + cy.get('input[data-test="username"]').should('exist').and('have.value', '') + cy.get('input[data-test="username"]').type(john.userId) + cy.get('input[data-test="username"]').should('have.value', john.userId) + // set the display name + cy.get('input[data-test="displayName"]').should('exist').and('have.value', '') + cy.get('input[data-test="displayName"]').type('John Smith') + cy.get('input[data-test="displayName"]').should('have.value', 'John Smith') + // set the email + cy.get('input[data-test="email"]').should('exist').and('have.value', '') + cy.get('input[data-test="email"]').type('john@example.org') + cy.get('input[data-test="email"]').should('have.value', 'john@example.org') + // set the password + cy.get('input[type="password"]').should('exist').and('have.value', '') + cy.get('input[type="password"]').type(john.password) + cy.get('input[type="password"]').should('have.value', john.password) + }) + + cy.get('form[data-test="form"]').parents('[role="dialog"]').within(() => { + // submit the new user form + cy.get('button[type="submit"]').click({ force: true }) + }) + + // Make sure no confirmation modal is shown + handlePasswordConfirmation(admin.password) + + // see that the created user is in the list + getUserListRow(john.userId) + // see that the list of users contains the user john + .contains(john.userId) + .should('exist') + }) + + it('Can delete a user', function() { + let testUser + // create user + cy.createRandomUser() + .then(($user) => { + testUser = $user + }) + cy.login(admin) + // ensure created user is present + cy.reload().then(() => { + // see that the user is in the list + getUserListRow(testUser.userId).within(() => { + // see that the list of users contains the user testUser + cy.contains(testUser.userId).should('exist') + // open the actions menu for the user + cy.get('[data-cy-user-list-cell-actions]') + .find('button.action-item__menutoggle') + .click({ force: true }) + }) + + // The "Delete account" action in the actions menu is shown and clicked + cy.get('.action-item__popper .action').contains('Delete account').should('exist').click({ force: true }) + + // Make sure no confirmation modal is shown + handlePasswordConfirmation(admin.password) + + // And confirmation dialog accepted + cy.get('.nc-generic-dialog button').contains(`Delete ${testUser.userId}`).click({ force: true }) + + // deleted clicked the user is not shown anymore + getUserListRow(testUser.userId).should('not.exist') + }) + }) +}) diff --git a/cypress/e2e/settings/usersUtils.ts b/cypress/e2e/settings/usersUtils.ts new file mode 100644 index 00000000000..7d8ea55d35b --- /dev/null +++ b/cypress/e2e/settings/usersUtils.ts @@ -0,0 +1,90 @@ +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { User } from '@nextcloud/cypress' + +/** + * Assert that `element` does not exist or is not visible + * Useful in cases such as when NcModal is opened/closed rapidly + * @param element Element that is inspected + */ +export function assertNotExistOrNotVisible(element: JQuery<HTMLElement>) { + const doesNotExist = element.length === 0 + const isNotVisible = !element.is(':visible') + + // eslint-disable-next-line no-unused-expressions + expect(doesNotExist || isNotVisible, 'does not exist or is not visible').to.be.true +} + +/** + * Get the settings users list + * @return Cypress chainable object + */ +export function getUserList() { + return cy.get('[data-cy-user-list]') +} + +/** + * Get the row entry for given userId within the settings users list + * + * @param userId the user to query + * @return Cypress chainable object + */ +export function getUserListRow(userId: string) { + return getUserList().find(`[data-cy-user-row="${userId}"]`) +} + +export function waitLoading(selector: string) { + // We need to make sure the element is loading, otherwise the "done loading" will succeed even if we did not start loading. + // But Cypress might also be simply too slow to catch the loading phase. Thats why we need to wait in this case. + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.get(`${selector}[data-loading]`).if().should('exist').else().wait(1000) + // https://github.com/NoriSte/cypress-wait-until/issues/75#issuecomment-572685623 + cy.waitUntil(() => Cypress.$(selector).length > 0 && !Cypress.$(selector).attr('data-loading')?.length, { timeout: 10000 }) +} + +/** + * Toggle the edit button of the user row + * @param user The user row to edit + * @param toEdit True if it should be switch to edit mode, false to switch to read-only + */ +export function toggleEditButton(user: User, toEdit = true) { + // see that the list of users contains the user + getUserListRow(user.userId).should('exist') + // toggle the edit mode for the user + .find('[data-cy-user-list-cell-actions]') + .find(`[data-cy-user-list-action-toggle-edit="${!toEdit}"]`) + .if() + .click({ force: true }) + .else() + // otherwise ensure the button is already in edit mode + .then(() => getUserListRow(user.userId) + .find(`[data-cy-user-list-action-toggle-edit="${toEdit}"]`) + .should('exist'), + ) +} + +/** + * Handle the confirm password dialog (if needed) + * @param adminPassword The admin password for the dialog + */ +export function handlePasswordConfirmation(adminPassword = 'admin') { + const handleModal = (context: Cypress.Chainable) => { + return context.contains('.modal-container', 'Confirm your password') + .if() + .within(() => { + cy.get('input[type="password"]').type(adminPassword) + cy.get('button').contains('Confirm').click() + }) + } + + return cy.get('body') + .if() + .then(() => handleModal(cy.get('body'))) + .else() + // Handle if inside a cy.within + .root().closest('body') + .then(($body) => handleModal(cy.wrap($body))) +} diff --git a/cypress/e2e/settings/users_columns.cy.ts b/cypress/e2e/settings/users_columns.cy.ts new file mode 100644 index 00000000000..0afbf14e773 --- /dev/null +++ b/cypress/e2e/settings/users_columns.cy.ts @@ -0,0 +1,94 @@ +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { User } from '@nextcloud/cypress' +import { assertNotExistOrNotVisible, getUserList } from './usersUtils.js' + +const admin = new User('admin', 'admin') + +describe('Settings: Show and hide columns', function() { + before(function() { + cy.login(admin) + // open the User settings + cy.visit('/settings/users') + }) + + beforeEach(function() { + // open the settings dialog + cy.contains('button', 'Account management settings').click() + // reset all visibility toggles + cy.get('.modal-container #settings-section_visibility-settings input[type="checkbox"]').uncheck({ force: true }) + + cy.contains('.modal-container', 'Account management settings').within(() => { + // enable the last login toggle + cy.get('[data-test="showLastLogin"] input[type="checkbox"]').check({ force: true }) + // close the settings dialog + cy.get('button.modal-container__close').click() + }) + cy.waitUntil(() => cy.get('.modal-container').should(el => assertNotExistOrNotVisible(el))) + }) + + it('Can show a column', function() { + // see that the language column is not in the header + cy.get('[data-cy-user-list-header-languages]').should('not.exist') + + // see that the language column is not in all user rows + cy.get('tbody.user-list__body tr').each(($row) => { + cy.wrap($row).get('[data-test="language"]').should('not.exist') + }) + + // open the settings dialog + cy.contains('button', 'Account management settings').click() + + cy.contains('.modal-container', 'Account management settings').within(() => { + // enable the language toggle + cy.get('[data-test="showLanguages"] input[type="checkbox"]').should('not.be.checked') + cy.get('[data-test="showLanguages"] input[type="checkbox"]').check({ force: true }) + cy.get('[data-test="showLanguages"] input[type="checkbox"]').should('be.checked') + // close the settings dialog + cy.get('button.modal-container__close').click() + }) + cy.waitUntil(() => cy.get('.modal-container').should(el => assertNotExistOrNotVisible(el))) + + // see that the language column is in the header + cy.get('[data-cy-user-list-header-languages]').should('exist') + + // see that the language column is in all user rows + getUserList().find('tbody tr').each(($row) => { + cy.wrap($row).get('[data-cy-user-list-cell-language]').should('exist') + }) + }) + + it('Can hide a column', function() { + // see that the last login column is in the header + cy.get('[data-cy-user-list-header-last-login]').should('exist') + + // see that the last login column is in all user rows + getUserList().find('tbody tr').each(($row) => { + cy.wrap($row).get('[data-cy-user-list-cell-last-login]').should('exist') + }) + + // open the settings dialog + cy.contains('button', 'Account management settings').click() + + cy.contains('.modal-container', 'Account management settings').within(() => { + // disable the last login toggle + cy.get('[data-test="showLastLogin"] input[type="checkbox"]').should('be.checked') + cy.get('[data-test="showLastLogin"] input[type="checkbox"]').uncheck({ force: true }) + cy.get('[data-test="showLastLogin"] input[type="checkbox"]').should('not.be.checked') + // close the settings dialog + cy.get('button.modal-container__close').click() + }) + cy.waitUntil(() => cy.contains('.modal-container', 'Account management settings').should(el => assertNotExistOrNotVisible(el))) + + // see that the last login column is not in the header + cy.get('[data-cy-user-list-header-last-login]').should('not.exist') + + // see that the last login column is not in all user rows + getUserList().find('tbody tr').each(($row) => { + cy.wrap($row).get('[data-cy-user-list-cell-last-login]').should('not.exist') + }) + }) +}) diff --git a/cypress/e2e/settings/users_disable.cy.ts b/cypress/e2e/settings/users_disable.cy.ts new file mode 100644 index 00000000000..6195d43e211 --- /dev/null +++ b/cypress/e2e/settings/users_disable.cy.ts @@ -0,0 +1,79 @@ +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { User } from '@nextcloud/cypress' +import { getUserListRow } from './usersUtils' +import { clearState } from '../../support/commonUtils' + +const admin = new User('admin', 'admin') + +describe('Settings: Disable and enable users', function() { + let testUser: User + + beforeEach(function() { + clearState() + cy.createRandomUser().then(($user) => { + testUser = $user + }) + cy.login(admin) + // open the User settings + cy.visit('/settings/users') + }) + + // Not guranteed to run but would be nice to cleanup + after(() => { + cy.deleteUser(testUser) + }) + + it('Can disable the user', function() { + // ensure user is enabled + cy.enableUser(testUser) + + // see that the user is in the list of active users + getUserListRow(testUser.userId).within(() => { + // see that the list of users contains the user testUser + cy.contains(testUser.userId).should('exist') + // open the actions menu for the user + cy.get('[data-cy-user-list-cell-actions] button.action-item__menutoggle').click({ scrollBehavior: 'center' }) + }) + + // The "Disable account" action in the actions menu is shown and clicked + cy.get('.action-item__popper .action').contains('Disable account').should('exist').click() + // When clicked the section is not shown anymore + getUserListRow(testUser.userId).should('not.exist') + // But the disabled user section now exists + cy.get('#disabled').should('exist') + // Open disabled users section + cy.get('#disabled a').click() + cy.url().should('match', /\/disabled/) + // The list of disabled users should now contain the user + getUserListRow(testUser.userId).should('exist') + }) + + it('Can enable the user', function() { + // ensure user is disabled + cy.enableUser(testUser, false).reload() + + // Open disabled users section + cy.get('#disabled a').click() + cy.url().should('match', /\/disabled/) + + // see that the user is in the list of active users + getUserListRow(testUser.userId).within(() => { + // see that the list of disabled users contains the user testUser + cy.contains(testUser.userId).should('exist') + // open the actions menu for the user + cy.get('[data-cy-user-list-cell-actions] button.action-item__menutoggle').click({ scrollBehavior: 'center' }) + }) + + // The "Enable account" action in the actions menu is shown and clicked + cy.get('.action-item__popper .action').contains('Enable account').should('exist').click() + // When clicked the section is not shown anymore + cy.get('#disabled').should('not.exist') + // Make sure it is still gone after the reload reload + cy.reload().login(admin) + cy.get('#disabled').should('not.exist') + }) +}) diff --git a/cypress/e2e/settings/users_groups.cy.ts b/cypress/e2e/settings/users_groups.cy.ts new file mode 100644 index 00000000000..8d84ddc6bb4 --- /dev/null +++ b/cypress/e2e/settings/users_groups.cy.ts @@ -0,0 +1,291 @@ +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { User } from '@nextcloud/cypress' +import { assertNotExistOrNotVisible, getUserListRow, handlePasswordConfirmation, toggleEditButton } from './usersUtils' + +// eslint-disable-next-line n/no-extraneous-import +import randomString from 'crypto-random-string' + +const admin = new User('admin', 'admin') + +describe('Settings: Create groups', () => { + before(() => { + cy.login(admin) + cy.visit('/settings/users') + }) + + it('Can create a group', () => { + const groupName = randomString(7) + // open the Create group menu + cy.get('button[aria-label="Create group"]').click() + + cy.get('li[data-cy-users-settings-new-group-name]').within(() => { + // see that the group name is "" + cy.get('input').should('exist').and('have.value', '') + // set the group name to foo + cy.get('input').type(groupName) + // see that the group name is foo + cy.get('input').should('have.value', groupName) + // submit the group name + cy.get('input ~ button').click() + }) + + // Make sure no confirmation modal is shown + handlePasswordConfirmation(admin.password) + + // see that the created group is in the list + cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => { + // see that the list of groups contains the group foo + cy.contains(groupName).should('exist') + }) + }) +}) + +describe('Settings: Assign user to a group', { testIsolation: false }, () => { + const groupName = randomString(7) + let testUser: User + + after(() => cy.deleteUser(testUser)) + before(() => { + cy.createRandomUser().then((user) => { + testUser = user + }) + cy.runOccCommand(`group:add '${groupName}'`) + cy.login(admin) + cy.intercept('GET', '**/ocs/v2.php/cloud/groups/details?search=&offset=*&limit=*').as('loadGroups') + cy.visit('/settings/users') + cy.wait('@loadGroups') + }) + + it('see that the group is in the list', () => { + cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName) + .should('exist') + cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName) + .find('.counter-bubble__counter') + .should('not.exist') // is hidden when 0 + }) + + it('see that the user is in the list', () => { + getUserListRow(testUser.userId) + .contains(testUser.userId) + .should('exist') + .scrollIntoView() + }) + + it('switch into user edit mode', () => { + toggleEditButton(testUser) + getUserListRow(testUser.userId) + .find('[data-cy-user-list-input-groups]') + .should('exist') + }) + + it('assign the group', () => { + // focus inside the input + getUserListRow(testUser.userId) + .find('[data-cy-user-list-input-groups] input') + .click({ force: true }) + // enter the group name + getUserListRow(testUser.userId) + .find('[data-cy-user-list-input-groups] input') + .type(`${groupName.slice(0, 5)}`) // only type part as otherwise we would create a new one with the same name + cy.contains('li.vs__dropdown-option', groupName) + .click({ force: true }) + + handlePasswordConfirmation(admin.password) + }) + + it('leave the user edit mode', () => { + toggleEditButton(testUser, false) + }) + + it('see the group was successfully assigned', () => { + // see a new memeber + cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName) + .find('.counter-bubble__counter') + .should('contain', '1') + }) + + it('validate the user was added on backend', () => { + cy.runOccCommand(`user:info --output=json '${testUser.userId}'`).then((output) => { + cy.wrap(output.code).should('eq', 0) + cy.wrap(JSON.parse(output.stdout)?.groups).should('include', groupName) + }) + }) +}) + +describe('Settings: Delete an empty group', { testIsolation: false }, () => { + const groupName = randomString(7) + + before(() => { + cy.runOccCommand(`group:add '${groupName}'`) + cy.login(admin) + cy.intercept('GET', '**/ocs/v2.php/cloud/groups/details?search=&offset=*&limit=*').as('loadGroups') + cy.visit('/settings/users') + cy.wait('@loadGroups') + }) + + it('see that the group is in the list', () => { + // see that the list of groups contains the group foo + cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName) + .should('exist') + .scrollIntoView() + // open the actions menu for the group + cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName) + .find('button.action-item__menutoggle') + .click({ force: true }) + }) + + it('can delete the group', () => { + // The "Delete group" action in the actions menu is shown and clicked + cy.get('.action-item__popper button').contains('Delete group').should('exist').click({ force: true }) + // And confirmation dialog accepted + cy.get('.modal-container button').contains('Confirm').click({ force: true }) + + // Make sure no confirmation modal is shown + handlePasswordConfirmation(admin.password) + }) + + it('deleted group is not shown anymore', () => { + // see that the list of groups does not contain the group + cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName) + .should('not.exist') + // and also not in database + cy.runOccCommand('group:list --output=json').then(($response) => { + const groups: string[] = Object.keys(JSON.parse($response.stdout)) + expect(groups).to.not.include(groupName) + }) + }) +}) + +describe('Settings: Delete a non empty group', () => { + let testUser: User + const groupName = randomString(7) + + before(() => { + cy.runOccCommand(`group:add '${groupName}'`) + cy.createRandomUser().then(($user) => { + testUser = $user + cy.runOccCommand(`group:addUser '${groupName}' '${$user.userId}'`) + }) + cy.login(admin) + cy.intercept('GET', '**/ocs/v2.php/cloud/groups/details?search=&offset=*&limit=*').as('loadGroups') + cy.visit('/settings/users') + cy.wait('@loadGroups') + }) + after(() => cy.deleteUser(testUser)) + + it('see that the group is in the list', () => { + // see that the list of groups contains the group + cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName) + .should('exist') + .scrollIntoView() + }) + + it('can delete the group', () => { + // open the menu + cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName) + .find('button.action-item__menutoggle') + .click({ force: true }) + + // The "Delete group" action in the actions menu is shown and clicked + cy.get('.action-item__popper button').contains('Delete group').should('exist').click({ force: true }) + // And confirmation dialog accepted + cy.get('.modal-container button').contains('Confirm').click({ force: true }) + + // Make sure no confirmation modal is shown + handlePasswordConfirmation(admin.password) + }) + + it('deleted group is not shown anymore', () => { + // see that the list of groups does not contain the group foo + cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName) + .should('not.exist') + // and also not in database + cy.runOccCommand('group:list --output=json').then(($response) => { + const groups: string[] = Object.keys(JSON.parse($response.stdout)) + expect(groups).to.not.include(groupName) + }) + }) +}) + +describe('Settings: Sort groups in the UI', () => { + before(() => { + // Clear state + cy.runOccCommand('group:list --output json').then((output) => { + const groups = Object.keys(JSON.parse(output.stdout)).filter((group) => group !== 'admin') + groups.forEach((group) => { + cy.runOccCommand(`group:delete '${group}'`) + }) + }) + + // Add two groups and add one user to group B + cy.runOccCommand('group:add A') + cy.runOccCommand('group:add B') + cy.createRandomUser().then((user) => { + cy.runOccCommand(`group:adduser B '${user.userId}'`) + }) + + // Visit the settings as admin + cy.login(admin) + cy.visit('/settings/users') + }) + + it('Can set sort by member count', () => { + // open the settings dialog + cy.contains('button', 'Account management settings').click() + + cy.contains('.modal-container', 'Account management settings').within(() => { + cy.get('[data-test="sortGroupsByMemberCount"] input[type="radio"]').scrollIntoView() + cy.get('[data-test="sortGroupsByMemberCount"] input[type="radio"]').check({ force: true }) + // close the settings dialog + cy.get('button.modal-container__close').click() + }) + cy.waitUntil(() => cy.get('.modal-container').should(el => assertNotExistOrNotVisible(el))) + }) + + it('See that the groups are sorted by the member count', () => { + cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => { + cy.get('li').eq(0).should('contain', 'B') // 1 member + cy.get('li').eq(1).should('contain', 'A') // 0 members + }) + }) + + it('See that the order is preserved after a reload', () => { + cy.reload() + cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => { + cy.get('li').eq(0).should('contain', 'B') // 1 member + cy.get('li').eq(1).should('contain', 'A') // 0 members + }) + }) + + it('Can set sort by group name', () => { + // open the settings dialog + cy.contains('button', 'Account management settings').click() + + cy.contains('.modal-container', 'Account management settings').within(() => { + cy.get('[data-test="sortGroupsByName"] input[type="radio"]').scrollIntoView() + cy.get('[data-test="sortGroupsByName"] input[type="radio"]').check({ force: true }) + // close the settings dialog + cy.get('button.modal-container__close').click() + }) + cy.waitUntil(() => cy.get('.modal-container').should(el => assertNotExistOrNotVisible(el))) + }) + + it('See that the groups are sorted by the user count', () => { + cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => { + cy.get('li').eq(0).should('contain', 'A') + cy.get('li').eq(1).should('contain', 'B') + }) + }) + + it('See that the order is preserved after a reload', () => { + cy.reload() + cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => { + cy.get('li').eq(0).should('contain', 'A') + cy.get('li').eq(1).should('contain', 'B') + }) + }) +}) diff --git a/cypress/e2e/settings/users_manager.cy.ts b/cypress/e2e/settings/users_manager.cy.ts new file mode 100644 index 00000000000..b7596ddf0ce --- /dev/null +++ b/cypress/e2e/settings/users_manager.cy.ts @@ -0,0 +1,121 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { User } from '@nextcloud/cypress' +import { getUserListRow, handlePasswordConfirmation, toggleEditButton, waitLoading } from './usersUtils' +import { clearState } from '../../support/commonUtils' + +const admin = new User('admin', 'admin') + +describe('Settings: User Manager Management', function() { + let user: User + let manager: User + + beforeEach(function() { + clearState() + cy.createRandomUser().then(($user) => { + manager = $user + return cy.createRandomUser() + }).then(($user) => { + user = $user + cy.login(admin) + cy.intercept('PUT', `/ocs/v2.php/cloud/users/${user.userId}*`).as('updateUser') + }) + }) + + it('Can assign and remove a manager through the UI', function() { + cy.visit('/settings/users') + + toggleEditButton(user, true) + + // Scroll to manager cell and wait for it to be visible + getUserListRow(user.userId) + .find('[data-cy-user-list-cell-manager]') + .scrollIntoView() + .should('be.visible') + + // Assign a manager + getUserListRow(user.userId).find('[data-cy-user-list-cell-manager]').within(() => { + // Verify no manager is set initially + cy.get('.vs__selected').should('not.exist') + + // Open the dropdown menu + cy.get('[role="combobox"]').click({ force: true }) + + // Wait for the dropdown to be visible and initialized + waitLoading('[data-cy-user-list-input-manager]') + + // Type the manager's username to search + cy.get('input[type="search"]').type(manager.userId, { force: true }) + + // Wait for the search results to load + waitLoading('[data-cy-user-list-input-manager]') + }) + + // Now select the manager from the filtered results + // Since the dropdown is floating, we need to search globally + cy.get('.vs__dropdown-menu').find('li').contains('span', manager.userId).should('be.visible').click({ force: true }) + + // Handle password confirmation if needed + handlePasswordConfirmation(admin.password) + + // Verify the manager is selected in the UI + cy.get('.vs__selected').should('exist').and('contain.text', manager.userId) + + // Verify the PUT request was made to set the manager + cy.wait('@updateUser').then((interception) => { + // Verify the request URL and body + expect(interception.request.url).to.match(/\/cloud\/users\/.+/) + expect(interception.request.body).to.deep.equal({ + key: 'manager', + value: manager.userId + }) + expect(interception.response?.statusCode).to.equal(200) + }) + + // Wait for the save to complete + waitLoading('[data-cy-user-list-input-manager]') + + // Verify the manager is set in the backend + cy.getUserData(user).then(($result) => { + expect($result.body).to.contain(`<manager>${manager.userId}</manager>`) + }) + + // Now remove the manager + getUserListRow(user.userId).find('[data-cy-user-list-cell-manager]').within(() => { + // Clear the manager selection + cy.get('.vs__clear').click({ force: true }) + + // Verify the manager is cleared in the UI + cy.get('.vs__selected').should('not.exist') + + // Handle password confirmation if needed + handlePasswordConfirmation(admin.password) + }) + + // Verify the PUT request was made to clear the manager + cy.wait('@updateUser').then((interception) => { + // Verify the request URL and body + expect(interception.request.url).to.match(/\/cloud\/users\/.+/) + expect(interception.request.body).to.deep.equal({ + key: 'manager', + value: '', + }) + expect(interception.response?.statusCode).to.equal(200) + }) + + // Wait for the save to complete + waitLoading('[data-cy-user-list-input-manager]') + + // Verify the manager is cleared in the backend + cy.getUserData(user).then(($result) => { + expect($result.body).to.not.contain(`<manager>${manager.userId}</manager>`) + expect($result.body).to.contain('<manager></manager>') + }) + + // Finish editing the user + toggleEditButton(user, false) + }) +}) diff --git a/cypress/e2e/settings/users_modify.cy.ts b/cypress/e2e/settings/users_modify.cy.ts new file mode 100644 index 00000000000..749bded2e94 --- /dev/null +++ b/cypress/e2e/settings/users_modify.cy.ts @@ -0,0 +1,225 @@ +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { User } from '@nextcloud/cypress' +import { getUserListRow, handlePasswordConfirmation, toggleEditButton, waitLoading } from './usersUtils' +import { clearState } from '../../support/commonUtils' + +const admin = new User('admin', 'admin') + +describe('Settings: Change user properties', function() { + let user: User + + beforeEach(function() { + clearState() + cy.createRandomUser().then(($user) => { user = $user }) + cy.login(admin) + }) + + it('Can change the display name', function() { + // open the User settings as admin + cy.visit('/settings/users') + + // toggle edit button into edit mode + toggleEditButton(user, true) + + getUserListRow(user.userId).within(() => { + // set the display name + cy.get('[data-cy-user-list-input-displayname]').should('exist').and('have.value', user.userId) + cy.get('[data-cy-user-list-input-displayname]').clear() + cy.get('[data-cy-user-list-input-displayname]').type('John Doe') + cy.get('[data-cy-user-list-input-displayname]').should('have.value', 'John Doe') + cy.get('[data-cy-user-list-input-displayname] ~ button').click() + + // Make sure no confirmation modal is shown + handlePasswordConfirmation(admin.password) + + // see that the display name cell is done loading + waitLoading('[data-cy-user-list-input-displayname]') + }) + + // Success message is shown + cy.get('.toastify.toast-success').contains(/Display.+name.+was.+successfully.+changed/i).should('exist') + }) + + it('Can change the password', function() { + // open the User settings as admin + cy.visit('/settings/users') + + // toggle edit button into edit mode + toggleEditButton(user, true) + + getUserListRow(user.userId).within(() => { + // see that the password of user is "" + cy.get('[data-cy-user-list-input-password]').should('exist').and('have.value', '') + // set the password for user to 123456 + cy.get('[data-cy-user-list-input-password]').type('123456') + // When I set the password for user to 123456 + cy.get('[data-cy-user-list-input-password]').should('have.value', '123456') + cy.get('[data-cy-user-list-input-password] ~ button').click() + + // Make sure no confirmation modal is shown + handlePasswordConfirmation(admin.password) + + // see that the password cell for user is done loading + waitLoading('[data-cy-user-list-input-password]') + // password input is emptied on change + cy.get('[data-cy-user-list-input-password]').should('have.value', '') + }) + + // Success message is shown + cy.get('.toastify.toast-success').contains(/Password.+successfully.+changed/i).should('exist') + }) + + it('Can change the email address', function() { + // open the User settings as admin + cy.visit('/settings/users') + + // toggle edit button into edit mode + toggleEditButton(user, true) + + getUserListRow(user.userId).find('[data-cy-user-list-cell-email]').within(() => { + // see that the email of user is "" + cy.get('input').should('exist').and('have.value', '') + // set the email for user to mymail@example.com + cy.get('input').type('mymail@example.com') + // When I set the password for user to mymail@example.com + cy.get('input').should('have.value', 'mymail@example.com') + cy.get('input ~ button').click() + + // Make sure no confirmation modal is shown + handlePasswordConfirmation(admin.password) + + // see that the password cell for user is done loading + waitLoading('[data-cy-user-list-input-email]') + }) + + // Success message is shown + cy.get('.toastify.toast-success').contains(/Email.+successfully.+changed/i).should('exist') + }) + + it('Can change the user quota to a predefined one', function() { + // open the User settings as admin + cy.visit('/settings/users') + + // toggle edit button into edit mode + toggleEditButton(user, true) + + getUserListRow(user.userId).find('[data-cy-user-list-cell-quota]').scrollIntoView() + getUserListRow(user.userId).find('[data-cy-user-list-cell-quota] [data-cy-user-list-input-quota]').within(() => { + // see that the quota of user is unlimited + cy.get('.vs__selected').should('exist').and('contain.text', 'Unlimited') + // Open the quota selector + cy.get('[role="combobox"]').click({ force: true }) + // see that there are default options for the quota + cy.get('li').then(($options) => { + expect($options).to.have.length(5) + cy.wrap($options).contains('Default quota') + cy.wrap($options).contains('Unlimited') + cy.wrap($options).contains('1 GB') + cy.wrap($options).contains('10 GB') + // select 5 GB + cy.wrap($options).contains('5 GB').click({ force: true }) + + // Make sure no confirmation modal is shown + handlePasswordConfirmation(admin.password) + }) + // see that the quota of user is 5 GB + cy.get('.vs__selected').should('exist').and('contain.text', '5 GB') + }) + + // see that the changes are loading + waitLoading('[data-cy-user-list-input-quota]') + + // finish editing the user + toggleEditButton(user, false) + + // I see that the quota was set on the backend + cy.runOccCommand(`user:info --output=json '${user.userId}'`).then(($result) => { + expect($result.code).to.equal(0) + const info = JSON.parse($result.stdout) + expect(info?.quota).to.equal('5 GB') + }) + }) + + it('Can change the user quota to a custom value', function() { + // open the User settings as admin + cy.visit('/settings/users') + + // toggle edit button into edit mode + toggleEditButton(user, true) + + getUserListRow(user.userId).find('[data-cy-user-list-cell-quota]').scrollIntoView() + getUserListRow(user.userId).find('[data-cy-user-list-cell-quota]').within(() => { + // see that the quota of user is unlimited + cy.get('.vs__selected').should('exist').and('contain.text', 'Unlimited') + // set the quota to 4 MB + cy.get('[data-cy-user-list-input-quota] input').type('4 MB{enter}') + + // Make sure no confirmation modal is shown + handlePasswordConfirmation(admin.password) + + // see that the quota of user is 4 MB + // TODO: Enable this after the file size handling is fixed + // cy.get('.vs__selected').should('exist').and('contain.text', '4 MB') + + // see that the changes are loading + waitLoading('[data-cy-user-list-input-quota]') + }) + + // finish editing the user + toggleEditButton(user, false) + + // I see that the quota was set on the backend + cy.runOccCommand(`user:info --output=json '${user.userId}'`).then(($result) => { + expect($result.code).to.equal(0) + // TODO: Enable this after the file size handling is fixed!!!!!! + // const info = JSON.parse($result.stdout) + // expect(info?.quota).to.equal('4 MB') + }) + }) + + it('Can make user a subadmin of a group', function() { + // create a group + const groupName = 'userstestgroup' + cy.runOccCommand(`group:add '${groupName}'`) + + // open the User settings as admin + cy.visit('/settings/users') + + // toggle edit button into edit mode + toggleEditButton(user, true) + + getUserListRow(user.userId).find('[data-cy-user-list-cell-subadmins]').scrollIntoView() + getUserListRow(user.userId).find('[data-cy-user-list-cell-subadmins]').within(() => { + // see that the user is no subadmin + cy.get('.vs__selected').should('not.exist') + // Open the dropdown menu + cy.get('[role="combobox"]').click({ force: true }) + // Search for the group + cy.get('[role="combobox"]').type('userstestgroup') + // select the group + cy.contains('li', groupName).click({ force: true }) + + // handle password confirmation on time out + handlePasswordConfirmation(admin.password) + + // see that the user is subadmin of the group + cy.get('.vs__selected').should('exist').and('contain.text', groupName) + }) + + waitLoading('[data-cy-user-list-input-subadmins]') + + // finish editing the user + toggleEditButton(user, false) + + // I see that the quota was set on the backend + cy.getUserData(user).then(($response) => { + expect($response.status).to.equal(200) + const dom = (new DOMParser()).parseFromString($response.body, 'text/xml') + expect(dom.querySelector('subadmin element')?.textContent).to.contain(groupName) + }) + }) +}) |