mirror of https://github.com/portainer/portainer
				
				
				
			chore(account): write tests for CreateAccessToken [EE-2561] (#6578)
							parent
							
								
									b7d18ef50f
								
							
						
					
					
						commit
						f1ea2b5c02
					
				| 
						 | 
				
			
			@ -0,0 +1,39 @@
 | 
			
		|||
import * as i18nextMocks from './i18next';
 | 
			
		||||
 | 
			
		||||
describe('mockT', () => {
 | 
			
		||||
  it('should return correctly with no arguments', async () => {
 | 
			
		||||
    const testText = `The company's new IT initiative, code named Phoenix Project, is critical to the
 | 
			
		||||
        future of Parts Unlimited, but the project is massively over budget and very late. The CEO wants
 | 
			
		||||
        Bill to report directly to him and fix the mess in ninety days or else Bill's entire department
 | 
			
		||||
        will be outsourced.`;
 | 
			
		||||
 | 
			
		||||
    const translatedText = i18nextMocks.mockT(testText);
 | 
			
		||||
 | 
			
		||||
    expect(translatedText).toBe(testText);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test.each`
 | 
			
		||||
    testText                            | args                                          | expectedText
 | 
			
		||||
    ${'{{fileName}} is invalid.'}       | ${{ fileName: 'example_5.csv' }}              | ${'example_5.csv is invalid.'}
 | 
			
		||||
    ${'{{fileName}} {is}.'}             | ${{ fileName: '   ' }}                        | ${'    {is}.'}
 | 
			
		||||
    ${'{{number}} of {{total}}'}        | ${{ number: 0, total: 999 }}                  | ${'0 of 999'}
 | 
			
		||||
    ${'There was an error:\n{{error}}'} | ${{ error: 'Failed' }}                        | ${'There was an error:\nFailed'}
 | 
			
		||||
    ${'Click:{{li}}{{li2}}{{li_3}}'}    | ${{ li: '', li2: 'https://', li_3: '!@#$%' }} | ${'Click:https://!@#$%'}
 | 
			
		||||
    ${'{{happy}}😏y✔{{sad}}{{laugh}}'}  | ${{ happy: '😃', sad: '😢', laugh: '🤣' }}    | ${'😃😏y✔😢🤣'}
 | 
			
		||||
  `(
 | 
			
		||||
    'should return correctly while handling arguments in different scenarios',
 | 
			
		||||
    ({ testText, args, expectedText }) => {
 | 
			
		||||
      const translatedText = i18nextMocks.mockT(testText, args);
 | 
			
		||||
 | 
			
		||||
      expect(translatedText).toBe(expectedText);
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('language', () => {
 | 
			
		||||
  it('should return language', async () => {
 | 
			
		||||
    const { language } = i18nextMocks.default;
 | 
			
		||||
 | 
			
		||||
    expect(language).toBe('en');
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,36 @@
 | 
			
		|||
function replaceBetween(
 | 
			
		||||
  startIndex: number,
 | 
			
		||||
  endIndex: number,
 | 
			
		||||
  original: string,
 | 
			
		||||
  insertion: string
 | 
			
		||||
) {
 | 
			
		||||
  const result =
 | 
			
		||||
    original.substring(0, startIndex) +
 | 
			
		||||
    insertion +
 | 
			
		||||
    original.substring(endIndex);
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function mockT(i18nKey: string, args?: Record<string, string>) {
 | 
			
		||||
  let key = i18nKey;
 | 
			
		||||
 | 
			
		||||
  while (key.includes('{{') && args) {
 | 
			
		||||
    const startIndex = key.indexOf('{{');
 | 
			
		||||
    const endIndex = key.indexOf('}}');
 | 
			
		||||
 | 
			
		||||
    const currentArg = key.substring(startIndex + 2, endIndex);
 | 
			
		||||
    const value = args[currentArg];
 | 
			
		||||
 | 
			
		||||
    key = replaceBetween(startIndex, endIndex + 2, key, value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return key;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const i18next: Record<string, unknown> = jest.createMockFromModule('i18next');
 | 
			
		||||
i18next.t = mockT;
 | 
			
		||||
i18next.language = 'en';
 | 
			
		||||
i18next.changeLanguage = () => new Promise(() => {});
 | 
			
		||||
i18next.use = () => i18next;
 | 
			
		||||
 | 
			
		||||
export default i18next;
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,16 @@
 | 
			
		|||
import { PropsWithChildren } from 'react';
 | 
			
		||||
 | 
			
		||||
import { mockT } from './i18next';
 | 
			
		||||
 | 
			
		||||
export function useTranslation() {
 | 
			
		||||
  return {
 | 
			
		||||
    t: mockT,
 | 
			
		||||
    i18n: {
 | 
			
		||||
      changeLanguage: () => new Promise(() => {}),
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function Trans({ children }: PropsWithChildren<unknown>) {
 | 
			
		||||
  return <>{children}</>;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -30,12 +30,12 @@ export function ReactExample({ text }: ReactExampleProps) {
 | 
			
		|||
      <div className={styles.redBg}>{text}</div>
 | 
			
		||||
      <div>
 | 
			
		||||
        <a href={href} onClick={onClick}>
 | 
			
		||||
          {t('reactExample.registries.useSref', 'Registries useSref')}
 | 
			
		||||
          {t('Registries useSref')}
 | 
			
		||||
        </a>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div>
 | 
			
		||||
        <Link to={route}>
 | 
			
		||||
          <Trans i18nKey="reactExample.registries.link">
 | 
			
		||||
          <Trans>
 | 
			
		||||
            Registries <strong>Link</strong>
 | 
			
		||||
          </Trans>
 | 
			
		||||
        </Link>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,54 @@
 | 
			
		|||
import userEvent from '@testing-library/user-event';
 | 
			
		||||
 | 
			
		||||
import { render } from '@/react-tools/test-utils';
 | 
			
		||||
 | 
			
		||||
import { CreateAccessToken } from './CreateAccessToken';
 | 
			
		||||
 | 
			
		||||
test('the button is disabled when description is missing and enabled when description is filled', async () => {
 | 
			
		||||
  const queries = renderComponent();
 | 
			
		||||
 | 
			
		||||
  const button = queries.getByRole('button', { name: 'Add access token' });
 | 
			
		||||
 | 
			
		||||
  expect(button).toBeDisabled();
 | 
			
		||||
 | 
			
		||||
  const descriptionField = queries.getByLabelText('Description');
 | 
			
		||||
 | 
			
		||||
  userEvent.type(descriptionField, 'description');
 | 
			
		||||
 | 
			
		||||
  expect(button).toBeEnabled();
 | 
			
		||||
 | 
			
		||||
  userEvent.clear(descriptionField);
 | 
			
		||||
 | 
			
		||||
  expect(button).toBeDisabled();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('once the button is clicked, the access token is generated and displayed', async () => {
 | 
			
		||||
  const token = 'a very long access token that should be displayed';
 | 
			
		||||
  const onSubmit = jest.fn(() => Promise.resolve({ rawAPIKey: token }));
 | 
			
		||||
 | 
			
		||||
  const queries = renderComponent(onSubmit);
 | 
			
		||||
 | 
			
		||||
  const descriptionField = queries.getByLabelText('Description');
 | 
			
		||||
 | 
			
		||||
  userEvent.type(descriptionField, 'description');
 | 
			
		||||
 | 
			
		||||
  const button = queries.getByRole('button', { name: 'Add access token' });
 | 
			
		||||
 | 
			
		||||
  userEvent.click(button);
 | 
			
		||||
 | 
			
		||||
  expect(onSubmit).toHaveBeenCalledWith('description');
 | 
			
		||||
  expect(onSubmit).toHaveBeenCalledTimes(1);
 | 
			
		||||
 | 
			
		||||
  await expect(queries.findByText('New access token')).resolves.toBeVisible();
 | 
			
		||||
  expect(queries.getByText(token)).toHaveTextContent(token);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function renderComponent(onSubmit = jest.fn()) {
 | 
			
		||||
  const queries = render(
 | 
			
		||||
    <CreateAccessToken onSubmit={onSubmit} onError={jest.fn()} />
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  expect(queries.getByLabelText('Description')).toBeVisible();
 | 
			
		||||
 | 
			
		||||
  return queries;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -18,9 +18,6 @@ interface AccessTokenResponse {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  // userId for whom the access token is generated for
 | 
			
		||||
  userId: number;
 | 
			
		||||
 | 
			
		||||
  // onSubmit dispatches a successful matomo analytics event
 | 
			
		||||
  onSubmit: (description: string) => Promise<AccessTokenResponse>;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -32,7 +29,8 @@ export function CreateAccessToken({
 | 
			
		|||
  onSubmit,
 | 
			
		||||
  onError,
 | 
			
		||||
}: PropsWithChildren<Props>) {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const translationNS = 'account.accessTokens.create';
 | 
			
		||||
  const { t } = useTranslation(translationNS);
 | 
			
		||||
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
  const [description, setDescription] = useState('');
 | 
			
		||||
| 
						 | 
				
			
			@ -42,12 +40,7 @@ export function CreateAccessToken({
 | 
			
		|||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (description.length === 0) {
 | 
			
		||||
      setErrorText(
 | 
			
		||||
        t(
 | 
			
		||||
          'users.access-tokens.create.form.description-field.error.required',
 | 
			
		||||
          'this field is required'
 | 
			
		||||
        )
 | 
			
		||||
      );
 | 
			
		||||
      setErrorText(t('this field is required'));
 | 
			
		||||
    } else setErrorText('');
 | 
			
		||||
  }, [description, t]);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -73,10 +66,7 @@ export function CreateAccessToken({
 | 
			
		|||
        <div>
 | 
			
		||||
          <FormControl
 | 
			
		||||
            inputId="input"
 | 
			
		||||
            label={t(
 | 
			
		||||
              'users.access-tokens.create.form.description-field.label',
 | 
			
		||||
              'Description'
 | 
			
		||||
            )}
 | 
			
		||||
            label={t('Description')}
 | 
			
		||||
            errors={errorText}
 | 
			
		||||
          >
 | 
			
		||||
            <Input
 | 
			
		||||
| 
						 | 
				
			
			@ -90,36 +80,30 @@ export function CreateAccessToken({
 | 
			
		|||
            onClick={() => generateAccessToken()}
 | 
			
		||||
            className={styles.addButton}
 | 
			
		||||
          >
 | 
			
		||||
            {t('users.access-tokens.create.add-button', 'Add access token')}
 | 
			
		||||
            {t('Add access token')}
 | 
			
		||||
          </Button>
 | 
			
		||||
        </div>
 | 
			
		||||
        {accessToken && (
 | 
			
		||||
          <>
 | 
			
		||||
            <FormSectionTitle>
 | 
			
		||||
              <Trans i18nKey="users.access-tokens.create.new-access-token.title">
 | 
			
		||||
                New access token
 | 
			
		||||
              </Trans>
 | 
			
		||||
              <Trans ns={translationNS}>New access token</Trans>
 | 
			
		||||
            </FormSectionTitle>
 | 
			
		||||
            <TextTip>
 | 
			
		||||
              <Trans i18nKey="users.access-tokens.create.new-access-token.explanation">
 | 
			
		||||
              <Trans ns={translationNS}>
 | 
			
		||||
                Please copy the new access token. You won't be able to view
 | 
			
		||||
                the token again.
 | 
			
		||||
              </Trans>
 | 
			
		||||
            </TextTip>
 | 
			
		||||
            <Code>{accessToken}</Code>
 | 
			
		||||
            <CopyButton copyText={accessToken} className={styles.copyButton}>
 | 
			
		||||
              <Trans i18nKey="users.access-tokens.create.new-access-token.copy-button">
 | 
			
		||||
                Copy access token
 | 
			
		||||
              </Trans>
 | 
			
		||||
              <Trans ns={translationNS}>Copy access token</Trans>
 | 
			
		||||
            </CopyButton>
 | 
			
		||||
            <hr />
 | 
			
		||||
            <Button
 | 
			
		||||
              type="button"
 | 
			
		||||
              onClick={() => router.stateService.go('portainer.account')}
 | 
			
		||||
            >
 | 
			
		||||
              <Trans i18nKey="users.access-tokens.create.done-button">
 | 
			
		||||
                Done
 | 
			
		||||
              </Trans>
 | 
			
		||||
              <Trans ns={translationNS}>Done</Trans>
 | 
			
		||||
            </Button>
 | 
			
		||||
          </>
 | 
			
		||||
        )}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
import i18n from 'i18next';
 | 
			
		||||
import { initReactI18next } from 'react-i18next';
 | 
			
		||||
 | 
			
		||||
import translation from '../../translations/en/translation.json';
 | 
			
		||||
 | 
			
		||||
i18n.use(initReactI18next).init({
 | 
			
		||||
  lng: 'en',
 | 
			
		||||
  fallbackLng: 'en',
 | 
			
		||||
 | 
			
		||||
  // have a common namespace used around the full app
 | 
			
		||||
  ns: ['translationsNS'],
 | 
			
		||||
  defaultNS: 'translationsNS',
 | 
			
		||||
 | 
			
		||||
  interpolation: {
 | 
			
		||||
    escapeValue: false,
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  resources: { en: { translationsNS: translation } },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default i18n;
 | 
			
		||||
| 
						 | 
				
			
			@ -24,6 +24,7 @@
 | 
			
		|||
    "strictNullChecks": true,
 | 
			
		||||
    "noUnusedLocals": true,
 | 
			
		||||
    "removeComments": true,
 | 
			
		||||
    "resolveJsonModule": true,
 | 
			
		||||
    // "sourceMap": true,
 | 
			
		||||
    "lib": ["dom", "dom.iterable", "esnext"],
 | 
			
		||||
    "paths": {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue