From eb66c6bf93794101b91d64b80cebeae8a917bb94 Mon Sep 17 00:00:00 2001 From: Vikram Raj Date: Tue, 26 Aug 2025 17:49:49 +0530 Subject: [PATCH 1/3] import createHash from crypto-browserify --- .../console-shared/src/hooks/useGetUserSettingConfigMap.ts | 2 +- .../integration-tests/features/e2e/add-flow-ci.feature | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/packages/console-shared/src/hooks/useGetUserSettingConfigMap.ts b/frontend/packages/console-shared/src/hooks/useGetUserSettingConfigMap.ts index a0ebde410bc..620777c45ff 100644 --- a/frontend/packages/console-shared/src/hooks/useGetUserSettingConfigMap.ts +++ b/frontend/packages/console-shared/src/hooks/useGetUserSettingConfigMap.ts @@ -1,5 +1,5 @@ -import { createHash } from 'crypto'; import { useMemo } from 'react'; +import { createHash } from 'crypto-browserify'; import { useSelector } from 'react-redux'; import { getImpersonate, getUser, K8sResourceKind } from '@console/dynamic-plugin-sdk/src'; import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook'; diff --git a/frontend/packages/dev-console/integration-tests/features/e2e/add-flow-ci.feature b/frontend/packages/dev-console/integration-tests/features/e2e/add-flow-ci.feature index 0c79b48c2a0..ef7b4d16178 100644 --- a/frontend/packages/dev-console/integration-tests/features/e2e/add-flow-ci.feature +++ b/frontend/packages/dev-console/integration-tests/features/e2e/add-flow-ci.feature @@ -34,7 +34,6 @@ Feature: Create the different workloads from Add page | Middleware | Apache HTTP Server | httpd-example | | Other | Nginx HTTP server and a reverse proxy | nginx-example | - @broken-test Scenario Outline: Deploy image with Runtime icon from external registry: A-02-TC02 Given user is at Deploy Image page When user enters Image name from external registry as "" @@ -83,7 +82,6 @@ Feature: Create the different workloads from Add page | image_name | custom_icon | name | | ghcr.io/logonoff/fortune-cowsay-motd:latest | https://i.imgur.com/cxiObse.jpeg | fortune-cowsay-motd | - @broken-test Scenario: Edit Runtime Icon while Editing Image: A-02-TC05 Given user has deployed container Image "openshift/hello-openshift" from external registry And user is at Topology page @@ -165,7 +163,7 @@ Feature: Create the different workloads from Add page @regression Scenario: Quick Starts page when Quick Start has completed: QS-03-TC03 - When user selects QuickStarts from the help menu icon on the masthead + When user selects QuickStarts from the help menu icon on the masthead And user has completed "Get started with a sample application" Quick Start Then user can see time taken to complete the "Get started with a sample application" tour on the card And user can see Complete label on "Get started with a sample application" card From cb5361690a992fca1daddd861e583c7720bfb114 Mon Sep 17 00:00:00 2001 From: Vikram Raj Date: Thu, 4 Sep 2025 19:57:39 +0530 Subject: [PATCH 2/3] use window.crypto --- .../useGetUserSettingConfigMap.spec.ts | 215 ++++++++++++++++++ .../src/hooks/useGetUserSettingConfigMap.ts | 40 +++- 2 files changed, 245 insertions(+), 10 deletions(-) create mode 100644 frontend/packages/console-shared/src/hooks/__tests__/useGetUserSettingConfigMap.spec.ts diff --git a/frontend/packages/console-shared/src/hooks/__tests__/useGetUserSettingConfigMap.spec.ts b/frontend/packages/console-shared/src/hooks/__tests__/useGetUserSettingConfigMap.spec.ts new file mode 100644 index 00000000000..fbfea7dee7e --- /dev/null +++ b/frontend/packages/console-shared/src/hooks/__tests__/useGetUserSettingConfigMap.spec.ts @@ -0,0 +1,215 @@ +import { useSelector } from 'react-redux'; +import { K8sResourceKind } from '@console/dynamic-plugin-sdk/src'; +import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook'; +import { ConfigMapModel } from '@console/internal/models'; +import { testHook } from '@console/shared/src/test-utils/hooks-utils'; +import { USER_SETTING_CONFIGMAP_NAMESPACE } from '../../utils/user-settings'; +import { useGetUserSettingConfigMap } from '../useGetUserSettingConfigMap'; + +// Mock dependencies +const useK8sWatchResourceMock = useK8sWatchResource as jest.Mock; +const useSelectorMock = useSelector as jest.Mock; + +jest.mock('@console/internal/components/utils/k8s-watch-hook', () => ({ + useK8sWatchResource: jest.fn(), +})); + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: jest.fn(), +})); + +describe('useGetUserSettingConfigMap', () => { + const mockConfigMapData: K8sResourceKind = { + apiVersion: 'v1', + kind: 'ConfigMap', + metadata: { + name: 'user-settings-test-uid', + namespace: USER_SETTING_CONFIGMAP_NAMESPACE, + }, + data: { + 'test.key': 'test value', + }, + }; + + beforeEach(() => { + jest.resetAllMocks(); + + // Default mock implementation for useK8sWatchResource + useK8sWatchResourceMock.mockImplementation((resource) => { + if (!resource) { + return [null, true, null]; + } + return [mockConfigMapData, true, null]; + }); + }); + + describe('user identification scenarios', () => { + it('should use impersonated user name when present', () => { + const impersonateUserInfo = { + impersonateName: 'impersonate-user', + uid: 'test-uid', + username: 'test-username', + }; + + useSelectorMock.mockReturnValue(impersonateUserInfo); + useK8sWatchResourceMock.mockReturnValue([mockConfigMapData, true, null]); + + const { result } = testHook(() => useGetUserSettingConfigMap()); + + // Should use impersonate name directly and return config map data + expect(result.current).toEqual([mockConfigMapData, true, null]); + + // Verify the correct resource spec was passed to useK8sWatchResource + expect(useK8sWatchResourceMock).toHaveBeenCalledWith({ + kind: ConfigMapModel.kind, + namespace: USER_SETTING_CONFIGMAP_NAMESPACE, + isList: false, + name: 'user-settings-impersonate-user', + }); + }); + + it('should use uid when no impersonation and uid is available', () => { + const userInfoWithUid = { + impersonateName: null, + uid: 'test-uid-123', + username: 'test-username', + }; + + useSelectorMock.mockReturnValue(userInfoWithUid); + useK8sWatchResourceMock.mockReturnValue([mockConfigMapData, true, null]); + + const { result } = testHook(() => useGetUserSettingConfigMap()); + + // Should use uid directly and return config map data + expect(result.current).toEqual([mockConfigMapData, true, null]); + + // Verify the correct resource spec was passed + expect(useK8sWatchResourceMock).toHaveBeenCalledWith({ + kind: ConfigMapModel.kind, + namespace: USER_SETTING_CONFIGMAP_NAMESPACE, + isList: false, + name: 'user-settings-test-uid-123', + }); + }); + + it('should handle empty user info gracefully', () => { + const emptyUserInfo = { + impersonateName: null, + uid: null, + username: null, + }; + + useSelectorMock.mockReturnValue(emptyUserInfo); + + const { result } = testHook(() => useGetUserSettingConfigMap()); + + // Should return null config map resource when no user identifier + expect(result.current).toEqual([null, true, null]); + + // Verify null resource was passed to useK8sWatchResource + expect(useK8sWatchResourceMock).toHaveBeenCalledWith(null); + }); + + it('should handle username-only scenario', () => { + const userInfoWithUsername = { + impersonateName: null, + uid: null, + username: 'test-username', + }; + + useSelectorMock.mockReturnValue(userInfoWithUsername); + + const { result } = testHook(() => useGetUserSettingConfigMap()); + + // Initially, should return null since hashing is async + expect(result.current[0]).toBeNull(); + }); + }); + + describe('integration with useK8sWatchResource', () => { + it('should pass through loading state', () => { + const userInfoWithUid = { + impersonateName: null, + uid: 'test-uid', + username: 'test-username', + }; + + useSelectorMock.mockReturnValue(userInfoWithUid); + useK8sWatchResourceMock.mockReturnValue([null, false, null]); + + const { result } = testHook(() => useGetUserSettingConfigMap()); + + expect(result.current).toEqual([null, false, null]); + }); + + it('should pass through error state', () => { + const userInfoWithUid = { + impersonateName: null, + uid: 'test-uid', + username: 'test-username', + }; + + const error = new Error('K8s API error'); + useSelectorMock.mockReturnValue(userInfoWithUid); + useK8sWatchResourceMock.mockReturnValue([null, false, error]); + + const { result } = testHook(() => useGetUserSettingConfigMap()); + + expect(result.current).toEqual([null, false, error]); + }); + + it('should pass through successful data', () => { + const userInfoWithUid = { + impersonateName: null, + uid: 'test-uid', + username: 'test-username', + }; + + useSelectorMock.mockReturnValue(userInfoWithUid); + useK8sWatchResourceMock.mockReturnValue([mockConfigMapData, true, null]); + + const { result } = testHook(() => useGetUserSettingConfigMap()); + + expect(result.current).toEqual([mockConfigMapData, true, null]); + }); + + it('should create ConfigMap resource with correct parameters', () => { + const userInfo = { + impersonateName: null, + uid: 'test-uid-456', + username: 'test-user', + }; + + useSelectorMock.mockReturnValue(userInfo); + useK8sWatchResourceMock.mockReturnValue([mockConfigMapData, true, null]); + + testHook(() => useGetUserSettingConfigMap()); + + // Verify useK8sWatchResource was called with correct resource spec + expect(useK8sWatchResourceMock).toHaveBeenCalledWith({ + kind: ConfigMapModel.kind, + namespace: USER_SETTING_CONFIGMAP_NAMESPACE, + isList: false, + name: 'user-settings-test-uid-456', + }); + }); + + it('should use null resource when no user identifier is available', () => { + const emptyUserInfo = { + impersonateName: null, + uid: null, + username: '', + }; + + useSelectorMock.mockReturnValue(emptyUserInfo); + useK8sWatchResourceMock.mockReturnValue([null, true, null]); + + const { result } = testHook(() => useGetUserSettingConfigMap()); + + // Should call useK8sWatchResource with null + expect(useK8sWatchResourceMock).toHaveBeenCalledWith(null); + expect(result.current).toEqual([null, true, null]); + }); + }); +}); diff --git a/frontend/packages/console-shared/src/hooks/useGetUserSettingConfigMap.ts b/frontend/packages/console-shared/src/hooks/useGetUserSettingConfigMap.ts index 620777c45ff..a9d3e7e9dec 100644 --- a/frontend/packages/console-shared/src/hooks/useGetUserSettingConfigMap.ts +++ b/frontend/packages/console-shared/src/hooks/useGetUserSettingConfigMap.ts @@ -1,5 +1,4 @@ -import { useMemo } from 'react'; -import { createHash } from 'crypto-browserify'; +import { useMemo, useState, useEffect } from 'react'; import { useSelector } from 'react-redux'; import { getImpersonate, getUser, K8sResourceKind } from '@console/dynamic-plugin-sdk/src'; import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook'; @@ -8,7 +7,9 @@ import { RootState } from '@console/internal/redux'; import { USER_SETTING_CONFIGMAP_NAMESPACE } from '../utils/user-settings'; export const useGetUserSettingConfigMap = () => { - const hashNameOrKubeadmin = (name: string): string | null => { + const [hashedUsername, setHashedUsername] = useState(null); + + const hashNameOrKubeadmin = async (name: string): Promise => { if (!name) { return null; } @@ -16,18 +17,37 @@ export const useGetUserSettingConfigMap = () => { if (name === 'kube:admin') { return 'kubeadmin'; } - const hash = createHash('sha256'); - hash.update(name); - return hash.digest('hex'); + + const encoder = new TextEncoder(); + const data = encoder.encode(name); + const hashBuffer = await window.crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); }; - // User and impersonate - const userUid = useSelector((state: RootState) => { + + // User and impersonate info + const userInfo = useSelector((state: RootState) => { const impersonateName = getImpersonate(state)?.name; const { uid, username } = getUser(state) ?? {}; - const hashName = hashNameOrKubeadmin(username); - return impersonateName || uid || hashName || ''; + return { impersonateName, uid, username }; }); + // Hash the username asynchronously + useEffect(() => { + if (userInfo.username) { + hashNameOrKubeadmin(userInfo.username) + .then(setHashedUsername) + .catch(() => { + setHashedUsername(null); + }); + } else { + setHashedUsername(null); + } + }, [userInfo.username]); + + // Compute the final user UID + const userUid = userInfo.impersonateName || userInfo.uid || hashedUsername || ''; + const configMapResource = useMemo( () => !userUid From df3895c4b305479e7a85bfeb67e262496b5f181f Mon Sep 17 00:00:00 2001 From: Vikram Raj Date: Wed, 15 Oct 2025 14:24:30 +0530 Subject: [PATCH 3/3] fix e2e add-flow-ci.feature --- .../integration-tests/support/pages/add-flow/git-page.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/packages/dev-console/integration-tests/support/pages/add-flow/git-page.ts b/frontend/packages/dev-console/integration-tests/support/pages/add-flow/git-page.ts index 0f6268cf8b8..a96721c8e9c 100644 --- a/frontend/packages/dev-console/integration-tests/support/pages/add-flow/git-page.ts +++ b/frontend/packages/dev-console/integration-tests/support/pages/add-flow/git-page.ts @@ -85,7 +85,7 @@ export const gitPage = { cy.get(gitPO.appName).click(); cy.get('[data-test="console-select-search-input"]').type(appName); cy.get('[data-test="console-select-menu-list"]').then(($el) => { - if ($el.find('[data-test="console-select-item"]').length === 0) { + if ($el.find(`[data-test-dropdown-menu="${appName}"]`).length === 0) { cy.byTestDropDownMenu('#CREATE_APPLICATION_KEY#').click(); cy.get('#form-input-application-name-field') .clear()