]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16565 Move max token lifetime logic to its own function
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Fri, 8 Jul 2022 08:13:41 +0000 (10:13 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 8 Jul 2022 20:02:47 +0000 (20:02 +0000)
server/sonar-web/src/main/js/api/settings.ts
server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx
server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx
server/sonar-web/src/main/js/helpers/__tests__/tokens-test.ts [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/tokens.ts [new file with mode: 0644]

index e322511bce821c8fa727345449e9c2f74168e7dc..c00cc81078b745570f16046e72f2ba43e771214e 100644 (file)
@@ -45,6 +45,15 @@ export function getValues(
   ]);
 }
 
+export function getAllValues(
+  data: { component?: string } & BranchParameters = {}
+): Promise<SettingValue[]> {
+  return getJSON('/api/settings/values', data).then((r: SettingValueResponse) => [
+    ...r.settings,
+    ...r.setSecuredSettings.map(key => ({ key }))
+  ]);
+}
+
 export function setSettingValue(
   definition: SettingDefinition,
   value: any,
index 3afaf428006c092e4277f850b1ff87d3437ccfbc..7abb80758a58923284e6dfbc7cfff876f1adf450 100644 (file)
 import { screen, waitFor, within } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { UserEvent } from '@testing-library/user-event/dist/types/setup';
-import { format } from 'date-fns';
 import selectEvent from 'react-select-event';
 import { getMyProjects, getScannableProjects } from '../../../api/components';
 import NotificationsMock from '../../../api/mocks/NotificationsMock';
 import UserTokensMock from '../../../api/mocks/UserTokensMock';
-import { getValues } from '../../../api/settings';
-import { generateToken } from '../../../api/user-tokens';
 import { mockUserToken } from '../../../helpers/mocks/token';
 import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks';
 import { renderApp } from '../../../helpers/testReactTestingUtils';
 import { Permissions } from '../../../types/permissions';
-import { SettingsKey } from '../../../types/settings';
-import { TokenExpiration, TokenType } from '../../../types/token';
+import { TokenType } from '../../../types/token';
 import { CurrentUser } from '../../../types/users';
 import routes from '../routes';
 
@@ -50,7 +46,7 @@ jest.mock('../../../api/settings', () => {
   const { SettingsKey } = jest.requireActual('../../../types/settings');
   return {
     ...jest.requireActual('../../../api/settings'),
-    getValues: jest.fn().mockResolvedValue([
+    getAllValues: jest.fn().mockResolvedValue([
       {
         key: SettingsKey.TokenMaxAllowedLifetime,
         value: 'No expiration'
@@ -234,7 +230,6 @@ describe('security page', () => {
 
   beforeAll(() => {
     tokenMock = new UserTokensMock();
-    (getValues as jest.Mock).mockClear();
   });
 
   afterEach(() => {
@@ -243,133 +238,6 @@ describe('security page', () => {
 
   const securityPagePath = 'account/security';
 
-  it.each([
-    [
-      '30 days',
-      [TokenExpiration.OneMonth],
-      [TokenExpiration.ThreeMonths, TokenExpiration.OneYear, TokenExpiration.NoExpiration]
-    ],
-    [
-      '90 days',
-      [TokenExpiration.OneMonth, TokenExpiration.ThreeMonths],
-      [TokenExpiration.OneYear, TokenExpiration.NoExpiration]
-    ],
-    [
-      '1 year',
-      [TokenExpiration.OneMonth, TokenExpiration.ThreeMonths, TokenExpiration.OneYear],
-      [TokenExpiration.NoExpiration]
-    ],
-    [
-      'No expiration',
-      [
-        TokenExpiration.OneMonth,
-        TokenExpiration.ThreeMonths,
-        TokenExpiration.OneYear,
-        TokenExpiration.NoExpiration
-      ],
-      []
-    ]
-  ])(
-    'should display expiration date inferior or equal to the settings limit %s',
-    async (settingMaxLifetime, expectedTime, notExpectedTime) => {
-      (getValues as jest.Mock).mockImplementationOnce(() =>
-        Promise.resolve([{ key: SettingsKey.TokenMaxAllowedLifetime, value: settingMaxLifetime }])
-      );
-
-      renderAccountApp(
-        mockLoggedInUser({ permissions: { global: [Permissions.Scan] } }),
-        securityPagePath
-      );
-
-      await selectEvent.openMenu(screen.getAllByRole('textbox')[2]);
-
-      expectedTime.forEach(time => {
-        // TokenExpiration.OneMonth is expected twice has it is the default value.
-        expect(screen.getAllByText(`users.tokens.expiration.${time}`).length).toBe(
-          time === TokenExpiration.OneMonth ? 2 : 1
-        );
-      });
-
-      notExpectedTime.forEach(time => {
-        expect(screen.queryByText(`users.tokens.expiration.${time}`)).not.toBeInTheDocument();
-      });
-    }
-  );
-
-  it('should handle absent setting', async () => {
-    (getValues as jest.Mock).mockImplementationOnce(() => Promise.resolve([]));
-
-    renderAccountApp(
-      mockLoggedInUser({ permissions: { global: [Permissions.Scan] } }),
-      securityPagePath
-    );
-
-    await selectEvent.openMenu(screen.getAllByRole('textbox')[2]);
-
-    [
-      TokenExpiration.OneMonth,
-      TokenExpiration.ThreeMonths,
-      TokenExpiration.OneYear,
-      TokenExpiration.NoExpiration
-    ].forEach(time => {
-      // TokenExpiration.OneMonth is expected twice has it is the default value.
-      expect(screen.getAllByText(`users.tokens.expiration.${time}`).length).toBe(
-        time === TokenExpiration.OneMonth ? 2 : 1
-      );
-    });
-  });
-
-  it.each([
-    [TokenExpiration.OneMonth, '2022-07-01'],
-    [TokenExpiration.ThreeMonths, '2022-08-30'],
-    [TokenExpiration.OneYear, '2023-06-01'],
-    [TokenExpiration.NoExpiration, undefined]
-  ])(
-    'should allow token creation with proper expiration date for %s days',
-    async (tokenExpirationTime, expectedTime) => {
-      const user = userEvent.setup();
-
-      renderAccountApp(
-        mockLoggedInUser({ permissions: { global: [Permissions.Scan] } }),
-        securityPagePath
-      );
-
-      // Add the token
-      const newTokenName = 'importantToken' + tokenExpirationTime;
-      const input = screen.getByPlaceholderText('users.tokens.enter_name');
-      const generateButton = screen.getByRole('button', { name: 'users.generate' });
-      expect(input).toBeInTheDocument();
-      await user.click(input);
-      await user.keyboard(newTokenName);
-
-      expect(generateButton).toBeDisabled();
-
-      const tokenTypeLabel = `users.tokens.${TokenType.User}`;
-      await selectEvent.select(screen.getAllByRole('textbox')[1], [tokenTypeLabel]);
-
-      const tokenExpirationLabel = `users.tokens.expiration.${tokenExpirationTime}`;
-      await selectEvent.select(screen.getAllByRole('textbox')[2], [tokenExpirationLabel]);
-
-      await user.click(generateButton);
-
-      expect(generateToken).toHaveBeenLastCalledWith({
-        name: newTokenName,
-        login: 'luke',
-        type: TokenType.User,
-        expirationDate: expectedTime
-      });
-
-      // ensure the list of tokens is updated
-      const rows = await screen.findAllByRole('row');
-      expect(rows).toHaveLength(4);
-      expect(rows.pop()).toHaveTextContent(
-        `${newTokenName}users.tokens.USER_TOKEN.shortneverApril 4, 2022${
-          expectedTime ? format(expectedTime, 'MMMM D, YYYY') : '–'
-        }users.tokens.revoke`
-      );
-    }
-  );
-
   it.each([
     ['user', TokenType.User],
     ['global', TokenType.Global],
index cff1307a7c226b7d7e5b3d4e964db3e95294d0d4..ec07f19b5e4ecc4ef378b59f67200c1e11e75ef3 100644 (file)
 import { isEmpty } from 'lodash';
 import * as React from 'react';
 import { getScannableProjects } from '../../../api/components';
-import { getValues } from '../../../api/settings';
 import { generateToken, getTokens } from '../../../api/user-tokens';
 import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
 import { SubmitButton } from '../../../components/controls/buttons';
 import Select, { BasicSelectOption } from '../../../components/controls/Select';
 import DeferredSpinner from '../../../components/ui/DeferredSpinner';
-import { now, toShortNotSoISOString } from '../../../helpers/dates';
 import { translate } from '../../../helpers/l10n';
+import {
+  computeTokenExpirationDate,
+  EXPIRATION_OPTIONS,
+  getAvailableExpirationOptions
+} from '../../../helpers/tokens';
 import { hasGlobalPermission } from '../../../helpers/users';
 import { Permissions } from '../../../types/permissions';
-import { SettingsKey } from '../../../types/settings';
 import { TokenExpiration, TokenType, UserToken } from '../../../types/token';
 import { CurrentUser } from '../../../types/users';
 import TokensFormItem, { TokenDeleteConfirmation } from './TokensFormItem';
@@ -57,25 +59,6 @@ interface State {
   tokenExpirationOptions: { value: TokenExpiration; label: string }[];
 }
 
-const EXPIRATION_OPTIONS = [
-  TokenExpiration.OneMonth,
-  TokenExpiration.ThreeMonths,
-  TokenExpiration.OneYear,
-  TokenExpiration.NoExpiration
-].map(value => {
-  return {
-    value,
-    label: translate('users.tokens.expiration', value.toString())
-  };
-});
-
-const SETTINGS_EXPIRATION_MAP: { [key: string]: TokenExpiration } = {
-  '30 days': TokenExpiration.OneMonth,
-  '90 days': TokenExpiration.ThreeMonths,
-  '1 year': TokenExpiration.OneYear,
-  'No expiration': TokenExpiration.NoExpiration
-};
-
 export class TokensForm extends React.PureComponent<Props, State> {
   mounted = false;
   state: State = {
@@ -120,29 +103,9 @@ export class TokensForm extends React.PureComponent<Props, State> {
   };
 
   fetchTokenSettings = async () => {
-    /*
-     * We intentionally fetch all settings, because fetching a specific setting will
-     * return it from the DB as a fallback, even if the setting is not defined at startup.
-     */
-    const settings = await getValues({ keys: '' });
-    const maxTokenLifetime = settings.find(
-      ({ key }) => key === SettingsKey.TokenMaxAllowedLifetime
-    );
-
-    if (maxTokenLifetime === undefined || maxTokenLifetime.value === undefined) {
-      return;
-    }
-
-    const maxTokenExpirationOption = SETTINGS_EXPIRATION_MAP[maxTokenLifetime.value];
-
-    if (maxTokenExpirationOption !== TokenExpiration.NoExpiration) {
-      const tokenExpirationOptions = EXPIRATION_OPTIONS.filter(
-        option =>
-          option.value <= maxTokenExpirationOption && option.value !== TokenExpiration.NoExpiration
-      );
-      if (this.mounted) {
-        this.setState({ tokenExpirationOptions });
-      }
+    const tokenExpirationOptions = await getAvailableExpirationOptions();
+    if (this.mounted) {
+      this.setState({ tokenExpirationOptions });
     }
   };
 
@@ -160,12 +123,6 @@ export class TokensForm extends React.PureComponent<Props, State> {
     }
   };
 
-  getExpirationDate = (days: number): string => {
-    const expirationDate = now();
-    expirationDate.setDate(expirationDate.getDate() + days);
-    return toShortNotSoISOString(expirationDate);
-  };
-
   handleGenerateToken = async (event: React.SyntheticEvent<HTMLFormElement>) => {
     event.preventDefault();
     const { login } = this.props;
@@ -184,7 +141,7 @@ export class TokensForm extends React.PureComponent<Props, State> {
         type: newTokenType,
         ...(newTokenType === TokenType.Project && { projectKey: selectedProject.key }),
         ...(newTokenExpiration !== TokenExpiration.NoExpiration && {
-          expirationDate: this.getExpirationDate(newTokenExpiration)
+          expirationDate: computeTokenExpirationDate(newTokenExpiration)
         })
       });
 
diff --git a/server/sonar-web/src/main/js/helpers/__tests__/tokens-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/tokens-test.ts
new file mode 100644 (file)
index 0000000..7f35313
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { getAllValues } from '../../api/settings';
+import { SettingsKey } from '../../types/settings';
+import { TokenExpiration } from '../../types/token';
+import { mockSettingValue } from '../mocks/settings';
+import {
+  computeTokenExpirationDate,
+  EXPIRATION_OPTIONS,
+  getAvailableExpirationOptions
+} from '../tokens';
+
+jest.mock('../../api/settings', () => {
+  return {
+    getAllValues: jest.fn().mockResolvedValue([])
+  };
+});
+
+jest.mock('../dates', () => {
+  return {
+    ...jest.requireActual('../dates'),
+    now: jest.fn(() => new Date('2022-06-01T12:00:00Z'))
+  };
+});
+
+describe('getAvailableExpirationOptions', () => {
+  it('should correctly return all options if no setting', async () => {
+    expect(await getAvailableExpirationOptions()).toEqual(EXPIRATION_OPTIONS);
+  });
+
+  it('should correctly return all options if the max setting is no expiration', async () => {
+    (getAllValues as jest.Mock).mockResolvedValueOnce([
+      mockSettingValue({ key: SettingsKey.TokenMaxAllowedLifetime, value: 'No expiration' })
+    ]);
+    expect(await getAvailableExpirationOptions()).toEqual(EXPIRATION_OPTIONS);
+  });
+
+  it('should correctly limit options if the max setting is 1 year', async () => {
+    (getAllValues as jest.Mock).mockResolvedValueOnce([
+      mockSettingValue({ key: SettingsKey.TokenMaxAllowedLifetime, value: '1 year' })
+    ]);
+    expect(await getAvailableExpirationOptions()).toEqual(
+      [TokenExpiration.OneMonth, TokenExpiration.ThreeMonths, TokenExpiration.OneYear].map(
+        value => {
+          return {
+            value,
+            label: `users.tokens.expiration.${value.toString()}`
+          };
+        }
+      )
+    );
+  });
+
+  it('should correctly limit options if the max setting is 3 months', async () => {
+    (getAllValues as jest.Mock).mockResolvedValueOnce([
+      mockSettingValue({ key: SettingsKey.TokenMaxAllowedLifetime, value: '90 days' })
+    ]);
+    expect(await getAvailableExpirationOptions()).toEqual(
+      [TokenExpiration.OneMonth, TokenExpiration.ThreeMonths].map(value => {
+        return {
+          value,
+          label: `users.tokens.expiration.${value.toString()}`
+        };
+      })
+    );
+  });
+
+  it('should correctly limit options if the max setting is 30 days', async () => {
+    (getAllValues as jest.Mock).mockResolvedValueOnce([
+      mockSettingValue({ key: SettingsKey.TokenMaxAllowedLifetime, value: '30 days' })
+    ]);
+    expect(await getAvailableExpirationOptions()).toEqual([
+      {
+        value: TokenExpiration.OneMonth,
+        label: `users.tokens.expiration.${TokenExpiration.OneMonth.toString()}`
+      }
+    ]);
+  });
+});
+
+describe('computeTokenExpirationDate', () => {
+  it.each([
+    [TokenExpiration.OneMonth, '2022-07-01'],
+    [TokenExpiration.ThreeMonths, '2022-08-30'],
+    [TokenExpiration.OneYear, '2023-06-01']
+  ])('should correctly compute the proper expiration date for %s days', (days, expected) => {
+    expect(computeTokenExpirationDate(days)).toBe(expected);
+  });
+});
diff --git a/server/sonar-web/src/main/js/helpers/tokens.ts b/server/sonar-web/src/main/js/helpers/tokens.ts
new file mode 100644 (file)
index 0000000..ab8f3c5
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { getAllValues } from '../api/settings';
+import { SettingsKey } from '../types/settings';
+import { TokenExpiration } from '../types/token';
+import { now, toShortNotSoISOString } from './dates';
+import { translate } from './l10n';
+
+export const EXPIRATION_OPTIONS = [
+  TokenExpiration.OneMonth,
+  TokenExpiration.ThreeMonths,
+  TokenExpiration.OneYear,
+  TokenExpiration.NoExpiration
+].map(value => {
+  return {
+    value,
+    label: translate('users.tokens.expiration', value.toString())
+  };
+});
+
+const SETTINGS_EXPIRATION_MAP: { [key: string]: TokenExpiration } = {
+  '30 days': TokenExpiration.OneMonth,
+  '90 days': TokenExpiration.ThreeMonths,
+  '1 year': TokenExpiration.OneYear,
+  'No expiration': TokenExpiration.NoExpiration
+};
+
+export async function getAvailableExpirationOptions() {
+  /*
+   * We intentionally fetch all settings, because fetching a specific setting will
+   * return it from the DB as a fallback, even if the setting is not defined at startup.
+   */
+  const setting = (await getAllValues()).find(v => v.key === SettingsKey.TokenMaxAllowedLifetime);
+  if (setting === undefined || setting.value === undefined) {
+    return EXPIRATION_OPTIONS;
+  }
+
+  const maxTokenLifetime = setting.value;
+  if (SETTINGS_EXPIRATION_MAP[maxTokenLifetime] !== TokenExpiration.NoExpiration) {
+    return EXPIRATION_OPTIONS.filter(
+      option =>
+        option.value <= SETTINGS_EXPIRATION_MAP[maxTokenLifetime] &&
+        option.value !== TokenExpiration.NoExpiration
+    );
+  }
+
+  return EXPIRATION_OPTIONS;
+}
+
+export function computeTokenExpirationDate(days: number) {
+  const expirationDate = now();
+  expirationDate.setDate(expirationDate.getDate() + days);
+  return toShortNotSoISOString(expirationDate);
+}