From 771f5fffaa84395decbaa08ae64ceff5ed12882d Mon Sep 17 00:00:00 2001 From: vikvorona Date: Fri, 5 May 2023 12:15:20 +0200 Subject: [PATCH] SONAR-18890 Users and Groups pages migrated to RTL --- .../main/js/api/mocks/GroupsServiceMock.ts | 53 ++- .../src/main/js/api/mocks/UserTokensMock.ts | 12 +- .../src/main/js/api/mocks/UsersServiceMock.ts | 151 ++++++- server/sonar-web/src/main/js/api/users.ts | 7 +- .../groups/{components => }/GroupsApp.tsx | 22 +- .../__tests__/GroupsApp-it.tsx | 65 ++- .../__tests__/EditMembersModal-test.tsx | 111 ----- .../EditMembersModal-test.tsx.snap | 64 --- .../src/main/js/apps/groups/routes.tsx | 2 +- .../js/apps/users/__tests__/UsersApp-it.tsx | 389 +++++++++++++++--- .../__tests__/DeactivateForm-test.tsx | 47 --- .../components/__tests__/GroupsForm-test.tsx | 128 ------ .../__tests__/PasswordForm-test.tsx | 94 ----- .../__tests__/TokensFormItem-test.tsx | 82 ---- .../__tests__/TokensFormModal-test.tsx | 29 -- .../components/__tests__/UserActions-test.tsx | 66 --- .../components/__tests__/UserForm-test.tsx | 124 ------ .../components/__tests__/UserGroups-test.tsx | 63 --- .../__tests__/UserListItem-test.tsx | 65 --- .../__tests__/UserListItemIdentity-test.tsx | 88 ---- .../__snapshots__/GroupsForm-test.tsx.snap | 75 ---- .../__snapshots__/PasswordForm-test.tsx.snap | 113 ----- .../TokensFormItem-test.tsx.snap | 151 ------- .../TokensFormModal-test.tsx.snap | 46 --- .../__snapshots__/UserActions-test.tsx.snap | 24 -- .../__snapshots__/UserForm-test.tsx.snap | 261 ------------ .../__snapshots__/UserGroups-test.tsx.snap | 36 -- .../__snapshots__/UserListItem-test.tsx.snap | 215 ---------- .../UserListItemIdentity-test.tsx.snap | 81 ---- .../src/main/js/helpers/testMocks.ts | 11 + 30 files changed, 587 insertions(+), 2088 deletions(-) rename server/sonar-web/src/main/js/apps/groups/{components => }/GroupsApp.tsx (84%) rename server/sonar-web/src/main/js/apps/groups/{components => }/__tests__/GroupsApp-it.tsx (80%) delete mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembersModal-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembersModal-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/users/components/__tests__/DeactivateForm-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/users/components/__tests__/GroupsForm-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/users/components/__tests__/PasswordForm-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormItem-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormModal-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/users/components/__tests__/UserActions-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/users/components/__tests__/UserForm-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/users/components/__tests__/UserGroups-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItem-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItemIdentity-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/GroupsForm-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/PasswordForm-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormItem-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormModal-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserActions-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserForm-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserGroups-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItem-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItemIdentity-test.tsx.snap diff --git a/server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts index fcf3191a99e..523f59c2237 100644 --- a/server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts @@ -24,7 +24,7 @@ import { mockGroup, mockIdentityProvider, mockPaging, - mockUser, + mockUserGroupMember, } from '../../helpers/testMocks'; import { Group, @@ -34,24 +34,37 @@ import { UserGroupMember, } from '../../types/types'; import { getSystemInfo } from '../system'; -import { getIdentityProviders } from '../users'; import { + addUserToGroup, createGroup, deleteGroup, getUsersInGroup, + removeUserFromGroup, searchUsersGroups, updateGroup, } from '../user_groups'; +import { getIdentityProviders } from '../users'; + +jest.mock('../users'); +jest.mock('../system'); +jest.mock('../user_groups'); export default class GroupsServiceMock { isManaged = false; paging: Paging; groups: Group[]; + users: UserGroupMember[]; readOnlyGroups = [ mockGroup({ name: 'managed-group', managed: true }), mockGroup({ name: 'local-group', managed: false }), ]; + defaultUsers = [ + mockUserGroupMember({ name: 'alice', login: 'alice.dev' }), + mockUserGroupMember({ name: 'bob', login: 'bob.dev' }), + mockUserGroupMember({ selected: false }), + ]; + constructor() { this.groups = cloneDeep(this.readOnlyGroups); this.paging = mockPaging({ @@ -59,6 +72,7 @@ export default class GroupsServiceMock { pageSize: 2, total: 200, }); + this.users = cloneDeep(this.defaultUsers); jest.mocked(getSystemInfo).mockImplementation(this.handleGetSystemInfo); jest.mocked(getIdentityProviders).mockImplementation(this.handleGetIdentityProviders); @@ -67,10 +81,13 @@ export default class GroupsServiceMock { jest.mocked(deleteGroup).mockImplementation((g) => this.handleDeleteGroup(g)); jest.mocked(updateGroup).mockImplementation((g) => this.handleUpdateGroup(g)); jest.mocked(getUsersInGroup).mockImplementation(this.handlegetUsersInGroup); + jest.mocked(addUserToGroup).mockImplementation(this.handleAddUserToGroup); + jest.mocked(removeUserFromGroup).mockImplementation(this.handleRemoveUserFromGroup); } reset() { this.groups = cloneDeep(this.readOnlyGroups); + this.users = cloneDeep(this.defaultUsers); } setIsManaged(managed: boolean) { @@ -132,16 +149,18 @@ export default class GroupsServiceMock { }): Promise => { return this.reply({ ...this.paging, - users: [ - { - ...mockUser({ name: 'alice' }), - selected: true, - } as UserGroupMember, - { - ...mockUser({ name: 'bob' }), - selected: false, - } as UserGroupMember, - ].filter((u) => u.name.includes(data.q ?? '')), + users: this.users + .filter((u) => u.name.includes(data.q ?? '')) + .filter((u) => { + switch (data.selected) { + case 'selected': + return u.selected; + case 'deselected': + return !u.selected; + default: + return true; + } + }), }); }; @@ -201,6 +220,16 @@ export default class GroupsServiceMock { ); }; + handleAddUserToGroup: typeof addUserToGroup = ({ login }) => { + this.users = this.users.map((u) => (u.login === login ? { ...u, selected: true } : u)); + return this.reply({}); + }; + + handleRemoveUserFromGroup: typeof removeUserFromGroup = ({ login }) => { + this.users = this.users.map((u) => (u.login === login ? { ...u, selected: false } : u)); + return this.reply({}); + }; + reply(response: T): Promise { return Promise.resolve(cloneDeep(response)); } diff --git a/server/sonar-web/src/main/js/api/mocks/UserTokensMock.ts b/server/sonar-web/src/main/js/api/mocks/UserTokensMock.ts index 4c6de69c5d9..f0f9f523a9e 100644 --- a/server/sonar-web/src/main/js/api/mocks/UserTokensMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/UserTokensMock.ts @@ -68,6 +68,10 @@ export default class UserTokensMock { return Promise.reject('x_x'); } + if (this.tokens.some((t) => t.name === name)) { + return Promise.reject('This name is already used'); + } + const token = { name, login, @@ -85,13 +89,7 @@ export default class UserTokensMock { }; handleRevokeToken = ({ name }: { name: string; login?: string }) => { - const index = this.tokens.findIndex((t) => t.name === name); - - if (index < 0) { - return Promise.resolve(); - } - - this.tokens.splice(index, 1); + this.tokens = this.tokens.filter((t) => t.name !== name); return Promise.resolve(); }; diff --git a/server/sonar-web/src/main/js/api/mocks/UsersServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/UsersServiceMock.ts index a46e8bb94bf..020683d6171 100644 --- a/server/sonar-web/src/main/js/api/mocks/UsersServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/UsersServiceMock.ts @@ -19,12 +19,26 @@ */ import { isAfter, isBefore } from 'date-fns'; -import { cloneDeep } from 'lodash'; +import { cloneDeep, isEmpty, isUndefined, omitBy } from 'lodash'; import { mockClusterSysInfo, mockIdentityProvider, mockUser } from '../../helpers/testMocks'; import { IdentityProvider, Paging, SysInfoCluster } from '../../types/types'; -import { User } from '../../types/users'; +import { ChangePasswordResults, User } from '../../types/users'; import { getSystemInfo } from '../system'; -import { createUser, getIdentityProviders, searchUsers } from '../users'; +import { addUserToGroup, removeUserFromGroup } from '../user_groups'; +import { + UserGroup, + changePassword, + createUser, + deactivateUser, + getIdentityProviders, + getUserGroups, + searchUsers, + updateUser, +} from '../users'; + +jest.mock('../users'); +jest.mock('../user_groups'); +jest.mock('../system'); const DEFAULT_USERS = [ mockUser({ @@ -40,16 +54,23 @@ const DEFAULT_USERS = [ name: 'Alice Merveille', lastConnectionDate: '2023-06-27T17:08:59+0200', sonarLintLastConnectionDate: '2023-05-27T17:08:59+0200', + groups: ['group1', 'group2', 'group3', 'group4'], }), mockUser({ managed: false, + local: false, login: 'charlie.cox', name: 'Charlie Cox', lastConnectionDate: '2023-06-25T17:08:59+0200', sonarLintLastConnectionDate: '2023-06-20T12:10:59+0200', + externalProvider: 'test', + externalIdentity: 'ExternalTest', }), mockUser({ managed: true, + local: false, + externalProvider: 'test2', + externalIdentity: 'UnknownExternalProvider', login: 'denis.villeneuve', name: 'Denis Villeneuve', lastConnectionDate: '2023-06-20T15:08:59+0200', @@ -68,14 +89,48 @@ const DEFAULT_USERS = [ }), ]; +const DEFAULT_GROUPS: UserGroup[] = [ + { + id: 1001, + name: 'test1', + description: 'test1', + selected: true, + default: true, + }, + { + id: 1002, + name: 'test2', + description: 'test2', + selected: true, + default: false, + }, + { + id: 1003, + name: 'test3', + description: 'test3', + selected: false, + default: false, + }, +]; + +const DEFAULT_PASSWORD = 'test'; + export default class UsersServiceMock { isManaged = true; users = cloneDeep(DEFAULT_USERS); + groups = cloneDeep(DEFAULT_GROUPS); + password = DEFAULT_PASSWORD; constructor() { jest.mocked(getSystemInfo).mockImplementation(this.handleGetSystemInfo); jest.mocked(getIdentityProviders).mockImplementation(this.handleGetIdentityProviders); jest.mocked(searchUsers).mockImplementation((p) => this.handleSearchUsers(p)); jest.mocked(createUser).mockImplementation(this.handleCreateUser); + jest.mocked(updateUser).mockImplementation(this.handleUpdateUser); + jest.mocked(getUserGroups).mockImplementation(this.handleGetUserGroups); + jest.mocked(addUserToGroup).mockImplementation(this.handleAddUserToGroup); + jest.mocked(removeUserFromGroup).mockImplementation(this.handleRemoveUserFromGroup); + jest.mocked(changePassword).mockImplementation(this.handleChangePassword); + jest.mocked(deactivateUser).mockImplementation(this.handleDeactivateUser); } setIsManaged(managed: boolean) { @@ -187,6 +242,12 @@ export default class UsersServiceMock { scmAccount: string[]; }) => { const { email, local, login, name, scmAccount } = data; + if (scmAccount.some((a) => isEmpty(a.trim()))) { + return Promise.reject({ + status: 400, + json: () => Promise.resolve({ errors: [{ msg: 'Error: Empty SCM' }] }), + }); + } const newUser = mockUser({ email, local, @@ -198,8 +259,27 @@ export default class UsersServiceMock { return this.reply(undefined); }; + handleUpdateUser = (data: { + email?: string; + login: string; + name: string; + scmAccount: string[]; + }) => { + const { email, login, name, scmAccount } = data; + const user = this.users.find((u) => u.login === login); + if (!user) { + return Promise.reject('No such user'); + } + Object.assign(user, { + ...omitBy({ name, email, scmAccount }, isUndefined), + }); + return this.reply({ user }); + }; + handleGetIdentityProviders = (): Promise<{ identityProviders: IdentityProvider[] }> => { - return this.reply({ identityProviders: [mockIdentityProvider()] }); + return this.reply({ + identityProviders: [mockIdentityProvider({ key: 'test' })], + }); }; handleGetSystemInfo = (): Promise => { @@ -218,9 +298,72 @@ export default class UsersServiceMock { ); }; + handleGetUserGroups: typeof getUserGroups = (data) => { + const filteredGroups = this.groups + .filter((g) => g.name.includes(data.q ?? '')) + .filter((g) => { + switch (data.selected) { + case 'selected': + return g.selected; + case 'deselected': + return !g.selected; + default: + return true; + } + }); + return this.reply({ + paging: { pageIndex: 1, pageSize: 10, total: filteredGroups.length }, + groups: filteredGroups, + }); + }; + + handleAddUserToGroup: typeof addUserToGroup = ({ name }) => { + this.groups = this.groups.map((g) => (g.name === name ? { ...g, selected: true } : g)); + return this.reply({}); + }; + + handleRemoveUserFromGroup: typeof removeUserFromGroup = ({ name }) => { + let isDefault = false; + this.groups = this.groups.map((g) => { + if (g.name === name) { + if (g.default) { + isDefault = true; + return g; + } + return { ...g, selected: false }; + } + return g; + }); + return isDefault + ? Promise.reject({ + errors: [{ msg: 'Cannot remove Default group' }], + }) + : this.reply({}); + }; + + handleChangePassword: typeof changePassword = (data) => { + if (data.previousPassword !== this.password) { + return Promise.reject(ChangePasswordResults.OldPasswordIncorrect); + } + if (data.password === this.password) { + return Promise.reject(ChangePasswordResults.NewPasswordSameAsOld); + } + this.password = data.password; + return this.reply({}); + }; + + handleDeactivateUser: typeof deactivateUser = (data) => { + const index = this.users.findIndex((u) => u.login === data.login); + const user = this.users.splice(index, 1)[0]; + user.active = false; + return this.reply({ user }); + }; + reset = () => { this.isManaged = true; this.users = cloneDeep(DEFAULT_USERS); + this.groups = cloneDeep(DEFAULT_GROUPS); + this.password = DEFAULT_PASSWORD; }; reply(response: T): Promise { diff --git a/server/sonar-web/src/main/js/api/users.ts b/server/sonar-web/src/main/js/api/users.ts index 444092043f7..5afe097a7bb 100644 --- a/server/sonar-web/src/main/js/api/users.ts +++ b/server/sonar-web/src/main/js/api/users.ts @@ -97,14 +97,17 @@ export function updateUser(data: { login: string; name?: string; scmAccount: string[]; -}): Promise { +}): Promise<{ user: User }> { return postJSON('/api/users/update', { ...data, scmAccount: data.scmAccount.length > 0 ? data.scmAccount : '', }); } -export function deactivateUser(data: { login: string; anonymize?: boolean }): Promise { +export function deactivateUser(data: { + login: string; + anonymize?: boolean; +}): Promise<{ user: User }> { return postJSON('/api/users/deactivate', data).catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/apps/groups/components/GroupsApp.tsx b/server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx similarity index 84% rename from server/sonar-web/src/main/js/apps/groups/components/GroupsApp.tsx rename to server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx index 3e72b40977e..50aeba427f7 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/GroupsApp.tsx +++ b/server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx @@ -20,17 +20,17 @@ import * as React from 'react'; import { useCallback, useEffect, useState } from 'react'; import { Helmet } from 'react-helmet-async'; -import { searchUsersGroups } from '../../../api/user_groups'; -import ListFooter from '../../../components/controls/ListFooter'; -import { ManagedFilter } from '../../../components/controls/ManagedFilter'; -import SearchBox from '../../../components/controls/SearchBox'; -import Suggestions from '../../../components/embed-docs-modal/Suggestions'; -import { useManageProvider } from '../../../components/hooks/useManageProvider'; -import { translate } from '../../../helpers/l10n'; -import { Group, Paging } from '../../../types/types'; -import '../groups.css'; -import Header from './Header'; -import List from './List'; +import { searchUsersGroups } from '../../api/user_groups'; +import ListFooter from '../../components/controls/ListFooter'; +import { ManagedFilter } from '../../components/controls/ManagedFilter'; +import SearchBox from '../../components/controls/SearchBox'; +import Suggestions from '../../components/embed-docs-modal/Suggestions'; +import { useManageProvider } from '../../components/hooks/useManageProvider'; +import { translate } from '../../helpers/l10n'; +import { Group, Paging } from '../../types/types'; +import Header from './components/Header'; +import List from './components/List'; +import './groups.css'; export default function App() { const [loading, setLoading] = useState(true); diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/GroupsApp-it.tsx b/server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx similarity index 80% rename from server/sonar-web/src/main/js/apps/groups/components/__tests__/GroupsApp-it.tsx rename to server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx index 28580e632e2..6ef453bd005 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/GroupsApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx @@ -18,19 +18,15 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { screen } from '@testing-library/react'; +import { screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; import { act } from 'react-dom/test-utils'; import { byRole, byText } from 'testing-library-selector'; -import GroupsServiceMock from '../../../../api/mocks/GroupsServiceMock'; -import { renderApp } from '../../../../helpers/testReactTestingUtils'; +import GroupsServiceMock from '../../../api/mocks/GroupsServiceMock'; +import { renderApp } from '../../../helpers/testReactTestingUtils'; import App from '../GroupsApp'; -jest.mock('../../../../api/users'); -jest.mock('../../../../api/system'); -jest.mock('../../../../api/user_groups'); - const handler = new GroupsServiceMock(); const ui = { @@ -38,6 +34,8 @@ const ui = { infoManageMode: byText(/groups\.page\.managed_description/), description: byText('user_groups.page.description'), allFilter: byRole('button', { name: 'all' }), + selectedFilter: byRole('button', { name: 'selected' }), + unselectedFilter: byRole('button', { name: 'unselected' }), managedFilter: byRole('button', { name: 'managed' }), localFilter: byRole('button', { name: 'local' }), searchInput: byRole('searchbox', { name: 'search.search_by_name' }), @@ -52,10 +50,13 @@ const ui = { descriptionInput: byRole('textbox', { name: 'description' }), createGroupDialogButton: byRole('button', { name: 'create' }), editGroupDialogButton: byRole('button', { name: 'groups.create_group' }), + reloadButton: byRole('button', { name: 'reload' }), + doneButton: byRole('button', { name: 'done' }), createGroupDialog: byRole('dialog', { name: 'groups.create_group' }), membersViewDialog: byRole('dialog', { name: 'users.list' }), membersDialog: byRole('dialog', { name: 'users.update' }), + getMembers: () => within(ui.membersDialog.get()).getAllByRole('checkbox'), managedGroupRow: byRole('row', { name: 'managed-group 1' }), managedGroupEditMembersButton: byRole('button', { name: 'groups.users.edit.managed-group' }), @@ -77,16 +78,19 @@ const ui = { }), }; +beforeEach(() => { + handler.reset(); +}); + describe('in non managed mode', () => { beforeEach(() => { handler.setIsManaged(false); - handler.reset(); }); it('should render all groups', async () => { renderGroupsApp(); - expect(await ui.localGroupRow.find()).toBeInTheDocument(); + await act(async () => expect(await ui.localGroupRow.find()).toBeInTheDocument()); expect(ui.managedGroupRow.get()).toBeInTheDocument(); expect(ui.localGroupRowWithLocalBadge.query()).not.toBeInTheDocument(); }); @@ -95,7 +99,7 @@ describe('in non managed mode', () => { const user = userEvent.setup(); renderGroupsApp(); - expect(await ui.description.find()).toBeInTheDocument(); + await act(async () => expect(await ui.description.find()).toBeInTheDocument()); await act(async () => { await user.click(ui.createGroupButton.get()); }); @@ -161,7 +165,7 @@ describe('in non managed mode', () => { const user = userEvent.setup(); renderGroupsApp(); - expect(await ui.localGroupRow.find()).toBeInTheDocument(); + await act(async () => expect(await ui.localGroupRow.find()).toBeInTheDocument()); expect(await ui.localGroupEditMembersButton.find()).toBeInTheDocument(); await act(async () => { @@ -169,13 +173,34 @@ describe('in non managed mode', () => { }); expect(await ui.membersDialog.find()).toBeInTheDocument(); + + expect(ui.getMembers()).toHaveLength(2); + + await user.click(ui.allFilter.get()); + expect(ui.getMembers()).toHaveLength(3); + + await user.click(ui.unselectedFilter.get()); + expect(ui.reloadButton.query()).not.toBeInTheDocument(); + await user.click(ui.getMembers()[0]); + expect(await ui.reloadButton.find()).toBeInTheDocument(); + + await user.click(ui.selectedFilter.get()); + expect(ui.getMembers()).toHaveLength(3); + expect(ui.reloadButton.query()).not.toBeInTheDocument(); + await user.click(ui.getMembers()[0]); + expect(await ui.reloadButton.find()).toBeInTheDocument(); + await user.click(ui.reloadButton.get()); + expect(ui.getMembers()).toHaveLength(2); + + await act(() => user.click(ui.doneButton.get())); + expect(ui.membersDialog.query()).not.toBeInTheDocument(); }); it('should be able search a group', async () => { const user = userEvent.setup(); renderGroupsApp(); - expect(await ui.localGroupRow.find()).toBeInTheDocument(); + await act(async () => expect(await ui.localGroupRow.find()).toBeInTheDocument()); expect(ui.managedGroupRow.get()).toBeInTheDocument(); await act(async () => { @@ -190,7 +215,7 @@ describe('in non managed mode', () => { const user = userEvent.setup(); renderGroupsApp(); - expect(await ui.localGroupRow.find()).toBeInTheDocument(); + await act(async () => expect(await ui.localGroupRow.find()).toBeInTheDocument()); expect(await screen.findAllByRole('row')).toHaveLength(3); await act(async () => { @@ -204,12 +229,11 @@ describe('in non managed mode', () => { describe('in manage mode', () => { beforeEach(() => { handler.setIsManaged(true); - handler.reset(); }); it('should not be able to create a group', async () => { renderGroupsApp(); - expect(await ui.createGroupButton.find()).toBeDisabled(); + await act(async () => expect(await ui.createGroupButton.find()).toBeDisabled()); expect(ui.infoManageMode.get()).toBeInTheDocument(); }); @@ -217,7 +241,7 @@ describe('in manage mode', () => { const user = userEvent.setup(); renderGroupsApp(); - expect(await ui.localGroupRowWithLocalBadge.find()).toBeInTheDocument(); + await act(async () => expect(await ui.localGroupRowWithLocalBadge.find()).toBeInTheDocument()); await act(async () => { await user.click(await ui.localFilter.find()); @@ -237,20 +261,21 @@ describe('in manage mode', () => { }); it('should not be able to delete or edit a managed group', async () => { + const user = userEvent.setup(); renderGroupsApp(); - expect(await ui.managedGroupRow.find()).toBeInTheDocument(); + await act(async () => expect(await ui.managedGroupRow.find()).toBeInTheDocument()); expect(ui.managedEditButton.query()).not.toBeInTheDocument(); expect(ui.managedGroupEditMembersButton.query()).not.toBeInTheDocument(); - await userEvent.click(ui.managedGroupViewMembersButton.get()); + await act(() => user.click(ui.managedGroupViewMembersButton.get())); expect(await ui.membersViewDialog.find()).toBeInTheDocument(); expect(ui.memberAliceUser.get()).toBeInTheDocument(); expect(ui.memberBobUser.get()).toBeInTheDocument(); - await userEvent.type(ui.memberSearchInput.get(), 'b'); + await act(() => user.type(ui.memberSearchInput.get(), 'b')); expect(await ui.memberBobUser.find()).toBeInTheDocument(); expect(ui.memberAliceUser.query()).not.toBeInTheDocument(); @@ -259,7 +284,7 @@ describe('in manage mode', () => { it('should render list of all groups', async () => { renderGroupsApp(); - expect(await ui.allFilter.find()).toBeInTheDocument(); + await act(async () => expect(await ui.allFilter.find()).toBeInTheDocument()); expect(ui.localGroupRowWithLocalBadge.get()).toBeInTheDocument(); expect(ui.managedGroupRow.get()).toBeInTheDocument(); diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembersModal-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembersModal-test.tsx deleted file mode 100644 index d0ba4a37412..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembersModal-test.tsx +++ /dev/null @@ -1,111 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { addUserToGroup, getUsersInGroup, removeUserFromGroup } from '../../../../api/user_groups'; -import SelectList, { SelectListFilter } from '../../../../components/controls/SelectList'; -import { mockGroup } from '../../../../helpers/testMocks'; -import { waitAndUpdate } from '../../../../helpers/testUtils'; -import EditMembersModal from '../EditMembersModal'; - -const group = mockGroup({ name: 'foo', membersCount: 1 }); - -jest.mock('../../../../api/user_groups', () => ({ - getUsersInGroup: jest.fn().mockResolvedValue({ - paging: { pageIndex: 1, pageSize: 10, total: 1 }, - users: [ - { - login: 'foo', - name: 'bar', - selected: true, - }, - ], - }), - addUserToGroup: jest.fn().mockResolvedValue({}), - removeUserFromGroup: jest.fn().mockResolvedValue({}), -})); - -beforeEach(() => { - jest.clearAllMocks(); -}); - -it('should render modal properly', async () => { - const wrapper = shallowRender(); - wrapper.find(SelectList).props().onSearch({ - query: '', - filter: SelectListFilter.Selected, - page: 1, - pageSize: 100, - }); - await waitAndUpdate(wrapper); - expect(wrapper.state().needToReload).toBe(false); - - expect(wrapper.instance().mounted).toBe(true); - expect(wrapper).toMatchSnapshot(); - expect(wrapper.instance().renderElement('test1')).toMatchSnapshot(); - expect(wrapper.instance().renderElement('test_foo')).toMatchSnapshot(); - - expect(getUsersInGroup).toHaveBeenCalledWith( - expect.objectContaining({ - name: group.name, - p: 1, - ps: 100, - q: undefined, - selected: SelectListFilter.Selected, - }) - ); - - wrapper.instance().componentWillUnmount(); - expect(wrapper.instance().mounted).toBe(false); -}); - -it('should handle selection properly', async () => { - const wrapper = shallowRender(); - wrapper.instance().handleSelect('toto'); - await waitAndUpdate(wrapper); - - expect(addUserToGroup).toHaveBeenCalledWith( - expect.objectContaining({ - name: group.name, - login: 'toto', - }) - ); - expect(wrapper.state().needToReload).toBe(true); -}); - -it('should handle deselection properly', async () => { - const wrapper = shallowRender(); - wrapper.instance().handleUnselect('tata'); - - await waitAndUpdate(wrapper); - expect(removeUserFromGroup).toHaveBeenCalledWith( - expect.objectContaining({ - name: group.name, - login: 'tata', - }) - ); - expect(wrapper.state().needToReload).toBe(true); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembersModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembersModal-test.tsx.snap deleted file mode 100644 index 35cc168d15e..00000000000 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembersModal-test.tsx.snap +++ /dev/null @@ -1,64 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render modal properly 1`] = ` - -
-

- users.update -

-
-
- -
-
- - done - -
-
-`; - -exports[`should render modal properly 2`] = ` -
- test1 -
-`; - -exports[`should render modal properly 3`] = ` -
- test_foo -
-`; diff --git a/server/sonar-web/src/main/js/apps/groups/routes.tsx b/server/sonar-web/src/main/js/apps/groups/routes.tsx index 04bebdb5d64..108ec3cf60d 100644 --- a/server/sonar-web/src/main/js/apps/groups/routes.tsx +++ b/server/sonar-web/src/main/js/apps/groups/routes.tsx @@ -19,7 +19,7 @@ */ import React from 'react'; import { Route } from 'react-router-dom'; -import App from './components/GroupsApp'; +import App from './GroupsApp'; const routes = () => } />; diff --git a/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx b/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx index 1a2c1ed45ea..c1bc94a971e 100644 --- a/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx @@ -18,33 +18,59 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { act } from '@testing-library/react'; +import { act, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; import selectEvent from 'react-select-event'; import { byLabelText, byRole, byText } from 'testing-library-selector'; +import ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock'; +import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock'; +import UserTokensMock from '../../../api/mocks/UserTokensMock'; import UsersServiceMock from '../../../api/mocks/UsersServiceMock'; +import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks'; import { renderApp } from '../../../helpers/testReactTestingUtils'; +import { ChangePasswordResults, CurrentUser } from '../../../types/users'; import UsersApp from '../UsersApp'; -jest.mock('../../../api/users'); -jest.mock('../../../api/system'); +jest.mock('../../../api/user-tokens'); +jest.mock('../../../api/components'); +jest.mock('../../../api/settings'); -const handler = new UsersServiceMock(); +const userHandler = new UsersServiceMock(); +const tokenHandler = new UserTokensMock(); +const componentsHandler = new ComponentsServiceMock(); +const settingsHandler = new SettingsServiceMock(); const ui = { createUserButton: byRole('button', { name: 'users.create_user' }), - infoManageMode: byText(/users\.page\.managed_description/), - description: byText('users.page.description'), allFilter: byRole('button', { name: 'all' }), + selectedFilter: byRole('button', { name: 'selected' }), + unselectedFilter: byRole('button', { name: 'unselected' }), managedFilter: byRole('button', { name: 'managed' }), localFilter: byRole('button', { name: 'local' }), - searchInput: byRole('searchbox', { name: 'search.search_by_login_or_name' }), - activityFilter: byRole('combobox', { name: 'users.activity_filter.label' }), + showMore: byRole('button', { name: 'show_more' }), + aliceUpdateGroupButton: byRole('button', { name: 'users.update_users_groups.alice.merveille' }), + aliceUpdateButton: byRole('button', { name: 'users.manage_user.alice.merveille' }), + alicedDeactivateButton: byRole('button', { name: 'users.deactivate' }), + bobUpdateGroupButton: byRole('button', { name: 'users.update_users_groups.bob.marley' }), + bobUpdateButton: byRole('button', { name: 'users.manage_user.bob.marley' }), + scmAddButton: byRole('button', { name: 'add_verb' }), + createUserDialogButton: byRole('button', { name: 'create' }), + reloadButton: byRole('button', { name: 'reload' }), + doneButton: byRole('button', { name: 'done' }), + changeButton: byRole('button', { name: 'change_verb' }), + revokeButton: byRole('button', { name: 'users.tokens.revoke' }), + generateButton: byRole('button', { name: 'users.generate' }), + sureButton: byRole('button', { name: 'users.tokens.sure' }), + updateButton: byRole('button', { name: 'update_verb' }), + deleteSCMButton: (value?: string) => + byRole('button', { + name: `remove_x.users.create_user.scm_account_${value ? `x.${value}` : 'new'}`, + }), + userRows: byRole('row', { name: (accessibleName) => /^[A-Z]+ /.test(accessibleName), }), - showMore: byRole('button', { name: 'show_more' }), aliceRow: byRole('row', { name: (accessibleName) => accessibleName.startsWith('AM Alice Merveille alice.merveille '), }), @@ -52,16 +78,11 @@ const ui = { name: (accessibleName) => accessibleName.startsWith('AM Alice Merveille alice.merveille local '), }), - aliceUpdateGroupButton: byRole('button', { name: 'users.update_users_groups.alice.merveille' }), - aliceUpdateButton: byRole('button', { name: 'users.manage_user.alice.merveille' }), - alicedDeactivateButton: byRole('button', { name: 'users.deactivate' }), - bobUpdateGroupButton: byRole('button', { name: 'users.update_users_groups.bob.marley' }), - bobUpdateButton: byRole('button', { name: 'users.manage_user.bob.marley' }), bobRow: byRole('row', { name: (accessibleName) => accessibleName.startsWith('BM Bob Marley bob.marley '), }), charlieRow: byRole('row', { - name: (accessibleName) => accessibleName.startsWith('CC Charlie Cox charlie.cox local '), + name: (accessibleName) => accessibleName.startsWith('CC Charlie Cox charlie.cox'), }), denisRow: byRole('row', { name: (accessibleName) => accessibleName.startsWith('DV Denis Villeneuve denis.villeneuve '), @@ -70,23 +91,45 @@ const ui = { name: (accessibleName) => accessibleName.startsWith('EG Eva Green eva.green '), }), franckRow: byRole('row', { - name: (accessibleName) => accessibleName.startsWith('FG Franck Grillo franck.grillo local '), + name: (accessibleName) => accessibleName.startsWith('FG Franck Grillo franck.grillo '), }), + jackRow: byRole('row', { name: /Jack/ }), + + dialogGroups: byRole('dialog', { name: 'users.update_groups' }), + getGroups: () => within(ui.dialogGroups.get()).getAllByRole('checkbox'), + dialogTokens: byRole('dialog', { name: 'users.tokens' }), + dialogPasswords: byRole('dialog', { name: 'my_profile.password.title' }), + dialogUpdateUser: byRole('dialog', { name: 'users.update_user' }), + dialogCreateUser: byRole('dialog', { name: 'users.create_user' }), + dialogDeactivateUser: byRole('dialog', { name: 'users.deactivate_user' }), + + infoManageMode: byText(/users\.page\.managed_description/), + description: byText('users.page.description'), + deleteUserAlert: byText('delete-user-warning'), + + searchInput: byRole('searchbox', { name: 'search.search_by_login_or_name' }), + activityFilter: byRole('combobox', { name: 'users.activity_filter.label' }), loginInput: byRole('textbox', { name: /login/ }), userNameInput: byRole('textbox', { name: /name/ }), + emailInput: byRole('textbox', { name: /email/ }), passwordInput: byLabelText(/password/), - scmAddButton: byRole('button', { name: 'add_verb' }), - createUserDialogButton: byRole('button', { name: 'create' }), dialogSCMInputs: byRole('textbox', { name: /users.create_user.scm_account/ }), dialogSCMInput: (value?: string) => byRole('textbox', { name: `users.create_user.scm_account_${value ? `x.${value}` : 'new'}` }), - deleteSCMButton: (value?: string) => - byRole('button', { - name: `remove_x.users.create_user.scm_account_${value ? `x.${value}` : 'new'}`, - }), - jackRow: byRole('row', { name: /Jack/ }), + oldPassword: byLabelText('my_profile.password.old', { selector: 'input', exact: false }), + newPassword: byLabelText('my_profile.password.new', { selector: 'input', exact: false }), + confirmPassword: byLabelText('my_profile.password.confirm', { selector: 'input', exact: false }), + tokenNameInput: byRole('textbox', { name: 'users.tokens.name' }), + deleteUserCheckbox: byRole('checkbox', { name: 'users.delete_user' }), }; +beforeEach(() => { + tokenHandler.reset(); + userHandler.reset(); + componentsHandler.reset(); + settingsHandler.reset(); +}); + describe('different filters combinations', () => { beforeAll(() => { jest.useFakeTimers({ @@ -102,13 +145,14 @@ describe('different filters combinations', () => { it('should display all users with default filters', async () => { renderUsersApp(); - expect(await ui.userRows.findAll()).toHaveLength(6); + await act(async () => expect(await ui.userRows.findAll()).toHaveLength(6)); }); it('should display users filtered with text search', async () => { + const user = userEvent.setup(); renderUsersApp(); - await userEvent.type(await ui.searchInput.find(), 'ar'); + await act(async () => user.type(await ui.searchInput.find(), 'ar')); expect(await ui.userRows.findAll()).toHaveLength(2); expect(ui.bobRow.get()).toBeInTheDocument(); @@ -116,9 +160,10 @@ describe('different filters combinations', () => { }); it('should display local active SonarLint users', async () => { + const user = userEvent.setup(); renderUsersApp(); - await userEvent.click(await ui.localFilter.find()); + await act(async () => user.click(await ui.localFilter.find())); await act(async () => { await selectEvent.select( ui.activityFilter.get(), @@ -131,9 +176,10 @@ describe('different filters combinations', () => { }); it('should display managed active SonarQube users', async () => { + const user = userEvent.setup(); renderUsersApp(); - await userEvent.click(await ui.managedFilter.find()); + await act(async () => user.click(await ui.managedFilter.find())); await act(async () => { await selectEvent.select( ui.activityFilter.get(), @@ -146,9 +192,10 @@ describe('different filters combinations', () => { }); it('should display all inactive users', async () => { + const user = userEvent.setup(); renderUsersApp(); - await userEvent.click(await ui.allFilter.find()); + await act(async () => user.click(await ui.allFilter.find())); await act(async () => { await selectEvent.select(ui.activityFilter.get(), 'users.activity_filter.inactive_users'); }); @@ -161,55 +208,63 @@ describe('different filters combinations', () => { describe('in non managed mode', () => { beforeEach(() => { - handler.setIsManaged(false); - }); - - afterAll(() => { - handler.reset(); + userHandler.setIsManaged(false); }); it('should allow the creation of user', async () => { + const user = userEvent.setup(); renderUsersApp(); - expect(await ui.description.find()).toBeInTheDocument(); + await act(async () => expect(await ui.description.find()).toBeInTheDocument()); expect(ui.createUserButton.get()).toBeEnabled(); - await userEvent.click(ui.createUserButton.get()); + await user.click(ui.createUserButton.get()); + + expect(await ui.dialogCreateUser.find()).toBeInTheDocument(); - await userEvent.type(ui.loginInput.get(), 'Login'); - await userEvent.type(ui.userNameInput.get(), 'Jack'); - await userEvent.type(ui.passwordInput.get(), 'Password'); + await user.type(ui.loginInput.get(), 'Login'); + await user.type(ui.userNameInput.get(), 'Jack'); + await user.type(ui.passwordInput.get(), 'Password'); // Add SCM account expect(ui.dialogSCMInputs.queryAll()).toHaveLength(0); - await userEvent.click(ui.scmAddButton.get()); + await user.click(ui.scmAddButton.get()); expect(ui.dialogSCMInputs.getAll()).toHaveLength(1); - await userEvent.type(ui.dialogSCMInput().get(), 'SCM'); + await user.type(ui.dialogSCMInput().get(), 'SCM'); expect(ui.dialogSCMInput('SCM').get()).toBeInTheDocument(); + // Clear input to get an error on save + await user.clear(ui.dialogSCMInput('SCM').get()); + await act(() => user.click(ui.createUserDialogButton.get())); + expect(ui.dialogCreateUser.get()).toBeInTheDocument(); + expect( + await within(ui.dialogCreateUser.get()).findByText('Error: Empty SCM') + ).toBeInTheDocument(); // Remove SCM account - await userEvent.click(ui.deleteSCMButton('SCM').get()); + await user.click(ui.deleteSCMButton().get()); expect(ui.dialogSCMInputs.queryAll()).toHaveLength(0); - await userEvent.click(ui.createUserDialogButton.get()); + await act(() => user.click(ui.createUserDialogButton.get())); expect(ui.jackRow.get()).toBeInTheDocument(); + expect(ui.dialogCreateUser.query()).not.toBeInTheDocument(); }); - it("should be able to add/remove user's group", async () => { + it("should be able to see user's group", async () => { + const user = userEvent.setup(); renderUsersApp(); - expect(await ui.aliceUpdateGroupButton.find()).toBeInTheDocument(); + await act(async () => + expect(await within(await ui.aliceRow.find()).findByText('group1')).toBeInTheDocument() + ); + expect(within(ui.aliceRow.get()).queryByText('group4')).not.toBeInTheDocument(); + expect(within(ui.aliceRow.get()).getByText('more_x.2')).toBeInTheDocument(); + await user.click(within(ui.aliceRow.get()).getByText('more_x.2')); + expect(within(ui.aliceRow.get()).queryByText('more_x.2')).not.toBeInTheDocument(); + expect(await within(ui.aliceRow.get()).findByText('group4')).toBeInTheDocument(); expect(ui.bobUpdateGroupButton.get()).toBeInTheDocument(); }); - it('should be able to update / change password / deactivate a user', async () => { - renderUsersApp(); - - expect(await ui.aliceUpdateButton.find()).toBeInTheDocument(); - expect(ui.bobUpdateButton.get()).toBeInTheDocument(); - }); - it('should render all users', async () => { renderUsersApp(); - expect(await ui.aliceRow.find()).toBeInTheDocument(); + await act(async () => expect(await ui.aliceRow.find()).toBeInTheDocument()); expect(ui.bobRow.get()).toBeInTheDocument(); expect(ui.aliceRowWithLocalBadge.query()).not.toBeInTheDocument(); }); @@ -218,34 +273,187 @@ describe('in non managed mode', () => { const user = userEvent.setup(); renderUsersApp(); - expect(await ui.aliceRow.find()).toBeInTheDocument(); + await act(async () => expect(await ui.aliceRow.find()).toBeInTheDocument()); expect(ui.bobRow.get()).toBeInTheDocument(); - expect(ui.userRows.getAll()).toHaveLength(7); + expect(ui.userRows.getAll()).toHaveLength(6); await act(async () => { await user.click(await ui.showMore.find()); }); - expect(ui.userRows.getAll()).toHaveLength(9); + expect(ui.userRows.getAll()).toHaveLength(8); + }); + + it('should be able to edit the groups of a user', async () => { + const user = userEvent.setup(); + renderUsersApp(); + + await act(async () => + user.click( + await within(await ui.aliceRow.find()).findByRole('button', { + name: 'users.update_users_groups.alice.merveille', + }) + ) + ); + expect(await ui.dialogGroups.find()).toBeInTheDocument(); + + expect(ui.getGroups()).toHaveLength(2); + + await user.click(await ui.allFilter.find()); + expect(ui.getGroups()).toHaveLength(3); + + await user.click(ui.unselectedFilter.get()); + expect(ui.reloadButton.query()).not.toBeInTheDocument(); + await user.click(ui.getGroups()[0]); + expect(await ui.reloadButton.find()).toBeInTheDocument(); + + await user.click(ui.selectedFilter.get()); + expect(ui.getGroups()).toHaveLength(3); + expect(ui.reloadButton.query()).not.toBeInTheDocument(); + await user.click(ui.getGroups()[1]); + expect(await ui.reloadButton.find()).toBeInTheDocument(); + await user.click(ui.reloadButton.get()); + expect(ui.getGroups()).toHaveLength(2); + + await user.type(within(ui.dialogGroups.get()).getByRole('searchbox'), '3'); + + expect(ui.getGroups()).toHaveLength(1); + + await act(() => user.click(ui.doneButton.get())); + expect(ui.dialogGroups.query()).not.toBeInTheDocument(); + }); + + it('should update user', async () => { + const user = userEvent.setup(); + renderUsersApp(); + + await act(async () => + user.click( + await within(await ui.aliceRow.find()).findByRole('button', { + name: 'users.manage_user.alice.merveille', + }) + ) + ); + await user.click( + await within(ui.aliceRow.get()).findByRole('button', { name: 'update_details' }) + ); + expect(await ui.dialogUpdateUser.find()).toBeInTheDocument(); + + expect(ui.userNameInput.get()).toHaveValue('Alice Merveille'); + expect(ui.emailInput.get()).toHaveValue(''); + await user.type(ui.userNameInput.get(), '1'); + await user.type(ui.emailInput.get(), 'test@test.com'); + await act(() => user.click(ui.updateButton.get())); + expect(ui.dialogUpdateUser.query()).not.toBeInTheDocument(); + expect(await screen.findByText('Alice Merveille1')).toBeInTheDocument(); + expect(await screen.findByText('test@test.com')).toBeInTheDocument(); + }); + + it('should deactivate user', async () => { + const user = userEvent.setup(); + renderUsersApp(); + + await act(async () => + user.click( + await within(await ui.aliceRow.find()).findByRole('button', { + name: 'users.manage_user.alice.merveille', + }) + ) + ); + await user.click( + await within(ui.aliceRow.get()).findByRole('button', { name: 'users.deactivate' }) + ); + expect(await ui.dialogDeactivateUser.find()).toBeInTheDocument(); + expect(ui.deleteUserAlert.query()).not.toBeInTheDocument(); + await user.click(ui.deleteUserCheckbox.get()); + expect(await ui.deleteUserAlert.find()).toBeInTheDocument(); + + await act(() => + user.click( + within(ui.dialogDeactivateUser.get()).getByRole('button', { name: 'users.deactivate' }) + ) + ); + expect(ui.aliceRow.query()).not.toBeInTheDocument(); + }); + + it('should change a password', async () => { + const user = userEvent.setup(); + const currentUser = mockLoggedInUser({ login: 'alice.merveille' }); + renderUsersApp(currentUser); + + await act(async () => + user.click( + await within(await ui.aliceRow.find()).findByRole('button', { + name: 'users.manage_user.alice.merveille', + }) + ) + ); + await user.click( + await within(ui.aliceRow.get()).findByRole('button', { name: 'my_profile.password.title' }) + ); + expect(await ui.dialogPasswords.find()).toBeInTheDocument(); + + expect(await ui.oldPassword.find()).toBeInTheDocument(); + + expect(ui.changeButton.get()).toBeDisabled(); + + await user.type(ui.oldPassword.get(), '123'); + await user.type(ui.newPassword.get(), '1234'); + await user.type(ui.confirmPassword.get(), '1234'); + + expect(ui.changeButton.get()).toBeEnabled(); + expect( + screen.queryByText(`user.${ChangePasswordResults.OldPasswordIncorrect}`) + ).not.toBeInTheDocument(); + await user.click(ui.changeButton.get()); + expect( + await within(ui.dialogPasswords.get()).findByText( + `user.${ChangePasswordResults.OldPasswordIncorrect}` + ) + ).toBeInTheDocument(); + + await user.clear(ui.oldPassword.get()); + await user.clear(ui.newPassword.get()); + await user.clear(ui.confirmPassword.get()); + await user.type(ui.oldPassword.get(), 'test'); + await user.type(ui.newPassword.get(), 'test'); + await user.type(ui.confirmPassword.get(), 'test'); + + expect( + screen.queryByText(`user.${ChangePasswordResults.NewPasswordSameAsOld}`) + ).not.toBeInTheDocument(); + await user.click(ui.changeButton.get()); + expect( + await screen.findByText(`user.${ChangePasswordResults.NewPasswordSameAsOld}`) + ).toBeInTheDocument(); + + await user.clear(ui.newPassword.get()); + await user.clear(ui.confirmPassword.get()); + await user.type(ui.newPassword.get(), 'test2'); + await user.type(ui.confirmPassword.get(), 'test2'); + + await user.click(ui.changeButton.get()); + + expect(ui.dialogPasswords.query()).not.toBeInTheDocument(); }); }); describe('in manage mode', () => { beforeEach(() => { - handler.setIsManaged(true); + userHandler.setIsManaged(true); }); it('should not be able to create a user"', async () => { renderUsersApp(); - expect(await ui.infoManageMode.find()).toBeInTheDocument(); + await act(async () => expect(await ui.infoManageMode.find()).toBeInTheDocument()); expect(ui.createUserButton.get()).toBeDisabled(); }); it("should not be able to add/remove a user's group", async () => { renderUsersApp(); - expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument(); + await act(async () => expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument()); expect(ui.aliceUpdateGroupButton.query()).not.toBeInTheDocument(); expect(ui.bobRow.get()).toBeInTheDocument(); expect(ui.bobUpdateGroupButton.query()).not.toBeInTheDocument(); @@ -254,7 +462,7 @@ describe('in manage mode', () => { it('should not be able to update / change password / deactivate a managed user', async () => { renderUsersApp(); - expect(await ui.bobRow.find()).toBeInTheDocument(); + await act(async () => expect(await ui.bobRow.find()).toBeInTheDocument()); expect(ui.bobUpdateButton.query()).not.toBeInTheDocument(); }); @@ -262,7 +470,7 @@ describe('in manage mode', () => { const user = userEvent.setup(); renderUsersApp(); - expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument(); + await act(async () => expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument()); await user.click(ui.aliceUpdateButton.get()); expect(await ui.alicedDeactivateButton.find()).toBeInTheDocument(); }); @@ -270,7 +478,7 @@ describe('in manage mode', () => { it('should render list of all users', async () => { renderUsersApp(); - expect(await ui.allFilter.find()).toBeInTheDocument(); + await act(async () => expect(await ui.allFilter.find()).toBeInTheDocument()); expect(ui.aliceRowWithLocalBadge.get()).toBeInTheDocument(); expect(ui.bobRow.get()).toBeInTheDocument(); @@ -280,11 +488,9 @@ describe('in manage mode', () => { const user = userEvent.setup(); renderUsersApp(); - expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument(); + await act(async () => expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument()); - await act(async () => { - await user.click(await ui.managedFilter.find()); - }); + await act(async () => user.click(await ui.managedFilter.find())); expect(await ui.bobRow.find()).toBeInTheDocument(); expect(ui.aliceRowWithLocalBadge.query()).not.toBeInTheDocument(); @@ -298,11 +504,58 @@ describe('in manage mode', () => { await user.click(await ui.localFilter.find()); }); - expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument(); + expect(ui.aliceRowWithLocalBadge.get()).toBeInTheDocument(); expect(ui.bobRow.query()).not.toBeInTheDocument(); }); + + it('should be able to change tokens of a user', async () => { + const user = userEvent.setup(); + renderUsersApp(); + + await act(async () => + user.click( + await within(await ui.aliceRow.find()).findByRole('button', { + name: 'users.update_tokens_for_x.Alice Merveille', + }) + ) + ); + expect(await ui.dialogTokens.find()).toBeInTheDocument(); + + const getTokensList = () => within(ui.dialogTokens.get()).getAllByRole('row'); + + expect(getTokensList()).toHaveLength(3); + + await user.type(ui.tokenNameInput.get(), 'test'); + await user.click(ui.generateButton.get()); + + // Not deleted because there is already token with name test + expect(screen.queryByText('users.tokens.new_token_created.test')).not.toBeInTheDocument(); + expect(getTokensList()).toHaveLength(3); + + expect(ui.sureButton.query()).not.toBeInTheDocument(); + await user.click(ui.revokeButton.getAll()[1]); + expect(await ui.sureButton.find()).toBeInTheDocument(); + await act(() => user.click(ui.sureButton.get())); + + expect(getTokensList()).toHaveLength(2); + + await act(() => user.click(ui.generateButton.get())); + expect(getTokensList()).toHaveLength(3); + expect(await screen.findByText('users.tokens.new_token_created.test')).toBeInTheDocument(); + + await user.click(ui.doneButton.get()); + expect(ui.dialogTokens.query()).not.toBeInTheDocument(); + }); +}); + +it('should render external identity Providers', async () => { + renderUsersApp(); + + await act(async () => expect(await ui.charlieRow.find()).toHaveTextContent(/ExternalTest/)); + expect(await ui.denisRow.find()).toHaveTextContent(/test2: UnknownExternalProvider/); }); -function renderUsersApp() { - return renderApp('admin/users', ); +function renderUsersApp(currentUser?: CurrentUser) { + // eslint-disable-next-line testing-library/no-unnecessary-act + return renderApp('admin/users', , { currentUser: mockCurrentUser(currentUser) }); } diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/DeactivateForm-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/DeactivateForm-test.tsx deleted file mode 100644 index 0a9f8e15941..00000000000 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/DeactivateForm-test.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { screen } from '@testing-library/react'; -import * as React from 'react'; -import { deactivateUser } from '../../../../api/users'; -import { mockUser } from '../../../../helpers/testMocks'; -import { renderComponent } from '../../../../helpers/testReactTestingUtils'; -import { UserActive } from '../../../../types/users'; -import DeactivateForm from '../DeactivateForm'; - -jest.mock('../../../../api/users', () => ({ - deactivateUser: jest.fn().mockResolvedValue({}), -})); - -it('should deactivate user with anonymize set to true', () => { - const user = mockUser() as UserActive; - renderDeactivateForm(user); - - screen.getByRole('checkbox').click(); - expect(screen.getByRole('alert')).toBeInTheDocument(); - - screen.getByRole('button', { name: 'users.deactivate' }).click(); - expect(deactivateUser).toHaveBeenCalledWith({ login: user.login, anonymize: true }); -}); - -function renderDeactivateForm(user: UserActive) { - return renderComponent( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/GroupsForm-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/GroupsForm-test.tsx deleted file mode 100644 index d793476b43c..00000000000 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/GroupsForm-test.tsx +++ /dev/null @@ -1,128 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { getUserGroups } from '../../../../api/users'; -import { addUserToGroup, removeUserFromGroup } from '../../../../api/user_groups'; -import SelectList, { SelectListFilter } from '../../../../components/controls/SelectList'; -import { mockUser } from '../../../../helpers/testMocks'; -import { waitAndUpdate } from '../../../../helpers/testUtils'; -import GroupsForm from '../GroupsForm'; - -const user = mockUser(); - -jest.mock('../../../../api/users', () => ({ - getUserGroups: jest.fn().mockResolvedValue({ - paging: { pageIndex: 1, pageSize: 10, total: 1 }, - groups: [ - { - id: 1001, - name: 'test1', - description: 'test1', - selected: true, - }, - { - id: 1002, - name: 'test2', - description: 'test2', - selected: true, - }, - { - id: 1003, - name: 'test3', - description: 'test3', - selected: false, - }, - ], - }), -})); - -jest.mock('../../../../api/user_groups', () => ({ - addUserToGroup: jest.fn().mockResolvedValue({}), - removeUserFromGroup: jest.fn().mockResolvedValue({}), -})); - -beforeEach(() => { - jest.clearAllMocks(); -}); - -it('should render correctly', async () => { - const wrapper = shallowRender(); - wrapper.find(SelectList).props().onSearch({ - query: '', - filter: SelectListFilter.Selected, - page: 1, - pageSize: 100, - }); - await waitAndUpdate(wrapper); - - expect(wrapper.instance().mounted).toBe(true); - expect(wrapper).toMatchSnapshot(); - expect(wrapper.instance().renderElement('test1')).toMatchSnapshot(); - expect(wrapper.instance().renderElement('test_foo')).toMatchSnapshot(); - - expect(getUserGroups).toHaveBeenCalledWith( - expect.objectContaining({ - login: user.login, - p: 1, - ps: 100, - q: undefined, - selected: SelectListFilter.Selected, - }) - ); - expect(wrapper.state().needToReload).toBe(false); - - wrapper.instance().componentWillUnmount(); - expect(wrapper.instance().mounted).toBe(false); -}); - -it('should handle selection properly', async () => { - const wrapper = shallowRender(); - wrapper.instance().handleSelect('toto'); - await waitAndUpdate(wrapper); - - expect(addUserToGroup).toHaveBeenCalledWith( - expect.objectContaining({ - login: user.login, - name: 'toto', - }) - ); - expect(wrapper.state().needToReload).toBe(true); -}); - -it('should handle deselection properly', async () => { - const wrapper = shallowRender(); - wrapper.instance().handleUnselect('tata'); - await waitAndUpdate(wrapper); - - expect(removeUserFromGroup).toHaveBeenCalledWith( - expect.objectContaining({ - login: user.login, - name: 'tata', - }) - ); - expect(wrapper.state().needToReload).toBe(true); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/PasswordForm-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/PasswordForm-test.tsx deleted file mode 100644 index 6b81c916a5e..00000000000 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/PasswordForm-test.tsx +++ /dev/null @@ -1,94 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { changePassword } from '../../../../api/users'; -import { mockUser } from '../../../../helpers/testMocks'; -import { mockEvent, waitAndUpdate } from '../../../../helpers/testUtils'; -import { ChangePasswordResults } from '../../../../types/users'; -import PasswordForm from '../PasswordForm'; - -const password = 'new password asdf'; - -jest.mock('../../../../api/users', () => ({ - changePassword: jest.fn(() => Promise.resolve()), -})); - -it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('should handle password change', async () => { - const onClose = jest.fn(); - const wrapper = shallowRender({ onClose }); - - wrapper.setState({ newPassword: password, confirmPassword: password }); - wrapper.instance().handleChangePassword(mockEvent({ preventDefault: jest.fn() })); - - await waitAndUpdate(wrapper); - - expect(onClose).toHaveBeenCalled(); -}); - -it('should handle password change error when new password is same as old', async () => { - const wrapper = shallowRender(); - - jest.mocked(changePassword).mockRejectedValue(ChangePasswordResults.NewPasswordSameAsOld); - wrapper.setState({ newPassword: password, confirmPassword: password }); - wrapper.instance().mounted = true; - wrapper.instance().handleChangePassword(mockEvent({ preventDefault: jest.fn() })); - - await waitAndUpdate(wrapper); - - expect(wrapper.state().errorTranslationKey).toBe('user.new_password_same_as_old'); -}); - -it('should handle password change error when old password is incorrect', async () => { - const wrapper = shallowRender(); - - jest.mocked(changePassword).mockRejectedValue(ChangePasswordResults.OldPasswordIncorrect); - - wrapper.setState({ newPassword: password, confirmPassword: password }); - wrapper.instance().mounted = true; - wrapper.instance().handleChangePassword(mockEvent({ preventDefault: jest.fn() })); - - await waitAndUpdate(wrapper); - - expect(wrapper.state().errorTranslationKey).toBe('user.old_password_incorrect'); -}); - -it('should handle form changes', () => { - const wrapper = shallowRender(); - - wrapper.instance().handleConfirmPasswordChange(mockEvent({ currentTarget: { value: 'pwd' } })); - expect(wrapper.state().confirmPassword).toBe('pwd'); - - wrapper.instance().handleNewPasswordChange(mockEvent({ currentTarget: { value: 'pwd' } })); - expect(wrapper.state().newPassword).toBe('pwd'); - - wrapper.instance().handleOldPasswordChange(mockEvent({ currentTarget: { value: 'pwd' } })); - expect(wrapper.state().oldPassword).toBe('pwd'); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormItem-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormItem-test.tsx deleted file mode 100644 index 9f50f4efc3c..00000000000 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormItem-test.tsx +++ /dev/null @@ -1,82 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { revokeToken } from '../../../../api/user-tokens'; -import { mockUserToken } from '../../../../helpers/mocks/token'; -import { click, waitAndUpdate } from '../../../../helpers/testUtils'; -import TokensFormItem from '../TokensFormItem'; - -jest.mock('../../../../components/intl/DateFormatter'); -jest.mock('../../../../components/intl/DateFromNow'); -jest.mock('../../../../components/intl/DateTimeFormatter'); - -jest.mock('../../../../api/user-tokens', () => ({ - revokeToken: jest.fn().mockResolvedValue(undefined), -})); - -const userToken = mockUserToken({ - name: 'foo', - createdAt: '2019-01-15T15:06:33+0100', - lastConnectionDate: '2019-01-18T15:06:33+0100', -}); - -beforeEach(() => { - (revokeToken as jest.Mock).mockClear(); -}); - -it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); - expect(shallowRender({ deleteConfirmation: 'modal' })).toMatchSnapshot(); -}); - -it('should revoke the token using inline confirmation', async () => { - const onRevokeToken = jest.fn(); - const wrapper = shallowRender({ deleteConfirmation: 'inline', onRevokeToken }); - expect(wrapper.find('Button')).toMatchSnapshot(); - click(wrapper.find('Button')); - expect(wrapper.find('Button')).toMatchSnapshot(); - click(wrapper.find('Button')); - expect(wrapper.find('DeferredSpinner').prop('loading')).toBe(true); - await waitAndUpdate(wrapper); - expect(revokeToken).toHaveBeenCalledWith({ login: 'luke', name: 'foo' }); - expect(onRevokeToken).toHaveBeenCalledWith(userToken); -}); - -it('should revoke the token using modal confirmation', async () => { - const onRevokeToken = jest.fn(); - const wrapper = shallowRender({ deleteConfirmation: 'modal', onRevokeToken }); - wrapper.find('ConfirmButton').prop('onConfirm')(); - expect(revokeToken).toHaveBeenCalledWith({ login: 'luke', name: 'foo' }); - await waitAndUpdate(wrapper); - expect(onRevokeToken).toHaveBeenCalledWith(userToken); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormModal-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormModal-test.tsx deleted file mode 100644 index 14b3ef325da..00000000000 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormModal-test.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { mockUser } from '../../../../helpers/testMocks'; -import TokensFormModal from '../TokensFormModal'; - -it('should render correctly', () => { - expect( - shallow() - ).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserActions-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserActions-test.tsx deleted file mode 100644 index f040b6070ed..00000000000 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserActions-test.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { click } from '../../../../helpers/testUtils'; -import UserActions from '../UserActions'; - -const user = { - login: 'obi', - name: 'One', - active: true, - scmAccounts: [], - local: false, - managed: false, -}; - -it('should render correctly', () => { - expect(getWrapper()).toMatchSnapshot(); -}); - -it('should open the update form', () => { - const wrapper = getWrapper(); - click(wrapper.find('.js-user-update')); - expect(wrapper.first().find('UserForm').exists()).toBe(true); -}); - -it('should open the password form', () => { - const wrapper = getWrapper({ user: { ...user, local: true } }); - click(wrapper.find('.js-user-change-password')); - expect(wrapper.first().find('PasswordForm').exists()).toBe(true); -}); - -it('should open the deactivate form', () => { - const wrapper = getWrapper(); - click(wrapper.find('.js-user-deactivate')); - expect(wrapper.first().find('DeactivateForm').exists()).toBe(true); -}); - -function getWrapper(props = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserForm-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserForm-test.tsx deleted file mode 100644 index a2cfd5e6258..00000000000 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserForm-test.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { createUser, updateUser } from '../../../../api/users'; -import { Alert } from '../../../../components/ui/Alert'; -import { mockUser } from '../../../../helpers/testMocks'; -import { submit, waitAndUpdate } from '../../../../helpers/testUtils'; -import UserForm from '../UserForm'; - -jest.mock('../../../../api/users', () => ({ - createUser: jest.fn().mockResolvedValue({}), - updateUser: jest.fn().mockResolvedValue({}), -})); - -beforeEach(() => { - jest.clearAllMocks(); -}); - -it('should render correctly', () => { - expect(shallowRender().dive()).toMatchSnapshot(); - expect(shallowRender({ user: undefined }).dive()).toMatchSnapshot(); -}); - -it('should correctly show errors', async () => { - const response = new Response(null, { status: 400 }); - response.json = jest.fn().mockRejectedValue(undefined); - - (updateUser as jest.Mock).mockRejectedValue(response); - - const wrapper = shallowRender(); - submit(wrapper.dive().find('form')); - await waitAndUpdate(wrapper); - - expect(wrapper.dive().find(Alert).children().text()).toMatch('default_error_message'); -}); - -it('should correctly disable name and email fields for non-local users', () => { - const wrapper = shallowRender({ user: mockUser({ local: false }) }).dive(); - expect(wrapper.find('#create-user-name').prop('disabled')).toBe(true); - expect(wrapper.find('#create-user-email').prop('disabled')).toBe(true); - expect(wrapper.find('Alert').exists()).toBe(true); - expect(wrapper.find(Alert).children().text()).toMatch('users.cannot_update_delegated_user'); -}); - -it('should correctly create a new user', () => { - const email = 'foo@bar.ch'; - const login = 'foo'; - const name = 'Foo'; - const password = 'bar'; - const scmAccounts = ['gh', 'gh', 'bitbucket']; - const wrapper = shallowRender({ user: undefined }); - - wrapper.setState({ email, login, name, password, scmAccounts }); - - submit(wrapper.dive().find('form')); - - expect(createUser).toHaveBeenCalledWith({ - email, - login, - name, - password, - scmAccount: ['gh', 'gh', 'bitbucket'], - }); -}); - -it('should correctly update a local user', () => { - const email = 'foo@bar.ch'; - const login = 'foo'; - const name = 'Foo'; - const scmAccounts = ['gh', 'gh', 'bitbucket']; - const wrapper = shallowRender({ user: mockUser({ email, login, name, scmAccounts }) }).dive(); - - submit(wrapper.find('form')); - - expect(updateUser).toHaveBeenCalledWith({ - email, - login, - name, - scmAccount: ['gh', 'gh', 'bitbucket'], - }); -}); - -it('should correctly update a non-local user', () => { - const email = 'foo@bar.ch'; - const login = 'foo'; - const name = 'Foo'; - const scmAccounts = ['gh', 'bitbucket']; - const wrapper = shallowRender({ - user: mockUser({ email, local: false, login, name, scmAccounts }), - }).dive(); - - submit(wrapper.find('form')); - - expect(updateUser).toHaveBeenCalledWith( - expect.not.objectContaining({ - email, - name, - }) - ); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserGroups-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserGroups-test.tsx deleted file mode 100644 index ae11594b784..00000000000 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserGroups-test.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { click } from '../../../../helpers/testUtils'; -import UserGroups from '../UserGroups'; - -const user = { - login: 'obi', - name: 'One', - active: true, - scmAccounts: [], - local: false, - managed: false, -}; - -const groups = ['foo', 'bar', 'baz', 'plop']; - -it('should render correctly', () => { - expect(getWrapper()).toMatchSnapshot(); -}); - -it('should show all groups', () => { - const wrapper = getWrapper(); - expect(wrapper.find('li')).toHaveLength(3); - click(wrapper.find('.js-user-more-groups')); - expect(wrapper.find('li')).toHaveLength(5); -}); - -it('should open the groups form', () => { - const wrapper = getWrapper(); - click(wrapper.find('.js-user-groups')); - expect(wrapper.find('GroupsForm').exists()).toBe(true); -}); - -function getWrapper(props = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItem-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItem-test.tsx deleted file mode 100644 index 76b58716685..00000000000 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItem-test.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { click } from '../../../../helpers/testUtils'; -import { User } from '../../../../types/users'; -import UserListItem, { UserListItemProps } from '../UserListItem'; - -jest.mock('../../../../components/intl/DateFromNow'); -jest.mock('../../../../components/intl/DateTimeFormatter'); - -const user: User = { - active: true, - lastConnectionDate: '2019-01-18T15:06:33+0100', - sonarLintLastConnectionDate: '2019-01-16T15:06:33+0100', - local: false, - login: 'obi', - name: 'One', - scmAccounts: [], - managed: false, -}; - -it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('should render correctly without last connection date', () => { - expect(shallowRender({})).toMatchSnapshot(); -}); - -it('should open the correct forms', () => { - const wrapper = shallowRender(); - click(wrapper.find('.js-user-tokens')); - expect(wrapper.find('TokensFormModal').exists()).toBe(true); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItemIdentity-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItemIdentity-test.tsx deleted file mode 100644 index a362fd780d4..00000000000 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItemIdentity-test.tsx +++ /dev/null @@ -1,88 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import UserListItemIdentity, { ExternalProvider, Props } from '../UserListItemIdentity'; - -describe('#UserListItemIdentity', () => { - it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); - }); - - function shallowRender(props: Partial = {}) { - return shallow( - - ); - } -}); - -describe('#ExternalProvider', () => { - it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); - }); - - it('should render the user external provider and identity', () => { - expect(shallowRender({ identityProvider: undefined })).toMatchSnapshot(); - }); - - function shallowRender(props: Partial = {}) { - return shallow( - - ); - } -}); diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/GroupsForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/GroupsForm-test.tsx.snap deleted file mode 100644 index cbdda1a7dac..00000000000 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/GroupsForm-test.tsx.snap +++ /dev/null @@ -1,75 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` - -
-

- users.update_groups -

-
-
- -
-
- - done - -
-
-`; - -exports[`should render correctly 2`] = ` -
- - test1 -
- - test1 - -
-
-`; - -exports[`should render correctly 3`] = ` -
- test_foo -
-`; diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/PasswordForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/PasswordForm-test.tsx.snap deleted file mode 100644 index 680f7a1d7fd..00000000000 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/PasswordForm-test.tsx.snap +++ /dev/null @@ -1,113 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` - -
-
-

- my_profile.password.title -

-
-
- -
- - - -
-
- - - -
-
- - - -
-
-
- - change_verb - - - cancel - -
-
-
-`; diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormItem-test.tsx.snap deleted file mode 100644 index 9f54f8afd36..00000000000 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormItem-test.tsx.snap +++ /dev/null @@ -1,151 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` - - - foo - - - users.tokens.USER_TOKEN.short - - - - - - - - - - – - - - - - -`; - -exports[`should render correctly 2`] = ` - - - foo - - - users.tokens.USER_TOKEN.short - - - - - - - - - - – - - - - foo - , - } - } - /> - } - modalHeader="users.tokens.revoke_token" - onConfirm={[Function]} - > - - - - -`; - -exports[`should revoke the token using inline confirmation 1`] = ` - -`; - -exports[`should revoke the token using inline confirmation 2`] = ` - -`; diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormModal-test.tsx.snap deleted file mode 100644 index 153c5c0ed03..00000000000 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormModal-test.tsx.snap +++ /dev/null @@ -1,46 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` - -
-

- - John Doe - , - } - } - /> -

-
-
- -
-
- - done - -
-
-`; diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserActions-test.tsx.snap deleted file mode 100644 index b00cbcea9be..00000000000 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserActions-test.tsx.snap +++ /dev/null @@ -1,24 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` - - - - update_details - - - - users.deactivate - - - -`; diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserForm-test.tsx.snap deleted file mode 100644 index 8c0d43a038c..00000000000 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserForm-test.tsx.snap +++ /dev/null @@ -1,261 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` - -
-
-

- users.update_user -

-
-
- -
- - -
-
- - -
-
-
- - my_profile.scm_accounts - -
- -
-
-

- user.login_or_email_used_as_scm_account -

-
-
-
- - update_verb - - - cancel - -
-
-
-`; - -exports[`should render correctly 2`] = ` - -
-
-

- users.create_user -

-
-
- -
- - -

- users.minimum_x_characters.3 -

-
-
- - -
-
- - -
-
- - -
-
-
- - my_profile.scm_accounts - -
- -
-
-

- user.login_or_email_used_as_scm_account -

-
-
-
- - create - - - cancel - -
-
-
-`; diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserGroups-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserGroups-test.tsx.snap deleted file mode 100644 index 44c695ef3de..00000000000 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserGroups-test.tsx.snap +++ /dev/null @@ -1,36 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -
    -
  • - foo -
  • -
  • - bar -
  • -
  • - - more_x.2 - - - - -
  • -
-`; diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItem-test.tsx.snap deleted file mode 100644 index 3b7305310a4..00000000000 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItem-test.tsx.snap +++ /dev/null @@ -1,215 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` - - -
- - -
- - - - - - - - - - - - - - - - - - - - - - -`; - -exports[`should render correctly without last connection date 1`] = ` - - -
- - -
- - - - - - - - - - - - - - - - - - - - - - -`; diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItemIdentity-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItemIdentity-test.tsx.snap deleted file mode 100644 index 48791a5d065..00000000000 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItemIdentity-test.tsx.snap +++ /dev/null @@ -1,81 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`#ExternalProvider should render correctly 1`] = ` -
-
- Foo Provider -
-
-`; - -exports[`#ExternalProvider should render the user external provider and identity 1`] = ` -
- - foo - : - -
-`; - -exports[`#UserListItemIdentity should render correctly 1`] = ` -
-
- - One - - - obi - -
-
- obi.one@empire -
- -
-`; diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index fe4dd38fa58..b3948def2ca 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -53,6 +53,7 @@ import { SysInfoCluster, SysInfoLogging, SysInfoStandalone, + UserGroupMember, } from '../types/types'; import { CurrentUser, LoggedInUser, User } from '../types/users'; @@ -676,6 +677,16 @@ export function mockUser(overrides: Partial = {}): User { }; } +export function mockUserGroupMember(overrides: Partial = {}): UserGroupMember { + return { + login: 'john.doe', + name: 'John Doe', + managed: false, + selected: true, + ...overrides, + }; +} + export function mockDocumentationMarkdown( overrides: Partial<{ content: string; title: string; key: string }> = {} ): string { -- 2.39.5