import { isAfter, isBefore } from 'date-fns';
import { cloneDeep, isEmpty, isUndefined, omitBy } from 'lodash';
+import { HttpStatus } from '../../helpers/request';
import { mockClusterSysInfo, mockIdentityProvider, mockRestUser } from '../../helpers/testMocks';
import { IdentityProvider, SysInfoCluster } from '../../types/types';
-import { ChangePasswordResults, RestUserDetailed, User } from '../../types/users';
+import { ChangePasswordResults, RestUserDetailed } from '../../types/users';
import { getSystemInfo } from '../system';
import { addUserToGroup, removeUserFromGroup } from '../user_groups';
import {
constructor() {
jest.mocked(getSystemInfo).mockImplementation(this.handleGetSystemInfo);
jest.mocked(getIdentityProviders).mockImplementation(this.handleGetIdentityProviders);
- jest.mocked(getUsers).mockImplementation((p) => this.handleGetUsers(p));
+ jest.mocked(getUsers).mockImplementation(this.handleGetUsers);
jest.mocked(postUser).mockImplementation(this.handlePostUser);
jest.mocked(updateUser).mockImplementation(this.handleUpdateUser);
jest.mocked(getUserGroups).mockImplementation(this.handleGetUserGroups);
const { email, local, login, name, scmAccounts } = data;
if (scmAccounts.some((a) => isEmpty(a.trim()))) {
return Promise.reject({
- status: 400,
- json: () => Promise.resolve({ message: 'Error: Empty SCM' }),
+ response: {
+ status: HttpStatus.BadRequest,
+ data: { message: 'Error: Empty SCM' },
+ },
});
}
const newUser = mockRestUser({
scmAccounts,
});
this.users.push(newUser);
- return this.reply(undefined);
+ return this.reply(newUser);
};
- 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) as User;
+ handleUpdateUser: typeof updateUser = (id, data) => {
+ const { email, name, scmAccounts } = data;
+ const user = this.users.find((u) => u.login === id);
if (!user) {
return Promise.reject('No such user');
}
Object.assign(user, {
- ...omitBy({ name, email, scmAccounts: scmAccount }, isUndefined),
+ ...omitBy({ name, email, scmAccounts }, isUndefined),
});
- return this.reply({ user });
+ return this.reply(user);
};
handleGetIdentityProviders = (): Promise<{ identityProviders: IdentityProvider[] }> => {
export function fetchGithubRolesMapping() {
return axios
- .get<unknown, { githubPermissionsMappings: GitHubMapping[] }>(
- '/api/v2/github-permission-mappings',
- )
+ .get<{ githubPermissionsMappings: GitHubMapping[] }>('/api/v2/github-permission-mappings')
.then((data) => data.githubPermissionsMappings);
}
export function updateGithubRolesMapping(
role: string,
data: Partial<Pick<GitHubMapping, 'permissions'>>,
-): Promise<GitHubMapping> {
- return axios.patch(`/api/v2/github-permission-mappings/${role}`, data);
+) {
+ return axios.patch<GitHubMapping>(`/api/v2/github-permission-mappings/${role}`, data);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import axios from 'axios';
import { throwGlobalError } from '../helpers/error';
-import {
- deleteJSON,
- getJSON,
- HttpStatus,
- parseJSON,
- post,
- postJSON,
- postJSONBody,
-} from '../helpers/request';
+import { HttpStatus, axiosToCatch, getJSON, parseJSON, post } from '../helpers/request';
import { IdentityProvider, Paging } from '../types/types';
import {
ChangePasswordResults,
HomePage,
NoticeType,
RestUserBase,
- User,
+ RestUserDetailed,
} from '../types/users';
export function getCurrentUser(): Promise<CurrentUser> {
sonarLintLastConnectionDateTo?: string;
pageSize?: number;
pageIndex?: number;
-}): Promise<{ page: Paging; users: T[] }> {
- return getJSON('/api/v2/users', data).catch(throwGlobalError);
+}) {
+ return axios.get<{ page: Paging; users: T[] }>(`/api/v2/users`, { params: data });
}
export function postUser(data: {
name: string;
password?: string;
scmAccounts: string[];
-}): Promise<void | Response> {
- return postJSONBody('/api/v2/users', data);
+}) {
+ return axiosToCatch.post<RestUserDetailed>('/api/v2/users', data);
}
-type UpdateUserArg =
- | {
- email?: string;
- login: string;
- name?: string;
- scmAccount: string[];
- }
- | { login: string; scmAccount: string[] };
-
-export function updateUser(data: UpdateUserArg): Promise<{ user: User }> {
- return postJSON('/api/users/update', {
- ...data,
- scmAccount: data.scmAccount.length > 0 ? data.scmAccount : '',
- });
+export function updateUser(
+ id: string,
+ data: Partial<Pick<RestUserDetailed, 'email' | 'name' | 'scmAccounts'>>,
+) {
+ return axiosToCatch.patch<RestUserDetailed>(`/api/v2/users/${id}`, data);
}
-export function deleteUser({
- login,
- anonymize,
-}: {
- login: string;
- anonymize?: boolean;
-}): Promise<void | Response> {
- return deleteJSON(`/api/v2/users/${login}`, { anonymize }).catch(throwGlobalError);
+export function deleteUser({ login, anonymize }: { login: string; anonymize?: boolean }) {
+ return axios.delete(`/api/v2/users/${login}`, { params: { anonymize } });
}
export function setHomePage(homepage: HomePage): Promise<void | Response> {
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' }),
+ denisUpdateButton: byRole('button', { name: 'users.manage_user.denis.villeneuve' }),
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' }),
expect(ui.dialogPasswords.query()).not.toBeInTheDocument();
});
+
+ it('should not allow to update non-local user', async () => {
+ const user = userEvent.setup();
+ const currentUser = mockLoggedInUser({ login: 'denis.villeneuve' });
+ renderUsersApp([], currentUser);
+
+ await act(async () => user.click(await ui.denisUpdateButton.find()));
+ await user.click(
+ await within(ui.denisRow.get()).findByRole('button', { name: 'update_details' }),
+ );
+ expect(await ui.dialogUpdateUser.find()).toBeInTheDocument();
+
+ expect(ui.userNameInput.get()).toHaveValue('Denis Villeneuve');
+ expect(ui.userNameInput.get()).toBeDisabled();
+ expect(ui.emailInput.get()).toBeDisabled();
+ await user.click(ui.scmAddButton.get());
+ await user.type(ui.dialogSCMInput().get(), 'SCM');
+ await act(() => user.click(ui.updateButton.get()));
+ expect(ui.dialogUpdateUser.query()).not.toBeInTheDocument();
+ expect(await within(ui.denisRow.get()).findByText('SCM')).toBeInTheDocument();
+ });
+
+ it('should be able to remove email', async () => {
+ const user = userEvent.setup();
+ const currentUser = mockLoggedInUser({ login: 'alice.merveille' });
+ renderUsersApp([], currentUser);
+
+ expect(
+ within(await ui.aliceRow.find()).getByText('alice.merveille@wonderland.com'),
+ ).toBeInTheDocument();
+ await act(async () => user.click(await ui.aliceUpdateButton.find()));
+ await user.click(
+ await within(ui.aliceRow.get()).findByRole('button', { name: 'update_details' }),
+ );
+ expect(await ui.dialogUpdateUser.find()).toBeInTheDocument();
+
+ expect(ui.emailInput.get()).toHaveValue('alice.merveille@wonderland.com');
+ await user.clear(ui.emailInput.get());
+ await act(() => user.click(ui.updateButton.get()));
+ expect(ui.dialogUpdateUser.query()).not.toBeInTheDocument();
+ expect(
+ within(ui.aliceRow.get()).queryByText('alice.merveille@wonderland.com'),
+ ).not.toBeInTheDocument();
+ });
});
describe('in manage mode', () => {
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { AxiosError, AxiosResponse } from 'axios';
import * as React from 'react';
import SimpleModal from '../../../components/controls/SimpleModal';
import { Button, ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
import { Alert } from '../../../components/ui/Alert';
import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
-import { throwGlobalError } from '../../../helpers/error';
+import { addGlobalErrorMessage } from '../../../helpers/globalMessages';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { parseError } from '../../../helpers/request';
+import { parseErrorResponse } from '../../../helpers/request';
import { usePostUserMutation, useUpdateUserMutation } from '../../../queries/users';
import { RestUserDetailed } from '../../../types/users';
import UserScmAccountInput from './UserScmAccountInput';
const [scmAccounts, setScmAccounts] = React.useState<string[]>(user?.scmAccounts ?? []);
const [error, setError] = React.useState<string | undefined>(undefined);
- const handleError = (response: Response) => {
- if (![BAD_REQUEST, INTERNAL_SERVER_ERROR].includes(response.status)) {
- throwGlobalError(response);
+ const handleError = (error: AxiosError<AxiosResponse>) => {
+ const { response } = error;
+ const message = parseErrorResponse(response);
+
+ if (!response || ![BAD_REQUEST, INTERNAL_SERVER_ERROR].includes(response.status)) {
+ addGlobalErrorMessage(message);
} else {
- parseError(response).then((errorMsg) => setError(errorMsg), throwGlobalError);
+ setError(message);
}
};
const { user } = props;
updateUser(
- isInstanceManaged
- ? { scmAccount: scmAccounts, login }
- : {
- email: user?.local ? email : undefined,
- login,
- name: user?.local ? name : undefined,
- scmAccount: scmAccounts,
- },
+ {
+ id: login,
+ data:
+ isInstanceManaged || !user?.local
+ ? { scmAccounts }
+ : {
+ email: email !== '' ? email : null,
+ name,
+ scmAccounts,
+ },
+ },
{ onSuccess: props.onClose, onError: handleError },
);
};
const queryClient = useQueryClient();
return useMutation({
- mutationFn: (data: Parameters<typeof updateUser>[0]) => updateUser(data),
+ mutationFn: ({
+ id,
+ data,
+ }: {
+ id: Parameters<typeof updateUser>[0];
+ data: Parameters<typeof updateUser>[1];
+ }) => updateUser(id, data),
onSuccess() {
queryClient.invalidateQueries({ queryKey: ['user', 'list'] });
},
--- /dev/null
+/*
+ * 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 'axios';
+
+declare module 'axios' {
+ export interface AxiosInstance {
+ get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>;
+ delete<T = void>(url: string, config?: AxiosRequestConfig): Promise<T>;
+ post<T = any, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<T>;
+ patch<T = any, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<T>;
+
+ defaults: Omit<AxiosDefaults, 'headers'> & {
+ headers: HeadersDefaults & {
+ [key: string]: AxiosHeaderValue;
+ };
+ };
+ }
+}
export interface UserBase {
active?: boolean;
avatar?: string;
- email?: string;
+ email?: string | null;
login: string;
name?: string;
}
}
export interface RestUser extends RestUserBase {
- email: string;
+ email: string | null;
active: boolean;
local: boolean;
externalProvider: string;