]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13657 Replace request-legacy by our maintained request api
authorPhilippe Perrin <philippe.perrin@sonarsource.com>
Wed, 16 Dec 2020 14:19:26 +0000 (15:19 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 10 Feb 2021 20:07:16 +0000 (20:07 +0000)
server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts
server/sonar-web/src/main/js/app/components/extensions/legacy/__tests__/request-legacy-test.ts [deleted file]
server/sonar-web/src/main/js/app/components/extensions/legacy/request-legacy.ts [deleted file]

index 139dd09fa65096287243e2db2435bbaacc108c32..86d322c6983c1a04b33cd66987ae3801eea3fdeb 100644 (file)
@@ -68,6 +68,15 @@ import Level from 'sonar-ui-common/components/ui/Level';
 import Rating from 'sonar-ui-common/components/ui/Rating';
 import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
 import { formatMeasure } from 'sonar-ui-common/helpers/measures';
+import {
+  get,
+  getJSON,
+  getText,
+  post,
+  postJSON,
+  postJSONBody,
+  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';
@@ -96,7 +105,6 @@ 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;
@@ -119,7 +127,13 @@ const exposeLibraries = () => {
   };
   global.SonarMeasures = { ...measures, formatMeasure };
   global.SonarRequest = {
-    ...request,
+    request,
+    get,
+    getJSON,
+    getText,
+    post,
+    postJSON,
+    postJSONBody,
     throwGlobalError,
     addGlobalSuccessMessage
   };
diff --git a/server/sonar-web/src/main/js/app/components/extensions/legacy/__tests__/request-legacy-test.ts b/server/sonar-web/src/main/js/app/components/extensions/legacy/__tests__/request-legacy-test.ts
deleted file mode 100644 (file)
index e77a08d..0000000
+++ /dev/null
@@ -1,309 +0,0 @@
-/*
- * 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);
-  });
-});
diff --git a/server/sonar-web/src/main/js/app/components/extensions/legacy/request-legacy.ts b/server/sonar-web/src/main/js/app/components/extensions/legacy/request-legacy.ts
deleted file mode 100644 (file)
index 1d3a51d..0000000
+++ /dev/null
@@ -1,325 +0,0 @@
-/*
- * 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
-};