"react": "16.8.6",
"react-dom": "16.8.6",
"regenerator-runtime": "0.13.2",
- "sonar-ui-common": "0.0.14",
+ "sonar-ui-common": "0.0.18",
"whatwg-fetch": "2.0.4"
},
"devDependencies": {
resolved "https://repox.jfrog.io/repox/api/npm/npm/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
integrity sha1-s56mIp72B+zYniyN8SU2iRysm40=
+lodash@4.17.14:
+ version "4.17.14"
+ resolved "https://repox.jfrog.io/repox/api/npm/npm/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba"
+ integrity sha1-nOSHrmbJYlT+ILWZ8htoFgKAeLo=
+
loglevel@^1.4.1:
version "1.6.3"
resolved "https://repox.jfrog.io/repox/api/npm/npm/loglevel/-/loglevel-1.6.3.tgz#77f2eb64be55a404c9fd04ad16d57c1d6d6b1280"
object.assign "^4.1.0"
reflect.ownkeys "^0.2.0"
-prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
+prop-types@15.7.2, prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://repox.jfrog.io/repox/api/npm/npm/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha1-UsQedbjIfnK52TYOAga5ncv/psU=
faye-websocket "^0.10.0"
uuid "^3.0.1"
-sonar-ui-common@0.0.14:
- version "0.0.14"
- resolved "https://repox.jfrog.io/repox/api/npm/npm/sonar-ui-common/-/sonar-ui-common-0.0.14.tgz#56faa2ba62503c206e9894f55f36bd9ff4934257"
- integrity sha1-VvqiumJQPCBumJT1Xza9n/STQlc=
+sonar-ui-common@0.0.18:
+ version "0.0.18"
+ resolved "https://repox.jfrog.io/repox/api/npm/npm/sonar-ui-common/-/sonar-ui-common-0.0.18.tgz#93b71859f83b85cc23e8c201bb9ed0420fcbb479"
+ integrity sha1-k7cYWfg7hcwj6MIBu57QQg/LtHk=
dependencies:
"@types/react-select" "1.2.6"
classnames "2.2.6"
date-fns "1.30.1"
formik "1.2.0"
history "3.3.0"
- lodash "4.17.11"
+ lodash "4.17.14"
+ prop-types "15.7.2"
react-draggable "3.2.1"
react-intl "2.8.0"
react-modal "3.8.2"
"regenerator-runtime": "0.13.2",
"remark-custom-blocks": "2.3.0",
"remark-slug": "5.1.0",
- "sonar-ui-common": "0.0.14",
+ "sonar-ui-common": "0.0.18",
"unist-util-visit": "1.4.0",
"valid-url": "1.0.9",
"whatwg-fetch": "2.0.4"
}
export function showLicense(): Promise<License> {
- return getJSON('/api/editions/show_license').catch((e: { response: Response }) => {
- if (e.response && e.response.status === 404) {
+ return getJSON('/api/editions/show_license').catch((response: Response) => {
+ if (response && response.status === 404) {
return Promise.resolve(undefined);
}
- return throwGlobalError(e);
+ return throwGlobalError(response);
});
}
*/
import { map } from 'lodash';
import { csvEscape } from 'sonar-ui-common/helpers/csv';
-import {
- checkStatus,
- getJSON,
- parseJSON,
- post,
- postJSON,
- request,
- RequestData
-} from 'sonar-ui-common/helpers/request';
+import { getJSON, post, postJSON, RequestData } from 'sonar-ui-common/helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';
export interface ProfileActions {
}
export function createQualityProfile(data: RequestData): Promise<any> {
- return request('/api/qualityprofiles/create')
- .setMethod('post')
- .setData(data)
- .submit()
- .then(checkStatus)
- .then(parseJSON)
- .catch(throwGlobalError);
+ return postJSON('/api/qualityprofiles/create', data).catch(throwGlobalError);
}
export function restoreQualityProfile(data: RequestData): Promise<any> {
- return request('/api/qualityprofiles/restore')
- .setMethod('post')
- .setData(data)
- .submit()
- .then(checkStatus)
- .then(parseJSON)
- .catch(throwGlobalError);
+ return postJSON('/api/qualityprofiles/restore', data).catch(throwGlobalError);
}
export interface ProfileProject {
return getJSON('/api/qualityprofiles/inheritance', { profileKey }).catch(throwGlobalError);
}
-export function setDefaultProfile(profileKey: string): Promise<void> {
+export function setDefaultProfile(profileKey: string) {
return post('/api/qualityprofiles/set_default', { profileKey });
}
}
export function changeProfileParent(profileKey: string, parentKey: string) {
- return post('/api/qualityprofiles/change_parent', { profileKey, parentKey }).catch(
- throwGlobalError
- );
+ return post('/api/qualityprofiles/change_parent', {
+ profileKey,
+ parentKey
+ }).catch(throwGlobalError);
}
export function getImporters(): Promise<
}): Promise<T.RuleDetails> {
return postJSON('/api/rules/create', data).then(
r => r.rule,
- error => {
+ response => {
// do not show global error if the status code is 409
// this case should be handled inside a component
- if (error && error.response && error.response.status === 409) {
- return Promise.reject(error.response);
+ if (response && response.status === 409) {
+ return Promise.reject(response);
} else {
- return throwGlobalError(error);
+ return throwGlobalError(response);
}
}
);
import Level from 'sonar-ui-common/components/ui/Level';
import Rating from 'sonar-ui-common/components/ui/Rating';
import { formatMeasure } from 'sonar-ui-common/helpers/measures';
-import * as request from 'sonar-ui-common/helpers/request';
import NotFound from '../../../app/components/NotFound';
import Favorite from '../../../components/controls/Favorite';
import HomePageSelect from '../../../components/controls/HomePageSelect';
import throwGlobalError from '../../utils/throwGlobalError';
import A11ySkipTarget from '../a11y/A11ySkipTarget';
import Suggestions from '../embed-docs-modal/Suggestions';
+import request from './legacy/request-legacy';
const exposeLibraries = () => {
const global = window as any;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 handleRequiredAuthentication from 'sonar-ui-common/helpers/handleRequiredAuthentication';
+import request from '../request-legacy';
+
+const { checkStatus, parseError, requestTryAndRepeatUntil } = request;
+
+jest.mock('sonar-ui-common/helpers/handleRequiredAuthentication', () => ({ default: jest.fn() }));
+jest.mock('sonar-ui-common/helpers/cookies', () => ({
+ getCookie: jest.fn().mockReturnValue('qwerasdf')
+}));
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+describe('parseError', () => {
+ it('should parse error and return the message', () => {
+ return expect(
+ parseError({
+ response: { json: jest.fn().mockResolvedValue({ errors: [{ msg: 'Error1' }] }) } as any
+ })
+ ).resolves.toBe('Error1');
+ });
+
+ it('should parse error and return concatenated messages', () => {
+ return expect(
+ parseError({
+ response: {
+ json: jest.fn().mockResolvedValue({ errors: [{ msg: 'Error1' }, { msg: 'Error2' }] })
+ } as any
+ })
+ ).resolves.toBe('Error1. Error2');
+ });
+
+ it('should parse error and return default message', () => {
+ return expect(
+ parseError({
+ response: {
+ json: jest.fn().mockResolvedValue({})
+ } as any
+ })
+ ).resolves.toBe('default_error_message');
+ });
+
+ it('should parse error and return default message', () => {
+ return expect(
+ parseError({
+ response: {
+ json: jest.fn().mockRejectedValue(undefined)
+ } as any
+ })
+ ).resolves.toBe('default_error_message');
+ });
+});
+
+describe('requestTryAndRepeatUntil', () => {
+ jest.useFakeTimers();
+
+ beforeEach(() => {
+ jest.clearAllTimers();
+ });
+
+ it('should repeat call until stop condition is met', async () => {
+ const apiCall = jest.fn().mockResolvedValue({ repeat: true });
+ const stopRepeat = jest.fn().mockImplementation(({ repeat }) => !repeat);
+
+ const promiseResult = requestTryAndRepeatUntil(
+ apiCall,
+ { max: -1, slowThreshold: -20 },
+ stopRepeat
+ );
+
+ for (let i = 1; i < 5; i++) {
+ jest.runAllTimers();
+ expect(apiCall).toBeCalledTimes(i);
+ await new Promise(setImmediate);
+ expect(stopRepeat).toBeCalledTimes(i);
+ }
+ apiCall.mockResolvedValue({ repeat: false });
+ jest.runAllTimers();
+ expect(apiCall).toBeCalledTimes(5);
+ await new Promise(setImmediate);
+ expect(stopRepeat).toBeCalledTimes(5);
+
+ return expect(promiseResult).resolves.toEqual({ repeat: false });
+ });
+
+ it('should repeat call as long as there is an error', async () => {
+ const apiCall = jest.fn().mockRejectedValue({ response: { status: 504 } });
+ const stopRepeat = jest.fn().mockReturnValue(true);
+ const promiseResult = requestTryAndRepeatUntil(
+ apiCall,
+ { max: -1, slowThreshold: -20 },
+ stopRepeat
+ );
+
+ for (let i = 1; i < 5; i++) {
+ jest.runAllTimers();
+ expect(apiCall).toBeCalledTimes(i);
+ await new Promise(setImmediate);
+ }
+ apiCall.mockResolvedValue('Success');
+ jest.runAllTimers();
+ expect(apiCall).toBeCalledTimes(5);
+ await new Promise(setImmediate);
+ expect(stopRepeat).toBeCalledTimes(1);
+
+ return expect(promiseResult).resolves.toBe('Success');
+ });
+
+ it('should stop after 3 calls', async () => {
+ const apiCall = jest.fn().mockResolvedValue({});
+ const stopRepeat = jest.fn().mockReturnValue(false);
+ const promiseResult = requestTryAndRepeatUntil(
+ apiCall,
+ { max: 3, slowThreshold: 0 },
+ stopRepeat
+ );
+
+ expect(promiseResult).rejects.toBe(undefined);
+
+ for (let i = 1; i < 3; i++) {
+ jest.runAllTimers();
+ expect(apiCall).toBeCalledTimes(i);
+ await new Promise(setImmediate);
+ }
+ apiCall.mockResolvedValue('Success');
+ jest.runAllTimers();
+ expect(apiCall).toBeCalledTimes(3);
+ });
+
+ it('should slow down after 2 calls', async () => {
+ const apiCall = jest.fn().mockResolvedValue({});
+ const stopRepeat = jest.fn().mockReturnValue(false);
+ requestTryAndRepeatUntil(apiCall, { max: 5, slowThreshold: 3 }, stopRepeat);
+
+ for (let i = 1; i < 3; i++) {
+ jest.advanceTimersByTime(500);
+ expect(apiCall).toBeCalledTimes(i);
+ await new Promise(setImmediate);
+ }
+
+ jest.advanceTimersByTime(500);
+ expect(apiCall).toBeCalledTimes(2);
+ jest.advanceTimersByTime(2000);
+ expect(apiCall).toBeCalledTimes(2);
+ jest.advanceTimersByTime(500);
+ expect(apiCall).toBeCalledTimes(3);
+ await new Promise(setImmediate);
+
+ jest.advanceTimersByTime(3000);
+ expect(apiCall).toBeCalledTimes(4);
+ });
+});
+
+describe('checkStatus', () => {
+ it('should resolve with the response', () => {
+ const response = mockResponse();
+ return expect(checkStatus(response)).resolves.toBe(response);
+ });
+
+ it('should reject with the response', () => {
+ const response = mockResponse({}, 500);
+ return expect(checkStatus(response)).rejects.toEqual({ response });
+ });
+
+ it('should handle required authentication', () => {
+ return checkStatus(mockResponse({}, 401)).catch(() => {
+ expect(handleRequiredAuthentication).toBeCalled();
+ });
+ });
+
+ it('should reload the page when version is changing', async () => {
+ const reload = jest.fn();
+ delete window.location;
+ (window as any).location = { reload };
+
+ await checkStatus(mockResponse({ 'Sonar-Version': '6.7' }));
+ expect(reload).not.toBeCalled();
+ await checkStatus(mockResponse({ 'Sonar-Version': '6.7' }));
+ expect(reload).not.toBeCalled();
+ checkStatus(mockResponse({ 'Sonar-Version': '7.9' }));
+ expect(reload).toBeCalled();
+ });
+
+ function mockResponse(headers: T.Dict<string> = {}, status = 200): any {
+ return {
+ headers: { get: (prop: string) => headers[prop] },
+ status
+ };
+ }
+});
+
+describe('request functions', () => {
+ window.fetch = jest.fn();
+
+ beforeEach(() => {
+ (window.fetch as jest.Mock).mockReset();
+ });
+
+ const jsonResponse = '{"foo": "bar"}';
+
+ it('getJSON should return correctly', () => {
+ const response = new Response(jsonResponse, { status: 200 });
+
+ (window.fetch as jest.Mock).mockResolvedValue(response);
+
+ return request.getJSON('/api/foo', { q: 'a' }).then(response => {
+ expect(response).toEqual({ foo: 'bar' });
+ });
+ });
+
+ it('postJSON should return correctly', () => {
+ const response = new Response(jsonResponse, { status: 200 });
+
+ (window.fetch as jest.Mock).mockResolvedValue(response);
+
+ return request.postJSON('/api/foo', { q: 'a' }).then(response => {
+ expect(response).toEqual({ foo: 'bar' });
+ });
+ });
+
+ it('post should return correctly', () => {
+ const response = new Response(null, { status: 200 });
+
+ (window.fetch as jest.Mock).mockResolvedValue(response);
+
+ return request.post('/api/foo', { q: 'a' }).then(response => {
+ expect(response).toBeUndefined();
+ });
+ });
+
+ it('post should handle FormData correctly', () => {
+ const response = new Response(null, { status: 200 });
+
+ (window.fetch as jest.Mock).mockResolvedValue(response);
+
+ const data = new FormData();
+ data.set('q', 'a');
+
+ return request.post('/api/foo', data).then(response => {
+ expect(response).toBeUndefined();
+ });
+ });
+
+ it('requestDelete should return correctly', () => {
+ const response = new Response('ha!', { status: 200 });
+
+ (window.fetch as jest.Mock).mockResolvedValue(response);
+
+ return request.requestDelete('/api/foo', { q: 'a' }).then(response => {
+ expect(response).toBe(response);
+ });
+ });
+
+ it('getCorsJSON should return correctly', () => {
+ const response = new Response(jsonResponse, { status: 200 });
+
+ (window.fetch as jest.Mock).mockResolvedValue(response);
+
+ return request.getCorsJSON('/api/foo').then(response => {
+ expect(response).toEqual({ foo: 'bar' });
+ });
+ });
+
+ it('getCorsJSON should reject correctly', () => {
+ const response = new Response(jsonResponse, { status: 418 });
+
+ (window.fetch as jest.Mock).mockResolvedValue(response);
+
+ return request
+ .getCorsJSON('/api/foo')
+ .then(() => {
+ fail('should throw');
+ })
+ .catch(error => {
+ expect(error.response).toBe(response);
+ });
+ });
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { stringify } from 'querystring';
+import { omitBy, isNil } from 'lodash';
+import { getCookie } from 'sonar-ui-common/helpers/cookies';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+
+/*
+ WARNING /!\ WARNING
+
+ This is a snapshot of requests.ts when it was extracted to sonar-ui-common
+
+ It's sole purpose is to not break the compatibility with 3rd party extensions that might be using
+ the various helpers.
+
+ Do not use these functions, but rather the ones from sonar-ui-common/helpers/request.ts
+*/
+
+/** Current application version. Can be changed if a newer version is deployed. */
+let currentApplicationVersion: string | undefined;
+
+function getCSRFTokenName(): string {
+ return 'X-XSRF-TOKEN';
+}
+
+function getCSRFTokenValue(): string {
+ const cookieName = 'XSRF-TOKEN';
+ const cookieValue = getCookie(cookieName);
+ if (!cookieValue) {
+ return '';
+ }
+ return cookieValue;
+}
+
+/**
+ * Return an object containing a special http request header used to prevent CSRF attacks.
+ */
+function getCSRFToken(): T.Dict<string> {
+ // Fetch API in Edge doesn't work with empty header,
+ // so we ensure non-empty value
+ const value = getCSRFTokenValue();
+ return value ? { [getCSRFTokenName()]: value } : {};
+}
+
+type RequestData = T.Dict<any>;
+
+function omitNil(obj: RequestData): RequestData {
+ return omitBy(obj, isNil);
+}
+
+/**
+ * Default options for any request
+ */
+const DEFAULT_OPTIONS: {
+ credentials: RequestCredentials;
+ method: string;
+} = {
+ credentials: 'same-origin',
+ method: 'GET'
+};
+
+/**
+ * Default request headers
+ */
+const DEFAULT_HEADERS = {
+ Accept: 'application/json'
+};
+
+/**
+ * Request
+ */
+class Request {
+ private data?: RequestData;
+
+ constructor(private readonly url: string, private readonly options: { method?: string } = {}) {}
+
+ getSubmitData(customHeaders: any = {}): { url: string; options: RequestInit } {
+ let { url } = this;
+ const options: RequestInit = { ...DEFAULT_OPTIONS, ...this.options };
+
+ if (this.data) {
+ if (this.data instanceof FormData) {
+ options.body = this.data;
+ } else {
+ const strData = stringify(omitNil(this.data));
+ if (options.method === 'GET') {
+ url += '?' + strData;
+ } else {
+ customHeaders['Content-Type'] = 'application/x-www-form-urlencoded';
+ options.body = strData;
+ }
+ }
+ }
+
+ options.headers = {
+ ...DEFAULT_HEADERS,
+ ...customHeaders
+ };
+ return { url, options };
+ }
+
+ submit(): Promise<Response> {
+ const { url, options } = this.getSubmitData({ ...getCSRFToken() });
+ return window.fetch(((window as any).baseUrl as string) + url, options);
+ }
+
+ setMethod(method: string): Request {
+ this.options.method = method;
+ return this;
+ }
+
+ setData(data?: RequestData): Request {
+ if (data) {
+ this.data = data;
+ }
+ return this;
+ }
+}
+
+/**
+ * Make a request
+ */
+function request(url: string): Request {
+ return new Request(url);
+}
+
+/**
+ * Make a cors request
+ */
+function corsRequest(url: string, mode: RequestMode = 'cors'): Request {
+ const options: RequestInit = { mode };
+ const request = new Request(url, options);
+ request.submit = function() {
+ const { url, options } = this.getSubmitData();
+ return window.fetch(url, options);
+ };
+ return request;
+}
+
+function checkApplicationVersion(response: Response): boolean {
+ const version = response.headers.get('Sonar-Version');
+ if (version) {
+ if (currentApplicationVersion && currentApplicationVersion !== version) {
+ window.location.reload();
+ return false;
+ } else {
+ currentApplicationVersion = version;
+ }
+ }
+ return true;
+}
+
+/**
+ * Check that response status is ok
+ */
+function checkStatus(response: Response): Promise<Response> {
+ return new Promise((resolve, reject) => {
+ if (checkApplicationVersion(response)) {
+ if (response.status === 401) {
+ import('sonar-ui-common/helpers/handleRequiredAuthentication')
+ .then(i => i.default())
+ .then(reject, reject);
+ } else if (response.status >= 200 && response.status < 300) {
+ resolve(response);
+ } else {
+ reject({ response });
+ }
+ }
+ });
+}
+
+/**
+ * Parse response as JSON
+ */
+function parseJSON(response: Response): Promise<any> {
+ return response.json();
+}
+
+/**
+ * Parse response of failed request
+ */
+function parseError(error: { response: Response }): Promise<string> {
+ const DEFAULT_MESSAGE = translate('default_error_message');
+ /* eslint-disable-next-line promise/no-promise-in-callback*/
+ return parseJSON(error.response)
+ .then(({ errors }) => errors.map((error: any) => error.msg).join('. '))
+ .catch(() => DEFAULT_MESSAGE);
+}
+
+/**
+ * Shortcut to do a GET request and return response json
+ */
+function getJSON(url: string, data?: RequestData): Promise<any> {
+ return request(url)
+ .setData(data)
+ .submit()
+ .then(checkStatus)
+ .then(parseJSON);
+}
+
+/**
+ * Shortcut to do a CORS GET request and return responsejson
+ */
+function getCorsJSON(url: string, data?: RequestData): Promise<any> {
+ return corsRequest(url)
+ .setData(data)
+ .submit()
+ .then(response => {
+ if (response.status >= 200 && response.status < 300) {
+ return parseJSON(response);
+ } else {
+ return Promise.reject({ response });
+ }
+ });
+}
+
+/**
+ * Shortcut to do a POST request and return response json
+ */
+function postJSON(url: string, data?: RequestData): Promise<any> {
+ return request(url)
+ .setMethod('POST')
+ .setData(data)
+ .submit()
+ .then(checkStatus)
+ .then(parseJSON);
+}
+
+/**
+ * Shortcut to do a POST request
+ */
+function post(url: string, data?: RequestData): Promise<void> {
+ return new Promise((resolve, reject) => {
+ request(url)
+ .setMethod('POST')
+ .setData(data)
+ .submit()
+ .then(checkStatus)
+ .then(() => {
+ resolve();
+ }, reject);
+ });
+}
+
+/**
+ * Shortcut to do a DELETE request and return response json
+ */
+function requestDelete(url: string, data?: RequestData): Promise<any> {
+ return request(url)
+ .setMethod('DELETE')
+ .setData(data)
+ .submit()
+ .then(checkStatus);
+}
+
+/**
+ * Delay promise for testing purposes
+ */
+function delay(response: any): Promise<any> {
+ return new Promise(resolve => setTimeout(() => resolve(response), 1200));
+}
+
+function tryRequestAgain<T>(
+ repeatAPICall: () => Promise<T>,
+ tries: { max: number; slowThreshold: number },
+ stopRepeat: (response: T) => boolean,
+ repeatErrors: number[] = []
+) {
+ tries.max--;
+ if (tries.max !== 0) {
+ return new Promise<T>(resolve => {
+ setTimeout(
+ () => resolve(requestTryAndRepeatUntil(repeatAPICall, tries, stopRepeat, repeatErrors)),
+ tries.max > tries.slowThreshold ? 500 : 3000
+ );
+ });
+ }
+ return Promise.reject();
+}
+
+function requestTryAndRepeatUntil<T>(
+ repeatAPICall: () => Promise<T>,
+ tries: { max: number; slowThreshold: number },
+ stopRepeat: (response: T) => boolean,
+ repeatErrors: number[] = []
+) {
+ return repeatAPICall().then(
+ r => {
+ if (stopRepeat(r)) {
+ return r;
+ }
+ return tryRequestAgain(repeatAPICall, tries, stopRepeat, repeatErrors);
+ },
+ (error: { response: Response }) => {
+ if (repeatErrors.length === 0 || repeatErrors.includes(error.response.status)) {
+ return tryRequestAgain(repeatAPICall, tries, stopRepeat, repeatErrors);
+ }
+ return Promise.reject(error);
+ }
+ );
+}
+
+export default {
+ checkStatus,
+ corsRequest,
+ delay,
+ getCorsJSON,
+ getCSRFToken,
+ getCSRFTokenName,
+ getCSRFTokenValue,
+ getJSON,
+ omitNil,
+ parseError,
+ parseJSON,
+ post,
+ postJSON,
+ request,
+ requestDelete,
+ requestTryAndRepeatUntil
+};
import getStore from '../getStore';
import throwGlobalError from '../throwGlobalError';
+jest.useFakeTimers();
+
it('should put the error message in the store', async () => {
- const response: any = { json: jest.fn().mockResolvedValue({ errors: [{ msg: 'error 1' }] }) };
- await throwGlobalError({ response }).catch(() => {});
+ const response = new Response();
+ response.json = jest.fn().mockResolvedValue({ errors: [{ msg: 'error 1' }] });
+
+ // We need to catch because throwGlobalError rethrows after displaying the message
+ await throwGlobalError(response)
+ .then(() => fail('Should throw'))
+ .catch(() => {});
+
expect(getStore().getState().globalMessages[0]).toMatchObject({
level: 'ERROR',
message: 'error 1'
});
it('should put a default error messsage in the store', async () => {
- const response: any = { json: jest.fn().mockResolvedValue({}) };
- await throwGlobalError({ response }).catch(() => {});
+ const response = new Response();
+ response.json = jest.fn().mockResolvedValue({});
+
+ // We need to catch because throwGlobalError rethrows after displaying the message
+ await throwGlobalError(response)
+ .then(() => fail('Should throw'))
+ .catch(() => {});
+
expect(getStore().getState().globalMessages[0]).toMatchObject({
level: 'ERROR',
message: 'default_error_message'
});
});
+
+it('should handle weird response types', () => {
+ const response = { weird: 'response type' };
+
+ return throwGlobalError(response)
+ .then(() => fail('Should throw'))
+ .catch(error => {
+ expect(error).toBe(response);
+ });
+});
+
+it('should unwrap response if necessary', async () => {
+ const response = new Response();
+ response.json = jest.fn().mockResolvedValue({});
+
+ /* eslint-disable-next-line no-console */
+ console.warn = jest.fn();
+
+ // We need to catch because throwGlobalError rethrows after displaying the message
+ await throwGlobalError({ response })
+ .then(() => fail('Should throw'))
+ .catch(() => {});
+
+ /* eslint-disable-next-line no-console */
+ expect(console.warn).toHaveBeenCalled();
+});
import { addGlobalErrorMessage } from '../../store/globalMessages';
import getStore from './getStore';
-export default function throwGlobalError(error: { response: Response }): Promise<Response> {
+export default function throwGlobalError(param: Response | any): Promise<Response | any> {
const store = getStore();
- // eslint-disable-next-line promise/no-promise-in-callback
- parseError(error).then(
- message => {
- store.dispatch(addGlobalErrorMessage(message));
- },
- () => {}
- );
- return Promise.reject(error.response);
+ if (param.response instanceof Response) {
+ /* eslint-disable-next-line no-console */
+ console.warn('DEPRECATED: response should not be wrapped, pass it directly.');
+ param = param.response;
+ }
+
+ if (param instanceof Response) {
+ return parseError(param)
+ .then(
+ message => {
+ store.dispatch(addGlobalErrorMessage(message));
+ },
+ () => {}
+ )
+ .then(() => Promise.reject(param));
+ }
+
+ return Promise.reject(param);
}
this.mounted = false;
}
- handleError = (error: { response: Response }) => {
- return parseError(error).then(message => {
+ handleError = (response: Response) => {
+ return parseError(response).then(message => {
if (this.mounted) {
this.setState({ error: message, loading: false });
}
}
function handleError(key: string, dispatch: Dispatch) {
- return (error: { response: Response }) => {
+ return (response: Response) => {
dispatch(stopLoading(key));
- return parseError(error).then(message => {
+ return parseError(response).then(message => {
dispatch(failValidation(key, message));
return Promise.reject();
});
this.mounted = false;
}
- handleError = (error: { response: Response }) => {
- if (!this.mounted || error.response.status !== 400) {
- return throwGlobalError(error);
+ handleError = (response: Response) => {
+ if (!this.mounted || response.status !== 400) {
+ return throwGlobalError(response);
} else {
- return parseError(error).then(
+ return parseError(response).then(
errorMsg => this.setState({ error: errorMsg, submitting: false }),
throwGlobalError
);
this.mounted = false;
}
- handleError = (error: { response: Response }) => {
- if (!this.mounted || ![400, 500].includes(error.response.status)) {
- return throwGlobalError(error);
+ handleError = (response: Response) => {
+ if (!this.mounted || ![400, 500].includes(response.status)) {
+ return throwGlobalError(response);
} else {
- return parseError(error).then(
+ return parseError(response).then(
errorMsg => this.setState({ error: errorMsg }),
throwGlobalError
);
import * as React from 'react';
import { mockUser } from '../../../../helpers/testMocks';
import PasswordForm from '../PasswordForm';
+import { changePassword } from '../../../../api/users';
+
+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({ preventDefault: jest.fn() } as any);
+
+ await new Promise(setImmediate);
+
+ expect(onClose).toHaveBeenCalled();
+});
+
+it('should handle password change error', async () => {
+ const wrapper = shallowRender();
+
+ (changePassword as jest.Mock).mockRejectedValue(new Response(undefined, { status: 400 }));
+
+ wrapper.setState({ newPassword: password, confirmPassword: password });
+ wrapper.instance().mounted = true;
+ wrapper.instance().handleChangePassword({ preventDefault: jest.fn() } as any);
+
+ await new Promise(setImmediate);
+
+ expect(wrapper.state('error')).toBe('default_error_message');
+});
+
+it('should handle form changes', () => {
+ const wrapper = shallowRender();
+
+ wrapper.instance().handleConfirmPasswordChange({ currentTarget: { value: 'pwd' } } as any);
+ expect(wrapper.state('confirmPassword')).toBe('pwd');
+
+ wrapper.instance().handleNewPasswordChange({ currentTarget: { value: 'pwd' } } as any);
+ expect(wrapper.state('newPassword')).toBe('pwd');
+
+ wrapper.instance().handleOldPasswordChange({ currentTarget: { value: 'pwd' } } as any);
+ expect(wrapper.state('oldPassword')).toBe('pwd');
+});
+
function shallowRender(props: Partial<PasswordForm['props']> = {}) {
- return shallow(
+ return shallow<PasswordForm>(
<PasswordForm isCurrentUser={true} onClose={jest.fn()} user={mockUser()} {...props} />
);
}
});
it('should correctly show errors', async () => {
- (updateUser as jest.Mock).mockRejectedValue({
- response: {
- status: 400,
- json: jest.fn().mockRejectedValue(undefined)
- }
- });
+ 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);
resolved "https://repox.jfrog.io/repox/api/npm/npm/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
integrity sha1-s56mIp72B+zYniyN8SU2iRysm40=
+lodash@4.17.14:
+ version "4.17.14"
+ resolved "https://repox.jfrog.io/repox/api/npm/npm/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba"
+ integrity sha1-nOSHrmbJYlT+ILWZ8htoFgKAeLo=
+
log-symbols@^1.0.2:
version "1.0.2"
resolved "https://repox.jfrog.io/repox/api/npm/npm/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
faye-websocket "^0.10.0"
uuid "^3.0.1"
-sonar-ui-common@0.0.14:
- version "0.0.14"
- resolved "https://repox.jfrog.io/repox/api/npm/npm/sonar-ui-common/-/sonar-ui-common-0.0.14.tgz#56faa2ba62503c206e9894f55f36bd9ff4934257"
- integrity sha1-VvqiumJQPCBumJT1Xza9n/STQlc=
+sonar-ui-common@0.0.18:
+ version "0.0.18"
+ resolved "https://repox.jfrog.io/repox/api/npm/npm/sonar-ui-common/-/sonar-ui-common-0.0.18.tgz#93b71859f83b85cc23e8c201bb9ed0420fcbb479"
+ integrity sha1-k7cYWfg7hcwj6MIBu57QQg/LtHk=
dependencies:
"@types/react-select" "1.2.6"
classnames "2.2.6"
date-fns "1.30.1"
formik "1.2.0"
history "3.3.0"
- lodash "4.17.11"
+ lodash "4.17.14"
+ prop-types "15.7.2"
react-draggable "3.2.1"
react-intl "2.8.0"
react-modal "3.8.2"