diff --git a/src/components/ControlPlane/ActionsMenu.tsx b/src/components/ControlPlane/ActionsMenu.tsx index 7a911df0..5939dc47 100644 --- a/src/components/ControlPlane/ActionsMenu.tsx +++ b/src/components/ControlPlane/ActionsMenu.tsx @@ -14,10 +14,9 @@ export type ActionItem = { export type ActionsMenuProps = { item: T; actions: ActionItem[]; - buttonIcon?: string; }; -export function ActionsMenu({ item, actions, buttonIcon = 'overflow' }: ActionsMenuProps) { +export function ActionsMenu({ item, actions }: ActionsMenuProps) { const popoverRef = useRef(null); const [open, setOpen] = useState(false); @@ -30,10 +29,11 @@ export function ActionsMenu({ item, actions, buttonIcon = 'overflow' }: Actio return ( <> - @@ -141,10 +142,10 @@ export function YamlSidePanel({ resource, filename, onApply, isEdit }: YamlSideP

{t('yaml.diffConfirmMessage')}

- - diff --git a/src/components/YamlEditor/YamlEditor.tsx b/src/components/YamlEditor/YamlEditor.tsx index c55e4c66..1c8a2716 100644 --- a/src/components/YamlEditor/YamlEditor.tsx +++ b/src/components/YamlEditor/YamlEditor.tsx @@ -85,7 +85,7 @@ export const YamlEditor = (props: YamlEditorProps) => { {t('yaml.editorTitle')} - diff --git a/src/hooks/useHandleResourcePatch.spec.ts b/src/hooks/useHandleResourcePatch.spec.ts new file mode 100644 index 00000000..ae37062c --- /dev/null +++ b/src/hooks/useHandleResourcePatch.spec.ts @@ -0,0 +1,140 @@ +import { act, renderHook } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest'; +import { useHandleResourcePatch } from './useHandleResourcePatch.ts'; +import { fetchApiServerJson } from '../lib/api/fetch.ts'; +import { assertNonNullish } from '../utils/test/vitest-utils.ts'; + +vi.mock('../lib/api/fetch.ts'); + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +vi.mock('../context/ToastContext', () => ({ + useToast: () => ({ + show: vi.fn(), + }), +})); + +vi.mock('../hooks/useResourcePluralNames', () => ({ + useResourcePluralNames: () => ({ + getPluralKind: (kind: string) => `${kind.toLowerCase()}s`, + }), +})); + +describe('useHandleResourcePatch', () => { + let fetchMock: Mock; + const mockErrorDialogRef = { current: null }; + + beforeEach(() => { + fetchMock = vi.mocked(fetchApiServerJson); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should successfully patch a namespaced resource', async () => { + // ARRANGE + fetchMock.mockResolvedValue(undefined); + + const item = { + kind: 'Subaccount', + apiVersion: 'account.btp.sap.crossplane.io/v1alpha1', + metadata: { + name: 'test-subaccount', + namespace: 'test-namespace', + }, + }; + + const parsed = { spec: { updated: true } }; + + // ACT + const renderHookResult = renderHook(() => useHandleResourcePatch(mockErrorDialogRef)); + const handlePatch = renderHookResult.result.current; + + let success: boolean = false; + await act(async () => { + success = await handlePatch(item, parsed); + }); + + // ASSERT + expect(success).toBe(true); + expect(fetchMock).toHaveBeenCalledTimes(1); + + const call = fetchMock.mock.calls[0]; + assertNonNullish(call); + const [url, _config, _excludeMcpConfig, method, body] = call; + + expect(url).toBe( + '/apis/account.btp.sap.crossplane.io/v1alpha1/namespaces/test-namespace/subaccounts/test-subaccount', + ); + expect(method).toBe('PATCH'); + expect(body).toBe(JSON.stringify(parsed)); + }); + + it('should successfully patch a cluster-scoped resource', async () => { + // ARRANGE + fetchMock.mockResolvedValue(undefined); + + const item = { + kind: 'ClusterRole', + apiVersion: 'rbac.authorization.k8s.io/v1', + metadata: { + name: 'test-role', + }, + }; + + const parsed = { spec: { updated: true } }; + + // ACT + const renderHookResult = renderHook(() => useHandleResourcePatch(mockErrorDialogRef)); + const handlePatch = renderHookResult.result.current; + + let success: boolean = false; + await act(async () => { + success = await handlePatch(item, parsed); + }); + + // ASSERT + expect(success).toBe(true); + expect(fetchMock).toHaveBeenCalledTimes(1); + + const call = fetchMock.mock.calls[0]; + assertNonNullish(call); + const [url] = call; + + expect(url).toBe('/apis/rbac.authorization.k8s.io/v1/clusterroles/test-role'); + }); + + it('should handle patch failure', async () => { + // ARRANGE + fetchMock.mockRejectedValue(new Error('Network error')); + + const item = { + kind: 'Subaccount', + apiVersion: 'account.btp.sap.crossplane.io/v1alpha1', + metadata: { + name: 'test-subaccount', + namespace: 'test-namespace', + }, + }; + + const parsed = { spec: { updated: true } }; + + // ACT + const renderHookResult = renderHook(() => useHandleResourcePatch(mockErrorDialogRef)); + const handlePatch = renderHookResult.result.current; + + let success: boolean = true; + await act(async () => { + success = await handlePatch(item, parsed); + }); + + // ASSERT + expect(success).toBe(false); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/lib/api/types/crossplane/useHandleResourcePatch.ts b/src/hooks/useHandleResourcePatch.ts similarity index 80% rename from src/lib/api/types/crossplane/useHandleResourcePatch.ts rename to src/hooks/useHandleResourcePatch.ts index 856f8c9e..45250839 100644 --- a/src/lib/api/types/crossplane/useHandleResourcePatch.ts +++ b/src/hooks/useHandleResourcePatch.ts @@ -1,12 +1,12 @@ import { useContext } from 'react'; import type { RefObject } from 'react'; import { useTranslation } from 'react-i18next'; -import { ApiConfigContext } from '../../../../components/Shared/k8s'; -import { useToast } from '../../../../context/ToastContext.tsx'; -import { useResourcePluralNames } from '../../../../hooks/useResourcePluralNames'; -import { ErrorDialogHandle } from '../../../../components/Shared/ErrorMessageBox.tsx'; -import { fetchApiServerJson } from '../../fetch.ts'; -import { APIError } from '../../error.ts'; +import { ApiConfigContext } from '../components/Shared/k8s/index.ts'; +import { useToast } from '../context/ToastContext.tsx'; +import { useResourcePluralNames } from './useResourcePluralNames.ts'; +import { ErrorDialogHandle } from '../components/Shared/ErrorMessageBox.tsx'; +import { fetchApiServerJson } from '../lib/api/fetch.ts'; +import { APIError } from '../lib/api/error.ts'; export type PatchableResourceRef = { kind: string;