]);
}
+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,
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';
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'
beforeAll(() => {
tokenMock = new UserTokensMock();
- (getValues as jest.Mock).mockClear();
});
afterEach(() => {
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],
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';
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 = {
};
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 });
}
};
}
};
- 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;
type: newTokenType,
...(newTokenType === TokenType.Project && { projectKey: selectedProject.key }),
...(newTokenExpiration !== TokenExpiration.NoExpiration && {
- expirationDate: this.getExpirationDate(newTokenExpiration)
+ expirationDate: computeTokenExpirationDate(newTokenExpiration)
})
});
--- /dev/null
+/*
+ * 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);
+ });
+});
--- /dev/null
+/*
+ * 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);
+}