diff --git a/packages/manager/.changeset/pr-10202-tests-1708119481596.md b/packages/manager/.changeset/pr-10202-tests-1708119481596.md new file mode 100644 index 00000000000..3017a0cdc53 --- /dev/null +++ b/packages/manager/.changeset/pr-10202-tests-1708119481596.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Improve User Profile integration test coverage and separate from Display Settings coverage ([#10202](https://github.com/linode/manager/pull/10202)) diff --git a/packages/manager/.changeset/pr-10202-upcoming-features-1708119409383.md b/packages/manager/.changeset/pr-10202-upcoming-features-1708119409383.md new file mode 100644 index 00000000000..1d906c12174 --- /dev/null +++ b/packages/manager/.changeset/pr-10202-upcoming-features-1708119409383.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Disable ability to edit or delete a proxy user via User Profile page ([#10202](https://github.com/linode/manager/pull/10202)) diff --git a/packages/manager/cypress/e2e/core/account/change-username.spec.ts b/packages/manager/cypress/e2e/core/account/display-settings.spec.ts similarity index 73% rename from packages/manager/cypress/e2e/core/account/change-username.spec.ts rename to packages/manager/cypress/e2e/core/account/display-settings.spec.ts index a0374fc698f..a08f8e04b94 100644 --- a/packages/manager/cypress/e2e/core/account/change-username.spec.ts +++ b/packages/manager/cypress/e2e/core/account/display-settings.spec.ts @@ -8,10 +8,7 @@ import { makeFeatureFlagData } from 'support/util/feature-flags'; import { mockGetProfile } from 'support/intercepts/profile'; import { getProfile } from 'support/api/account'; import { interceptGetProfile } from 'support/intercepts/profile'; -import { - interceptGetUser, - mockUpdateUsername, -} from 'support/intercepts/account'; +import { mockUpdateUsername } from 'support/intercepts/account'; import { ui } from 'support/ui'; import { randomString } from 'support/util/random'; @@ -59,54 +56,7 @@ const verifyUsernameAndEmail = ( } }; -describe('username', () => { - /* - * - Validates username update flow via the user profile page using mocked data. - */ - it('can change username via user profile page', () => { - const newUsername = randomString(12); - - getProfile().then((profile) => { - const username = profile.body.username; - - interceptGetUser(username).as('getUser'); - mockUpdateUsername(username, newUsername).as('updateUsername'); - - cy.visitWithLogin(`account/users/${username}`); - cy.wait('@getUser'); - - cy.findByText('Username').should('be.visible'); - cy.findByText('Email').should('be.visible'); - cy.findByText('Delete User').should('be.visible'); - - cy.get('[id="username"]') - .should('be.visible') - .should('have.value', username) - .clear() - .type(newUsername); - - cy.get('[data-qa-textfield-label="Username"]') - .parent() - .parent() - .parent() - .within(() => { - ui.button - .findByTitle('Save') - .should('be.visible') - .should('be.enabled') - .click(); - }); - - cy.wait('@updateUsername'); - - // No confirmation gets shown on this page when changes are saved. - // Confirm that the text field has the correct value instead. - cy.get('[id="username"]') - .should('be.visible') - .should('have.value', newUsername); - }); - }); - +describe('Display Settings', () => { /* * - Validates username update flow via the profile display page using mocked data. */ diff --git a/packages/manager/cypress/e2e/core/account/user-profile.spec.ts b/packages/manager/cypress/e2e/core/account/user-profile.spec.ts new file mode 100644 index 00000000000..a7ffc031bc6 --- /dev/null +++ b/packages/manager/cypress/e2e/core/account/user-profile.spec.ts @@ -0,0 +1,283 @@ +import { accountUserFactory } from 'src/factories/accountUsers'; +import { getProfile } from 'support/api/account'; +import { + interceptGetUser, + mockGetUser, + mockGetUsers, + mockUpdateUsername, +} from 'support/intercepts/account'; +import { randomString } from 'support/util/random'; +import { ui } from 'support/ui'; +import { mockUpdateProfile } from 'support/intercepts/profile'; + +describe('User Profile', () => { + /* + * - Validates the flow of updating the username and email of the active account user via the User Profile page using mocked data. + */ + it('can change email and username of the active account', () => { + const newUsername = randomString(12); + const newEmail = `${newUsername}@example.com`; + + getProfile().then((profile) => { + const activeUsername = profile.body.username; + const activeEmail = profile.body.email; + + interceptGetUser(activeUsername).as('getUser'); + mockUpdateUsername(activeUsername, newUsername).as('updateUsername'); + mockUpdateProfile({ + ...profile.body, + email: newEmail, + }).as('updateEmail'); + + cy.visitWithLogin(`account/users/${activeUsername}`); + cy.wait('@getUser'); + + cy.findByText('Username').should('be.visible'); + cy.findByText('Email').should('be.visible'); + cy.findByText('Delete User').should('be.visible'); + + // Confirm the currently active user cannot be deleted. + ui.button + .findByTitle('Delete') + .should('be.visible') + .should('be.disabled') + .trigger('mouseover'); + // Click the button first, then confirm the tooltip is shown. + ui.tooltip + .findByText('You can\u{2019}t delete the currently active user.') + .should('be.visible'); + + // Confirm user can update their email before updating the username, since you cannot update a different user's (as determined by username) email. + cy.get('[id="email"]') + .should('be.visible') + .should('have.value', activeEmail) + .clear() + .type(newEmail); + + cy.get('[data-qa-textfield-label="Email"]') + .parent() + .parent() + .parent() + .within(() => { + ui.button + .findByTitle('Save') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait('@updateEmail'); + + // Confirm success notice displays. + cy.findByText('Email updated successfully').should('be.visible'); + + // Confirm user can update their username. + cy.get('[id="username"]') + .should('be.visible') + .should('have.value', activeUsername) + .clear() + .type(newUsername); + + cy.get('[data-qa-textfield-label="Username"]') + .parent() + .parent() + .parent() + .within(() => { + ui.button + .findByTitle('Save') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait('@updateUsername'); + + // No confirmation gets shown on this page when changes are saved. + // Confirm that the text field has the correct value instead. + cy.get('[id="username"]') + .should('be.visible') + .should('have.value', newUsername); + }); + }); + + /* + * - Validates the flow of updating the username and email of another user via the User Profile page using mocked data. + */ + it('can change the username but not email of another user account', () => { + const newUsername = randomString(12); + + getProfile().then((profile) => { + const additionalUsername = 'mock_user2'; + const mockAccountUsers = accountUserFactory.buildList(1, { + username: additionalUsername, + }); + const additionalUser = mockAccountUsers[0]; + + mockGetUsers(mockAccountUsers).as('getUsers'); + mockGetUser(additionalUser).as('getUser'); + mockUpdateUsername(additionalUsername, newUsername).as('updateUsername'); + + cy.visitWithLogin(`account/users/${additionalUsername}`); + + cy.wait('@getUser'); + + cy.findByText('Username').should('be.visible'); + cy.findByText('Email').should('be.visible'); + cy.findByText('Delete User').should('be.visible'); + ui.button.findByTitle('Delete').should('be.visible').should('be.enabled'); + + // Confirm email of another user cannot be updated. + cy.get('[id="email"]') + .should('be.visible') + .should('have.value', additionalUser.email) + .should('be.disabled') + .parent() + .parent() + .parent() + .within(() => { + ui.button + .findByAttribute('data-qa-help-button', 'true') + .should('be.visible') + .trigger('mouseover'); + // Click the button first, then confirm the tooltip is shown. + ui.tooltip + .findByText( + 'You can\u{2019}t change another user\u{2019}s email address.' + ) + .should('be.visible'); + }); + + cy.get('[data-qa-textfield-label="Email"]') + .parent() + .parent() + .parent() + .within(() => { + ui.button + .findByTitle('Save') + .should('be.visible') + .should('be.disabled') + .click(); + }); + + // Confirm username of another user can be updated. + cy.get('[id="username"]') + .should('be.visible') + .should('have.value', additionalUsername) + .clear() + .type(newUsername); + + cy.get('[data-qa-textfield-label="Username"]') + .parent() + .parent() + .parent() + .within(() => { + ui.button + .findByTitle('Save') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait('@updateUsername'); + + // No confirmation gets shown on this page when changes are saved. + // Confirm that the text field has the correct value instead. + cy.get('[id="username"]') + .should('be.visible') + .should('have.value', newUsername); + }); + }); + + /* + * - Validates disabled username and email flow for a proxy user via the User Profile page using mocked data. + */ + it('cannot change username or email for a proxy user or delete the proxy user', () => { + getProfile().then((profile) => { + const proxyUsername = 'proxy_user'; + const mockAccountUsers = accountUserFactory.buildList(1, { + username: proxyUsername, + user_type: 'proxy', + }); + + mockGetUsers(mockAccountUsers).as('getUsers'); + mockGetUser(mockAccountUsers[0]).as('getUser'); + + cy.visitWithLogin(`account/users/${proxyUsername}`); + + cy.wait('@getUser'); + + cy.findByText('Username').should('be.visible'); + cy.findByText('Email').should('be.visible'); + cy.findByText('Delete User').should('be.visible'); + + cy.get('[id="username"]') + .should('be.visible') + .should('have.value', proxyUsername) + .should('be.disabled') + .parent() + .parent() + .parent() + .within(() => { + ui.button + .findByAttribute('data-qa-help-button', 'true') + .should('be.visible') + .trigger('mouseover'); + // Click the button first, then confirm the tooltip is shown. + ui.tooltip + .findByText('This account type cannot update this field.') + .should('be.visible'); + }); + + cy.get('[data-qa-textfield-label="Username"]') + .parent() + .parent() + .parent() + .within(() => { + ui.button + .findByTitle('Save') + .should('be.visible') + .should('be.disabled'); + }); + + cy.get('[id="email"]') + .should('be.visible') + .should('be.disabled') + .parent() + .parent() + .parent() + .within(() => { + ui.button + .findByAttribute('data-qa-help-button', 'true') + .should('be.visible') + .trigger('mouseover'); + // Click the button first, then confirm the tooltip is shown. + ui.tooltip + .findByText('This account type cannot update this field.') + .should('be.visible'); + }); + + cy.get('[data-qa-textfield-label="Email"]') + .parent() + .parent() + .parent() + .within(() => { + ui.button + .findByTitle('Save') + .should('be.visible') + .should('be.disabled') + .click(); + }); + + // Confirms the proxy user cannot be deleted. + ui.button + .findByTitle('Delete') + .should('be.visible') + .should('be.disabled') + .trigger('mouseover'); + // Click the button first, then confirm the tooltip is shown. + ui.tooltip + .findByText('You can\u{2019}t delete a business partner user.') + .should('be.visible'); + }); + }); +}); diff --git a/packages/manager/src/features/Account/constants.ts b/packages/manager/src/features/Account/constants.ts index 0bb075f38d4..83ac50faa9c 100644 --- a/packages/manager/src/features/Account/constants.ts +++ b/packages/manager/src/features/Account/constants.ts @@ -22,3 +22,6 @@ export const CHILD_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT = // TODO: Parent/Child: Requires updated copy... export const PARENT_SESSION_EXPIRED = 'Session expired. Please log in again to your business partner account.'; + +export const RESTRICTED_FIELD_TOOLTIP = + 'This account type cannot update this field.'; diff --git a/packages/manager/src/features/Profile/DisplaySettings/DisplaySettings.tsx b/packages/manager/src/features/Profile/DisplaySettings/DisplaySettings.tsx index cd45bec69f9..d86f9f66760 100644 --- a/packages/manager/src/features/Profile/DisplaySettings/DisplaySettings.tsx +++ b/packages/manager/src/features/Profile/DisplaySettings/DisplaySettings.tsx @@ -18,6 +18,7 @@ import { useMutateProfile, useProfile } from 'src/queries/profile'; import { ApplicationState } from 'src/store'; import { TimezoneForm } from './TimezoneForm'; +import { RESTRICTED_FIELD_TOOLTIP } from 'src/features/Account/constants'; export const DisplaySettings = () => { const theme = useTheme(); @@ -65,9 +66,6 @@ export const DisplaySettings = () => { ); - const restrictedProxyUserTooltip = - 'This account type cannot update this field.'; - return ( { profile?.restricted ? 'Restricted users cannot update their username. Please contact an account administrator.' : isProxyUser - ? restrictedProxyUserTooltip + ? RESTRICTED_FIELD_TOOLTIP : undefined } disabled={profile?.restricted || isProxyUser} @@ -138,7 +136,7 @@ export const DisplaySettings = () => { key={emailResetToken} label="Email" submitForm={updateEmail} - tooltipText={isProxyUser ? restrictedProxyUserTooltip : undefined} + tooltipText={isProxyUser ? RESTRICTED_FIELD_TOOLTIP : undefined} trimmed type="email" /> diff --git a/packages/manager/src/features/Users/UserProfile.tsx b/packages/manager/src/features/Users/UserProfile.tsx index 3a81a5be023..94dea27e373 100644 --- a/packages/manager/src/features/Users/UserProfile.tsx +++ b/packages/manager/src/features/Users/UserProfile.tsx @@ -9,13 +9,14 @@ import { CircleProgress } from 'src/components/CircleProgress'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { Notice } from 'src/components/Notice/Notice'; import { TextField } from 'src/components/TextField'; -import { TooltipIcon } from 'src/components/TooltipIcon'; import { Typography } from 'src/components/Typography'; +import { useAccountUser } from 'src/queries/accountUsers'; import { useProfile } from 'src/queries/profile'; import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; import { UserDeleteConfirmationDialog } from './UserDeleteConfirmationDialog'; import { StyledTitle, StyledWrapper } from './UserProfile.styles'; +import { RESTRICTED_FIELD_TOOLTIP } from '../Account/constants'; interface UserProfileProps { accountErrors?: APIError[]; @@ -59,12 +60,15 @@ export const UserProfile = (props: UserProfileProps) => { } = props; const { data: profile } = useProfile(); + const { data: currentUser } = useAccountUser(username); const [ deleteConfirmDialogOpen, setDeleteConfirmDialogOpen, ] = React.useState(false); + const isProxyUserProfile = currentUser?.user_type === 'proxy'; + const renderProfileSection = () => { const hasAccountErrorFor = getAPIErrorFor( { username: 'Username' }, @@ -97,7 +101,11 @@ export const UserProfile = (props: UserProfileProps) => { /> )} { /> )} { Delete User - {profile?.username === originalUsername && ( - - )}