+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2021 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, delay, 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')
-}));
-
-beforeAll(() => {
- jest.useFakeTimers();
-});
-
-afterAll(() => {
- jest.useRealTimers();
-});
-
-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 if empty object', () => {
- return expect(
- parseError({
- response: {
- json: jest.fn().mockResolvedValue({})
- } as any
- })
- ).resolves.toBe('default_error_message');
- });
-
- it('should parse error and return default message if undefined', () => {
- 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);
- // eslint-disable-next-line no-await-in-loop
- 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);
- // eslint-disable-next-line no-await-in-loop
- 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
- );
-
- // eslint-disable-next-line jest/valid-expect
- expect(promiseResult).rejects.toBeUndefined();
-
- for (let i = 1; i < 3; i++) {
- jest.runAllTimers();
- expect(apiCall).toBeCalledTimes(i);
- // eslint-disable-next-line no-await-in-loop
- 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);
- // eslint-disable-next-line no-await-in-loop
- 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();
- });
- });
-
- 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);
- });
- });
-});
-
-describe('delay', () => {
- it('should work as expected', async () => {
- const param = { some: 'response' };
-
- const promise = delay(param);
- jest.runAllTimers();
-
- expect(await promise).toBe(param);
- });
-});
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2021 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 { isNil, omitBy } from 'lodash';
-import { stringify } from 'querystring';
-import { getCookie } from 'sonar-ui-common/helpers/cookies';
-import { translate } from 'sonar-ui-common/helpers/l10n';
-import { getBaseUrl } from '../../../../helpers/system';
-
-/*
- 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
-*/
-
-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 } = {}) {
- this.url = url;
- this.options = options;
- }
-
- 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(getBaseUrl() + 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;
-}
-
-/**
- * Check that response status is ok
- */
-function checkStatus(response: Response): Promise<Response> {
- return new Promise((resolve, reject) => {
- 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
-};