Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions cypress/support/commands.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
declare global {
namespace Cypress {
interface Chainable<Subject = unknown> {
/**
* Deep-compares two objects after normalising them with
* JSON.stringify/parse (removes proxies, undefined, symbols …).
*
* @example
* cy.wrap(actual).deepEqualJson(expected)
*/
deepEqualJson(expected: unknown): Chainable<Subject>;
}
}
}

export {};
9 changes: 8 additions & 1 deletion cypress/support/commands.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
import '@ui5/webcomponents-cypress-commands';
import "../../src/utils/i18n/i18n";
import '../../src/utils/i18n/i18n';

const toPlain = <T>(o: T): T => JSON.parse(JSON.stringify(o));

Cypress.Commands.add('deepEqualJson', { prevSubject: true }, (subject, expected) => {
expect(toPlain(subject)).to.deep.equal(toPlain(expected));
return subject;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { CreateManagedControlPlaneWizardContainer } from './CreateManagedControlPlaneWizardContainer.tsx';
import { useCreateManagedControlPlane } from '../../../hooks/useCreateManagedControlPlane.tsx';
import { CreateManagedControlPlaneType } from '../../../lib/api/types/crate/createManagedControlPlane.ts';
import { useAuthOnboarding } from '../../../spaces/onboarding/auth/AuthContextOnboarding.tsx';

describe('CreateManagedControlPlaneWizardContainer', () => {
let createMutationPayload: CreateManagedControlPlaneType | null = null;

const fakeUseCreateManagedControlPlane: typeof useCreateManagedControlPlane = () => ({
mutate: async (data: CreateManagedControlPlaneType): Promise<CreateManagedControlPlaneType> => {
createMutationPayload = data;
return data;
},
});
const fakeUseAuthOnboarding = (() => ({
user: {
email: 'name@domain.com',
},
})) as typeof useAuthOnboarding;

beforeEach(() => {
createMutationPayload = null;
});

it('creates a Managed Control Plane', () => {
cy.mount(
<CreateManagedControlPlaneWizardContainer
useCreateManagedControlPlane={fakeUseCreateManagedControlPlane}
useAuthOnboarding={fakeUseAuthOnboarding}
isOpen={true}
setIsOpen={() => {}}
/>,
);

const expMutationPayload: CreateManagedControlPlaneType = {
apiVersion: 'core.openmcp.cloud/v1alpha1',
kind: 'ManagedControlPlane',
metadata: {
name: 'some-text',
namespace: '--ws-',
annotations: {
'openmcp.cloud/display-name': '',
},
labels: {
'openmcp.cloud.sap/charging-target-type': '',
'openmcp.cloud.sap/charging-target': '',
},
},
spec: {
authentication: {
enableSystemIdentityProvider: true,
},
components: {
apiServer: {
type: 'GardenerDedicated',
},
},
authorization: {
roleBindings: [
{
role: 'admin',
subjects: [
{
kind: 'User',
name: 'openmcp:name@domain.com',
},
],
},
],
},
},
};

cy.get('#name').find(' input[id*="inner"]').type('some-text');
cy.get('ui5-button').contains('Next').click(); // navigate to Members
cy.get('ui5-button').contains('Next').click(); // navigate to Component Selection
cy.get('ui5-button').contains('Next').click(); // navigate to Summarize
cy.get('ui5-button').contains('Create').click();
cy.then(() => cy.wrap(createMutationPayload).deepEqualJson(expMutationPayload));
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {

import { SummarizeStep } from './SummarizeStep.tsx';
import { Trans, useTranslation } from 'react-i18next';
import { useAuthOnboarding } from '../../../spaces/onboarding/auth/AuthContextOnboarding.tsx';
import { useAuthOnboarding as _useAuthOnboarding } from '../../../spaces/onboarding/auth/AuthContextOnboarding.tsx';
import { ErrorDialog, ErrorDialogHandle } from '../../Shared/ErrorMessageBox.tsx';
import { CreateDialogProps } from '../../Dialogs/CreateWorkspaceDialogContainer.tsx';
import { createManagedControlPlaneSchema } from '../../../lib/api/validations/schemas.ts';
Expand Down Expand Up @@ -61,6 +61,7 @@ import { stringify } from 'yaml';
import { useComponentsSelectionData } from './useComponentsSelectionData.ts';
import { Infobox } from '../../Ui/Infobox/Infobox.tsx';
import styles from './CreateManagedControlPlaneWizardContainer.module.css';
import { useCreateManagedControlPlane as _useCreateManagedControlPlane } from '../../../hooks/useCreateManagedControlPlane.tsx';

type CreateManagedControlPlaneWizardContainerProps = {
isOpen: boolean;
Expand All @@ -73,6 +74,8 @@ type CreateManagedControlPlaneWizardContainerProps = {
initialData?: ManagedControlPlaneInterface;
isOnMcpPage?: boolean;
initialSection?: WizardStepType;
useCreateManagedControlPlane?: typeof _useCreateManagedControlPlane;
useAuthOnboarding?: typeof _useAuthOnboarding;
};

export type WizardStepType = 'metadata' | 'members' | 'componentSelection' | 'summarize' | 'success';
Expand All @@ -90,6 +93,8 @@ export const CreateManagedControlPlaneWizardContainer: FC<CreateManagedControlPl
initialData,
isOnMcpPage = false,
initialSection,
useCreateManagedControlPlane = _useCreateManagedControlPlane,
useAuthOnboarding = _useAuthOnboarding,
}) => {
const { t } = useTranslation();
const { user } = useAuthOnboarding();
Expand Down Expand Up @@ -216,6 +221,7 @@ export const CreateManagedControlPlaneWizardContainer: FC<CreateManagedControlPl
const { trigger } = useApiResourceMutation<CreateManagedControlPlaneType>(
CreateManagedControlPlaneResource(projectName, workspaceName),
);
const { mutate: createManagedControlPlane } = useCreateManagedControlPlane(projectName, workspaceName);
const { trigger: triggerUpdate } = useApiResourceMutation<CreateManagedControlPlaneType>(
UpdateManagedControlPlaneResource(projectName, workspaceName, initialData?.metadata?.name ?? ''),
undefined,
Expand Down Expand Up @@ -250,7 +256,7 @@ export const CreateManagedControlPlaneWizardContainer: FC<CreateManagedControlPl
),
);
} else {
await trigger(
await createManagedControlPlane(
CreateManagedControlPlane(
finalName,
`${projectName}--ws-${workspaceName}`,
Expand Down
115 changes: 115 additions & 0 deletions src/hooks/useCreateManagedControlPlane.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { act, renderHook } from '@testing-library/react';
import { useCreateManagedControlPlane } from './useCreateManagedControlPlane.tsx';
import { CreateManagedControlPlaneType } from '../lib/api/types/crate/createManagedControlPlane.ts';

import { describe, it, expect, vi, afterEach, Mock } from 'vitest';
import { assertNonNullish, assertString } from '../utils/test/vitest-utils.ts';

describe('useCreateManagedControlPlane', () => {
afterEach(() => {
vi.clearAllMocks();
});

it('should perform a valid request', async () => {
// ARRANGE
const mockData: CreateManagedControlPlaneType = {
apiVersion: 'core.openmcp.cloud/v1alpha1',
kind: 'ManagedControlPlane',
metadata: {
name: 'name',
namespace: 'project-projectName--ws-workspaceName',
annotations: {
'openmcp.cloud/display-name': 'display-name',
},
labels: {
'openmcp.cloud.sap/charging-target-type': 'BTP',
'openmcp.cloud.sap/charging-target': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
},
},
spec: {
authentication: {
enableSystemIdentityProvider: true,
},
components: {
externalSecretsOperator: {
version: '0.20.1',
},
flux: {
version: '2.16.2',
},
kyverno: {
version: '3.5.2',
},
btpServiceOperator: {
version: '0.9.2',
},
apiServer: {
type: 'GardenerDedicated',
},
crossplane: {
version: '1.19.0',
providers: [
{
name: 'provider-hana',
version: '0.2.0',
},
],
},
},
authorization: {
roleBindings: [
{
role: 'admin',
subjects: [
{
kind: 'User',
name: 'openmcp:user@domain.com',
},
],
},
],
},
},
};

const fetchMock: Mock<typeof fetch> = vi.fn();
fetchMock.mockResolvedValue({
ok: true,
status: 200,
json: vi.fn().mockResolvedValue({}),
} as unknown as Response);
global.fetch = fetchMock;

// ACT
const renderHookResult = renderHook(() => useCreateManagedControlPlane('projectName', 'workspaceName'));
const { mutate: create } = renderHookResult.result.current;

await act(async () => {
await create(mockData);
});

// ASSERT
expect(fetchMock).toHaveBeenCalledTimes(1);
const call = fetchMock.mock.calls[0];
const [url, init] = call;
assertNonNullish(init);
const { method, headers, body } = init;

expect(url).toContain(
'/api/onboarding/apis/core.openmcp.cloud/v1alpha1/namespaces/projectName--ws-workspaceName/managedcontrolplanes',
);

expect(method).toBe('POST');

expect(headers).toEqual(
expect.objectContaining({
'Content-Type': 'application/json',
'X-use-crate': 'true',
}),
);

assertString(body);
const parsedBody = JSON.parse(body);
expect(parsedBody).toEqual(mockData);
});
});
17 changes: 17 additions & 0 deletions src/hooks/useCreateManagedControlPlane.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {
CreateManagedControlPlaneResource,
CreateManagedControlPlaneType,
} from '../lib/api/types/crate/createManagedControlPlane.ts';
import { useApiResourceMutation } from '../lib/api/useApiResource.ts';

export function useCreateManagedControlPlane(projectName: string, workspaceName: string) {
const { trigger } = useApiResourceMutation<CreateManagedControlPlaneType>(
CreateManagedControlPlaneResource(projectName, workspaceName),
);

const mutate = async (data: CreateManagedControlPlaneType) => {
return trigger(data);
};

return { mutate };
}
18 changes: 18 additions & 0 deletions src/utils/test/vitest-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { expect } from 'vitest';

/**
* Asserts that `value` is neither `null` nor `undefined` (non-nullish).
* Narrows the type, so that subsequent type assertions (!) become unnecessary.
*/
export function assertNonNullish<T>(value: T): asserts value is NonNullable<T> {
expect(value).toBeDefined();
expect(value).not.toBeNull();
}

/**
* Asserts that `value` is a `string`.
* Narrows the type of `value` to `string` after this call.
*/
export function assertString(value: unknown): asserts value is string {
expect(typeof value).toBe('string');
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"noFallthroughCasesInSwitch": true,
"types": ["node", "cypress"]
},
"include": ["src", "cypress.d.ts"],
"include": ["src", "cypress.d.ts", "cypress/**/*.d.ts"],
"references": [
{
"path": "./tsconfig.node.json"
Expand Down
Loading