Repository: provectus/kafka-ui
Issue: Improve Sonar Cloud security hotspots metrics
Labels: type/bug
scope/frontend
status/accepted

**Describe the bug**
Fix security hotspots
https://sonarcloud.io/project/security_hotspots?id=provectus_kafka-ui_frontend&sinceLeakPeriod=true

**Expected behavior**
Security Hotspots Metric should be 100%

Issue comments:

Pull request review comments:
`@ts-expect-error first arg is number`
I think this is not the correct solution.
If the argument type expects a string, you must pass a string.
It was there before

Those mutes just to remove mutes from top of the file
```
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
```

I don't really know why they were here, but I guess it's because we get `GIT_TAG` from `process.env` so it can be `undefined`
👍 
👍 

W/e rewrote this part

Patch that resolved the issue:
diff --git a/kafka-ui-react-app/src/components/App.tsx b/kafka-ui-react-app/src/components/App.tsx
index 15cc54856d5..13e5fdef5ac 100644
--- a/kafka-ui-react-app/src/components/App.tsx
+++ b/kafka-ui-react-app/src/components/App.tsx
@@ -61,7 +61,7 @@ const App: React.FC = () => {
             <S.Hyperlink href="/ui">UI for Apache Kafka</S.Hyperlink>
 
             <S.NavbarItem>
-              <Version tag={GIT_TAG} commit={GIT_COMMIT} />
+              {GIT_TAG && <Version tag={GIT_TAG} commit={GIT_COMMIT} />}
             </S.NavbarItem>
           </S.NavbarBrand>
         </S.Navbar>
diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__tests__/CustomParamField.spec.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__tests__/CustomParamField.spec.tsx
index 5983684a732..0cc39496e82 100644
--- a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__tests__/CustomParamField.spec.tsx
+++ b/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__tests__/CustomParamField.spec.tsx
@@ -1,5 +1,5 @@
 import React from 'react';
-import {screen, waitFor, within} from '@testing-library/react';
+import { screen, waitFor, within } from '@testing-library/react';
 import { render } from 'lib/testHelpers';
 import CustomParamsField, {
   Props,
diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__tests__/CustomParams.spec.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__tests__/CustomParams.spec.tsx
index 9be012a0aa2..ded3e1b80f3 100644
--- a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__tests__/CustomParams.spec.tsx
+++ b/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__tests__/CustomParams.spec.tsx
@@ -9,8 +9,10 @@ import userEvent from '@testing-library/user-event';
 import { TOPIC_CUSTOM_PARAMS } from 'lib/constants';
 
 const selectOption = async (listbox: HTMLElement, option: string) => {
-  await waitFor(() => userEvent.click(listbox));
-  await waitFor(() => userEvent.click(screen.getByText(option)));
+  await waitFor(() => {
+    userEvent.click(listbox);
+    userEvent.click(screen.getByText(option));
+  });
 };
 
 const expectOptionIsSelected = (listbox: HTMLElement, option: string) => {
@@ -19,17 +21,27 @@ const expectOptionIsSelected = (listbox: HTMLElement, option: string) => {
   expect(selectedOption[0]).toHaveTextContent(option);
 };
 
-const expectOptionIsDisabled = async (
+const expectOptionAvailability = async (
   listbox: HTMLElement,
   option: string,
   disabled: boolean
 ) => {
   await waitFor(() => userEvent.click(listbox));
-  const selectedOption = within(listbox).getAllByText(option);
-  expect(selectedOption[1]).toHaveStyleRule(
+  const selectedOptions = within(listbox).getAllByText(option).reverse();
+  // its either two or one nodes, we only need last one
+  const selectedOption = selectedOptions[0];
+
+  if (disabled) {
+    expect(selectedOption).toHaveAttribute('disabled');
+  } else {
+    expect(selectedOption).toBeEnabled();
+  }
+
+  expect(selectedOption).toHaveStyleRule(
     'cursor',
     disabled ? 'not-allowed' : 'pointer'
   );
+  await waitFor(() => userEvent.click(listbox));
 };
 
 describe('CustomParams', () => {
@@ -51,15 +63,15 @@ describe('CustomParams', () => {
   });
 
   it('renders with props', () => {
-    const addParamButton = screen.getByRole('button');
-    expect(addParamButton).toBeInTheDocument();
-    expect(addParamButton).toHaveTextContent('Add Custom Parameter');
+    const button = screen.getByRole('button');
+    expect(button).toBeInTheDocument();
+    expect(button).toHaveTextContent('Add Custom Parameter');
   });
 
   describe('works with user inputs correctly', () => {
     it('button click creates custom param fieldset', async () => {
-      const addParamButton = screen.getByRole('button');
-      await waitFor(() => userEvent.click(addParamButton));
+      const button = screen.getByRole('button');
+      await waitFor(() => userEvent.click(button));
 
       const listbox = screen.getByRole('listbox');
       expect(listbox).toBeInTheDocument();
@@ -69,38 +81,38 @@ describe('CustomParams', () => {
     });
 
     it('can select option', async () => {
-      const addParamButton = screen.getByRole('button');
-      await waitFor(() => userEvent.click(addParamButton));
-
+      const button = screen.getByRole('button');
+      await waitFor(() => userEvent.click(button));
       const listbox = screen.getByRole('listbox');
 
       await selectOption(listbox, 'compression.type');
       expectOptionIsSelected(listbox, 'compression.type');
-      expectOptionIsDisabled(listbox, 'compression.type', true);
+      await expectOptionAvailability(listbox, 'compression.type', true);
 
       const textbox = screen.getByRole('textbox');
       expect(textbox).toHaveValue(TOPIC_CUSTOM_PARAMS['compression.type']);
     });
 
     it('when selected option changes disabled options update correctly', async () => {
-      const addParamButton = screen.getByRole('button');
-      await waitFor(() => userEvent.click(addParamButton));
+      const button = screen.getByRole('button');
+      await waitFor(() => userEvent.click(button));
 
       const listbox = screen.getByRole('listbox');
 
       await selectOption(listbox, 'compression.type');
-      expectOptionIsDisabled(listbox, 'compression.type', true);
+      expectOptionIsSelected(listbox, 'compression.type');
+      await expectOptionAvailability(listbox, 'compression.type', true);
 
       await selectOption(listbox, 'delete.retention.ms');
-      expectOptionIsDisabled(listbox, 'delete.retention.ms', true);
-      expectOptionIsDisabled(listbox, 'compression.type', false);
+      await expectOptionAvailability(listbox, 'delete.retention.ms', true);
+      await expectOptionAvailability(listbox, 'compression.type', false);
     });
 
     it('multiple button clicks create multiple fieldsets', async () => {
-      const addParamButton = screen.getByRole('button');
-      await waitFor(() => userEvent.click(addParamButton));
-      await waitFor(() => userEvent.click(addParamButton));
-      await waitFor(() => userEvent.click(addParamButton));
+      const button = screen.getByRole('button');
+      await waitFor(() => userEvent.click(button));
+      await waitFor(() => userEvent.click(button));
+      await waitFor(() => userEvent.click(button));
 
       const listboxes = screen.getAllByRole('listbox');
       expect(listboxes.length).toBe(3);
@@ -110,48 +122,64 @@ describe('CustomParams', () => {
     });
 
     it("can't select already selected option", async () => {
-      const addParamButton = screen.getByRole('button');
-      userEvent.click(addParamButton);
-      userEvent.click(addParamButton);
+      const button = screen.getByRole('button');
+      await waitFor(() => userEvent.click(button));
+      await waitFor(() => userEvent.click(button));
 
       const listboxes = screen.getAllByRole('listbox');
 
       const firstListbox = listboxes[0];
       await selectOption(firstListbox, 'compression.type');
-      expectOptionIsDisabled(firstListbox, 'compression.type', true);
+      await expectOptionAvailability(firstListbox, 'compression.type', true);
 
       const secondListbox = listboxes[1];
-      expectOptionIsDisabled(secondListbox, 'compression.type', true);
+      await expectOptionAvailability(secondListbox, 'compression.type', true);
     });
 
     it('when fieldset with selected custom property type is deleted disabled options update correctly', async () => {
-      const addParamButton = screen.getByRole('button');
-      userEvent.click(addParamButton);
-      userEvent.click(addParamButton);
-      userEvent.click(addParamButton);
+      const button = screen.getByRole('button');
+      await waitFor(() => userEvent.click(button));
+      await waitFor(() => userEvent.click(button));
+      await waitFor(() => userEvent.click(button));
 
       const listboxes = screen.getAllByRole('listbox');
 
       const firstListbox = listboxes[0];
       await selectOption(firstListbox, 'compression.type');
-      expectOptionIsDisabled(firstListbox, 'compression.type', true);
+      await expectOptionAvailability(firstListbox, 'compression.type', true);
 
       const secondListbox = listboxes[1];
       await selectOption(secondListbox, 'delete.retention.ms');
-      expectOptionIsDisabled(secondListbox, 'delete.retention.ms', true);
+      await expectOptionAvailability(
+        secondListbox,
+        'delete.retention.ms',
+        true
+      );
 
       const thirdListbox = listboxes[2];
       await selectOption(thirdListbox, 'file.delete.delay.ms');
-      expectOptionIsDisabled(thirdListbox, 'file.delete.delay.ms', true);
+      await expectOptionAvailability(
+        thirdListbox,
+        'file.delete.delay.ms',
+        true
+      );
 
       const deleteSecondFieldsetButton = screen.getByTitle(
         'Delete customParam field 1'
       );
-      userEvent.click(deleteSecondFieldsetButton);
+      await waitFor(() => userEvent.click(deleteSecondFieldsetButton));
       expect(secondListbox).not.toBeInTheDocument();
 
-      expectOptionIsDisabled(firstListbox, 'delete.retention.ms', false);
-      expectOptionIsDisabled(thirdListbox, 'delete.retention.ms', false);
+      await expectOptionAvailability(
+        firstListbox,
+        'delete.retention.ms',
+        false
+      );
+      await expectOptionAvailability(
+        thirdListbox,
+        'delete.retention.ms',
+        false
+      );
     });
   });
 });
diff --git a/kafka-ui-react-app/src/components/Version/Version.tsx b/kafka-ui-react-app/src/components/Version/Version.tsx
index 6fde9db8ecf..37acf181461 100644
--- a/kafka-ui-react-app/src/components/Version/Version.tsx
+++ b/kafka-ui-react-app/src/components/Version/Version.tsx
@@ -5,7 +5,7 @@ import { GIT_REPO_LATEST_RELEASE_LINK } from 'lib/constants';
 import compareVersions from './compareVersions';
 
 export interface VesionProps {
-  tag?: string;
+  tag: string;
   commit?: string;
 }
 
@@ -15,20 +15,15 @@ const Version: React.FC<VesionProps> = ({ tag, commit }) => {
     latestTag: '',
   });
   useEffect(() => {
-    if (tag) {
-      fetch(GIT_REPO_LATEST_RELEASE_LINK)
-        .then((response) => response.json())
-        .then((data) => {
-          setLatestVersionInfo({
-            outdated: compareVersions(tag, data.tag_name) === -1,
-            latestTag: data.tag_name,
-          });
+    fetch(GIT_REPO_LATEST_RELEASE_LINK)
+      .then((response) => response.json())
+      .then((data) => {
+        setLatestVersionInfo({
+          outdated: compareVersions(tag, data.tag_name) === -1,
+          latestTag: data.tag_name,
         });
-    }
+      });
   }, [tag]);
-  if (!tag) {
-    return null;
-  }
 
   const { outdated, latestTag } = latestVersionInfo;
 
diff --git a/kafka-ui-react-app/src/components/Version/__tests__/Version.spec.tsx b/kafka-ui-react-app/src/components/Version/__tests__/Version.spec.tsx
index 8215bd8d275..eebe8636fb5 100644
--- a/kafka-ui-react-app/src/components/Version/__tests__/Version.spec.tsx
+++ b/kafka-ui-react-app/src/components/Version/__tests__/Version.spec.tsx
@@ -1,29 +1,22 @@
 import React from 'react';
-import { mount } from 'enzyme';
-import Version from 'components/Version/Version';
+import Version, { VesionProps } from 'components/Version/Version';
+import { screen } from '@testing-library/react';
+import { render } from 'lib/testHelpers';
 
 const tag = 'v1.0.1-SHAPSHOT';
 const commit = '123sdf34';
 
 describe('Version', () => {
-  it('shows nothing if tag is not defined', () => {
-    const component = mount(<Version />);
-    expect(component.html()).toEqual(null);
-  });
+  const setupComponent = (props: VesionProps) => render(<Version {...props} />);
 
-  it('shows current tag when only tag is defined', () => {
-    const component = mount(<Version tag={tag} />);
-    expect(component.text()).toContain(tag);
+  it('renders', () => {
+    setupComponent({ tag });
+    expect(screen.getByText('Version:')).toBeInTheDocument();
   });
 
   it('shows current tag and commit', () => {
-    const component = mount(<Version tag={tag} commit={commit} />);
-    expect(component.text()).toContain(tag);
-    expect(component.text()).toContain(commit);
-  });
-
-  it('matches snapshot', () => {
-    const component = mount(<Version tag={tag} commit={commit} />);
-    expect(component).toMatchSnapshot();
+    setupComponent({ tag, commit });
+    expect(screen.getByText(tag)).toBeInTheDocument();
+    expect(screen.getByText(commit)).toBeInTheDocument();
   });
 });
diff --git a/kafka-ui-react-app/src/components/Version/__tests__/__snapshots__/Version.spec.tsx.snap b/kafka-ui-react-app/src/components/Version/__tests__/__snapshots__/Version.spec.tsx.snap
deleted file mode 100644
index 92926af577a..00000000000
--- a/kafka-ui-react-app/src/components/Version/__tests__/__snapshots__/Version.spec.tsx.snap
+++ /dev/null
@@ -1,36 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Version matches snapshot 1`] = `
-<Version
-  commit="123sdf34"
-  tag="v1.0.1-SHAPSHOT"
->
-  <div
-    className="is-size-8 has-text-grey"
-  >
-    <span
-      className="has-text-grey-light mr-1"
-    >
-      Version:
-    </span>
-    <span
-      className="mr-1"
-    >
-      v1.0.1-SHAPSHOT
-    </span>
-    <span>
-      (
-    </span>
-    <a
-      href="https://github.com/provectus/kafka-ui/commit/123sdf34"
-      target="__blank"
-      title="Current commit"
-    >
-      123sdf34
-    </a>
-    <span>
-      )
-    </span>
-  </div>
-</Version>
-`;
diff --git a/kafka-ui-react-app/src/components/Version/__tests__/compareVersions.spec.ts b/kafka-ui-react-app/src/components/Version/__tests__/compareVersions.spec.ts
index 083ba825709..9875255555f 100644
--- a/kafka-ui-react-app/src/components/Version/__tests__/compareVersions.spec.ts
+++ b/kafka-ui-react-app/src/components/Version/__tests__/compareVersions.spec.ts
@@ -1,6 +1,3 @@
-// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-// @ts-nocheck
-
 import compareVersions from 'components/Version/compareVersions';
 
 const runTests = (dataSet: [string, string, number][]) => {
@@ -63,10 +60,11 @@ describe('compareVersions function', () => {
   });
 
   it('returns valid result (negative test cases)', () => {
+    expect(compareVersions()).toEqual(0);
+    expect(compareVersions('v0.0.0')).toEqual(0);
+    // @ts-expect-error first arg is number
     expect(compareVersions(123, 'v0.0.0')).toEqual(0);
-    expect(compareVersions(undefined, 'v0.0.0')).toEqual(0);
+    // @ts-expect-error second arg is number
     expect(compareVersions('v0.0.0', 123)).toEqual(0);
-    expect(compareVersions('v0.0.0', undefined)).toEqual(0);
-    expect(compareVersions(undefined, undefined)).toEqual(0);
   });
 });
diff --git a/kafka-ui-react-app/src/components/Version/compareVersions.ts b/kafka-ui-react-app/src/components/Version/compareVersions.ts
index 72a8db926f7..71b3e11da14 100644
--- a/kafka-ui-react-app/src/components/Version/compareVersions.ts
+++ b/kafka-ui-react-app/src/components/Version/compareVersions.ts
@@ -1,9 +1,12 @@
 const split = (v: string): string[] => {
-  const c = v.replace(/^v/, '').replace(/\+.*$/, '');
-  return c.split('-')[0].split('.');
+  const c = v.replace('v', '').split('-')[0];
+  return c.split('.');
 };
 
-const compareVersions = (v1: string, v2: string): number => {
+const compareVersions = (v1?: string, v2?: string): number => {
+  if (!v1 || !v2) return 0;
+
+  // try..catch - is our safeguard for strange git tags (or usecases without network)
   try {
     const s1 = split(v1);
     const s2 = split(v2);
diff --git a/kafka-ui-react-app/src/redux/reducers/alerts/reducer.ts b/kafka-ui-react-app/src/redux/reducers/alerts/reducer.ts
index 5e75f1023b7..6f98a1b65fe 100644
--- a/kafka-ui-react-app/src/redux/reducers/alerts/reducer.ts
+++ b/kafka-ui-react-app/src/redux/reducers/alerts/reducer.ts
@@ -9,8 +9,7 @@ export const initialState: AlertsState = {};
 const reducer = (state = initialState, action: Action): AlertsState => {
   const { type } = action;
 
-  const matches = /(.*)__(FAILURE)$/.exec(type);
-  if (matches && matches[2]) return addError(state, action);
+  if (type.endsWith('__FAILURE')) return addError(state, action);
 
   if (type === getType(dismissAlert)) {
     return removeAlert(state, action);
diff --git a/kafka-ui-react-app/src/redux/reducers/loader/reducer.ts b/kafka-ui-react-app/src/redux/reducers/loader/reducer.ts
index 18197eb2f41..0d14d5e2bf1 100644
--- a/kafka-ui-react-app/src/redux/reducers/loader/reducer.ts
+++ b/kafka-ui-react-app/src/redux/reducers/loader/reducer.ts
@@ -4,12 +4,17 @@ export const initialState: LoaderState = {};
 
 const reducer = (state = initialState, action: Action): LoaderState => {
   const { type } = action;
-  const matches = /(.*)__(REQUEST|SUCCESS|FAILURE|CANCEL)$/.exec(type);
+  const splitType = type.split('__');
+  const requestState = splitType.pop();
+  const requestName = splitType.join('__');
 
   // not a *__REQUEST / *__SUCCESS /  *__FAILURE /  *__CANCEL actions, so we ignore them
-  if (!matches) return state;
-
-  const [, requestName, requestState] = matches;
+  if (
+    requestState &&
+    !['REQUEST', 'SUCCESS', 'FAILURE', 'CANCEL'].includes(requestState)
+  ) {
+    return state;
+  }
 
   switch (requestState) {
     case 'REQUEST':


Test patch: