@@ -12,7 +12,7 @@ | |||
"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": { |
@@ -5849,6 +5849,11 @@ lodash@4.17.11, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.3 | |||
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" | |||
@@ -7358,7 +7363,7 @@ prop-types-exact@^1.2.0: | |||
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= | |||
@@ -8416,10 +8421,10 @@ sockjs@0.3.19: | |||
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" | |||
@@ -8433,7 +8438,8 @@ sonar-ui-common@0.0.14: | |||
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" |
@@ -35,7 +35,7 @@ | |||
"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" |
@@ -42,10 +42,10 @@ export function isValidLicense(): Promise<{ isValidLicense: boolean }> { | |||
} | |||
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); | |||
}); | |||
} |
@@ -19,15 +19,7 @@ | |||
*/ | |||
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 { | |||
@@ -89,23 +81,11 @@ export function getQualityProfile(data: { | |||
} | |||
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 { | |||
@@ -125,7 +105,7 @@ export function getProfileInheritance(profileKey: string): Promise<any> { | |||
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 }); | |||
} | |||
@@ -142,9 +122,10 @@ export function deleteProfile(profileKey: string) { | |||
} | |||
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< |
@@ -82,13 +82,13 @@ export function createRule(data: { | |||
}): 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); | |||
} | |||
} | |||
); |
@@ -64,7 +64,6 @@ import DuplicationsRating from 'sonar-ui-common/components/ui/DuplicationsRating | |||
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'; | |||
@@ -92,6 +91,7 @@ import addGlobalSuccessMessage from '../../utils/addGlobalSuccessMessage'; | |||
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; |
@@ -0,0 +1,298 @@ | |||
/* | |||
* 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); | |||
}); | |||
}); | |||
}); |
@@ -0,0 +1,337 @@ | |||
/* | |||
* 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 | |||
}; |
@@ -20,9 +20,17 @@ | |||
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' | |||
@@ -30,10 +38,42 @@ it('should put the error message in the store', async () => { | |||
}); | |||
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(); | |||
}); |
@@ -21,15 +21,25 @@ import { parseError } from 'sonar-ui-common/helpers/request'; | |||
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); | |||
} |
@@ -61,8 +61,8 @@ export class EmailForm extends React.PureComponent<Props, State> { | |||
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 }); | |||
} |
@@ -130,9 +130,9 @@ export function resetValue(key: string, component?: string) { | |||
} | |||
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(); | |||
}); |
@@ -58,11 +58,11 @@ export default class PasswordForm extends React.PureComponent<Props, State> { | |||
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 | |||
); |
@@ -76,11 +76,11 @@ export default class UserForm extends React.PureComponent<Props, State> { | |||
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 | |||
); |
@@ -21,13 +21,59 @@ import { shallow } from 'enzyme'; | |||
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} /> | |||
); | |||
} |
@@ -39,12 +39,11 @@ it('should render correctly', () => { | |||
}); | |||
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); |
@@ -6361,6 +6361,11 @@ lodash@4.17.11, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.3 | |||
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" | |||
@@ -9263,10 +9268,10 @@ sockjs@0.3.19: | |||
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" | |||
@@ -9280,7 +9285,8 @@ sonar-ui-common@0.0.14: | |||
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" |