Skip to content

Commit c22e0ba

Browse files
test: create MCP (#340)
1 parent 1864449 commit c22e0ba

File tree

8 files changed

+264
-4
lines changed

8 files changed

+264
-4
lines changed

cypress/support/commands.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
declare global {
2+
namespace Cypress {
3+
interface Chainable<Subject = unknown> {
4+
/**
5+
* Deep-compares two objects after normalising them with
6+
* JSON.stringify/parse (removes proxies, undefined, symbols …).
7+
*
8+
* @example
9+
* cy.wrap(actual).deepEqualJson(expected)
10+
*/
11+
deepEqualJson(expected: unknown): Chainable<Subject>;
12+
}
13+
}
14+
}
15+
16+
export {};

cypress/support/commands.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,9 @@
11
import '@ui5/webcomponents-cypress-commands';
2-
import "../../src/utils/i18n/i18n";
2+
import '../../src/utils/i18n/i18n';
3+
4+
const toPlain = <T>(o: T): T => JSON.parse(JSON.stringify(o));
5+
6+
Cypress.Commands.add('deepEqualJson', { prevSubject: true }, (subject, expected) => {
7+
expect(toPlain(subject)).to.deep.equal(toPlain(expected));
8+
return subject;
9+
});
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { CreateManagedControlPlaneWizardContainer } from './CreateManagedControlPlaneWizardContainer.tsx';
2+
import { useCreateManagedControlPlane } from '../../../hooks/useCreateManagedControlPlane.tsx';
3+
import { CreateManagedControlPlaneType } from '../../../lib/api/types/crate/createManagedControlPlane.ts';
4+
import { useAuthOnboarding } from '../../../spaces/onboarding/auth/AuthContextOnboarding.tsx';
5+
6+
describe('CreateManagedControlPlaneWizardContainer', () => {
7+
let createMutationPayload: CreateManagedControlPlaneType | null = null;
8+
9+
const fakeUseCreateManagedControlPlane: typeof useCreateManagedControlPlane = () => ({
10+
mutate: async (data: CreateManagedControlPlaneType): Promise<CreateManagedControlPlaneType> => {
11+
createMutationPayload = data;
12+
return data;
13+
},
14+
});
15+
const fakeUseAuthOnboarding = (() => ({
16+
user: {
17+
email: 'name@domain.com',
18+
},
19+
})) as typeof useAuthOnboarding;
20+
21+
beforeEach(() => {
22+
createMutationPayload = null;
23+
});
24+
25+
it('creates a Managed Control Plane', () => {
26+
cy.mount(
27+
<CreateManagedControlPlaneWizardContainer
28+
useCreateManagedControlPlane={fakeUseCreateManagedControlPlane}
29+
useAuthOnboarding={fakeUseAuthOnboarding}
30+
isOpen={true}
31+
setIsOpen={() => {}}
32+
/>,
33+
);
34+
35+
const expMutationPayload: CreateManagedControlPlaneType = {
36+
apiVersion: 'core.openmcp.cloud/v1alpha1',
37+
kind: 'ManagedControlPlane',
38+
metadata: {
39+
name: 'some-text',
40+
namespace: '--ws-',
41+
annotations: {
42+
'openmcp.cloud/display-name': '',
43+
},
44+
labels: {
45+
'openmcp.cloud.sap/charging-target-type': '',
46+
'openmcp.cloud.sap/charging-target': '',
47+
},
48+
},
49+
spec: {
50+
authentication: {
51+
enableSystemIdentityProvider: true,
52+
},
53+
components: {
54+
apiServer: {
55+
type: 'GardenerDedicated',
56+
},
57+
},
58+
authorization: {
59+
roleBindings: [
60+
{
61+
role: 'admin',
62+
subjects: [
63+
{
64+
kind: 'User',
65+
name: 'openmcp:name@domain.com',
66+
},
67+
],
68+
},
69+
],
70+
},
71+
},
72+
};
73+
74+
cy.get('#name').find(' input[id*="inner"]').type('some-text');
75+
cy.get('ui5-button').contains('Next').click(); // navigate to Members
76+
cy.get('ui5-button').contains('Next').click(); // navigate to Component Selection
77+
cy.get('ui5-button').contains('Next').click(); // navigate to Summarize
78+
cy.get('ui5-button').contains('Create').click();
79+
cy.then(() => cy.wrap(createMutationPayload).deepEqualJson(expMutationPayload));
80+
});
81+
});

src/components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222

2323
import { SummarizeStep } from './SummarizeStep.tsx';
2424
import { Trans, useTranslation } from 'react-i18next';
25-
import { useAuthOnboarding } from '../../../spaces/onboarding/auth/AuthContextOnboarding.tsx';
25+
import { useAuthOnboarding as _useAuthOnboarding } from '../../../spaces/onboarding/auth/AuthContextOnboarding.tsx';
2626
import { ErrorDialog, ErrorDialogHandle } from '../../Shared/ErrorMessageBox.tsx';
2727
import { CreateDialogProps } from '../../Dialogs/CreateWorkspaceDialogContainer.tsx';
2828
import { createManagedControlPlaneSchema } from '../../../lib/api/validations/schemas.ts';
@@ -61,6 +61,7 @@ import { stringify } from 'yaml';
6161
import { useComponentsSelectionData } from './useComponentsSelectionData.ts';
6262
import { Infobox } from '../../Ui/Infobox/Infobox.tsx';
6363
import styles from './CreateManagedControlPlaneWizardContainer.module.css';
64+
import { useCreateManagedControlPlane as _useCreateManagedControlPlane } from '../../../hooks/useCreateManagedControlPlane.tsx';
6465

6566
type CreateManagedControlPlaneWizardContainerProps = {
6667
isOpen: boolean;
@@ -73,6 +74,8 @@ type CreateManagedControlPlaneWizardContainerProps = {
7374
initialData?: ManagedControlPlaneInterface;
7475
isOnMcpPage?: boolean;
7576
initialSection?: WizardStepType;
77+
useCreateManagedControlPlane?: typeof _useCreateManagedControlPlane;
78+
useAuthOnboarding?: typeof _useAuthOnboarding;
7679
};
7780

7881
export type WizardStepType = 'metadata' | 'members' | 'componentSelection' | 'summarize' | 'success';
@@ -90,6 +93,8 @@ export const CreateManagedControlPlaneWizardContainer: FC<CreateManagedControlPl
9093
initialData,
9194
isOnMcpPage = false,
9295
initialSection,
96+
useCreateManagedControlPlane = _useCreateManagedControlPlane,
97+
useAuthOnboarding = _useAuthOnboarding,
9398
}) => {
9499
const { t } = useTranslation();
95100
const { user } = useAuthOnboarding();
@@ -216,6 +221,7 @@ export const CreateManagedControlPlaneWizardContainer: FC<CreateManagedControlPl
216221
const { trigger } = useApiResourceMutation<CreateManagedControlPlaneType>(
217222
CreateManagedControlPlaneResource(projectName, workspaceName),
218223
);
224+
const { mutate: createManagedControlPlane } = useCreateManagedControlPlane(projectName, workspaceName);
219225
const { trigger: triggerUpdate } = useApiResourceMutation<CreateManagedControlPlaneType>(
220226
UpdateManagedControlPlaneResource(projectName, workspaceName, initialData?.metadata?.name ?? ''),
221227
undefined,
@@ -250,7 +256,7 @@ export const CreateManagedControlPlaneWizardContainer: FC<CreateManagedControlPl
250256
),
251257
);
252258
} else {
253-
await trigger(
259+
await createManagedControlPlane(
254260
CreateManagedControlPlane(
255261
finalName,
256262
`${projectName}--ws-${workspaceName}`,
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { act, renderHook } from '@testing-library/react';
2+
import { useCreateManagedControlPlane } from './useCreateManagedControlPlane.tsx';
3+
import { CreateManagedControlPlaneType } from '../lib/api/types/crate/createManagedControlPlane.ts';
4+
5+
import { describe, it, expect, vi, afterEach, Mock } from 'vitest';
6+
import { assertNonNullish, assertString } from '../utils/test/vitest-utils.ts';
7+
8+
describe('useCreateManagedControlPlane', () => {
9+
afterEach(() => {
10+
vi.clearAllMocks();
11+
});
12+
13+
it('should perform a valid request', async () => {
14+
// ARRANGE
15+
const mockData: CreateManagedControlPlaneType = {
16+
apiVersion: 'core.openmcp.cloud/v1alpha1',
17+
kind: 'ManagedControlPlane',
18+
metadata: {
19+
name: 'name',
20+
namespace: 'project-projectName--ws-workspaceName',
21+
annotations: {
22+
'openmcp.cloud/display-name': 'display-name',
23+
},
24+
labels: {
25+
'openmcp.cloud.sap/charging-target-type': 'BTP',
26+
'openmcp.cloud.sap/charging-target': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
27+
},
28+
},
29+
spec: {
30+
authentication: {
31+
enableSystemIdentityProvider: true,
32+
},
33+
components: {
34+
externalSecretsOperator: {
35+
version: '0.20.1',
36+
},
37+
flux: {
38+
version: '2.16.2',
39+
},
40+
kyverno: {
41+
version: '3.5.2',
42+
},
43+
btpServiceOperator: {
44+
version: '0.9.2',
45+
},
46+
apiServer: {
47+
type: 'GardenerDedicated',
48+
},
49+
crossplane: {
50+
version: '1.19.0',
51+
providers: [
52+
{
53+
name: 'provider-hana',
54+
version: '0.2.0',
55+
},
56+
],
57+
},
58+
},
59+
authorization: {
60+
roleBindings: [
61+
{
62+
role: 'admin',
63+
subjects: [
64+
{
65+
kind: 'User',
66+
name: 'openmcp:user@domain.com',
67+
},
68+
],
69+
},
70+
],
71+
},
72+
},
73+
};
74+
75+
const fetchMock: Mock<typeof fetch> = vi.fn();
76+
fetchMock.mockResolvedValue({
77+
ok: true,
78+
status: 200,
79+
json: vi.fn().mockResolvedValue({}),
80+
} as unknown as Response);
81+
global.fetch = fetchMock;
82+
83+
// ACT
84+
const renderHookResult = renderHook(() => useCreateManagedControlPlane('projectName', 'workspaceName'));
85+
const { mutate: create } = renderHookResult.result.current;
86+
87+
await act(async () => {
88+
await create(mockData);
89+
});
90+
91+
// ASSERT
92+
expect(fetchMock).toHaveBeenCalledTimes(1);
93+
const call = fetchMock.mock.calls[0];
94+
const [url, init] = call;
95+
assertNonNullish(init);
96+
const { method, headers, body } = init;
97+
98+
expect(url).toContain(
99+
'/api/onboarding/apis/core.openmcp.cloud/v1alpha1/namespaces/projectName--ws-workspaceName/managedcontrolplanes',
100+
);
101+
102+
expect(method).toBe('POST');
103+
104+
expect(headers).toEqual(
105+
expect.objectContaining({
106+
'Content-Type': 'application/json',
107+
'X-use-crate': 'true',
108+
}),
109+
);
110+
111+
assertString(body);
112+
const parsedBody = JSON.parse(body);
113+
expect(parsedBody).toEqual(mockData);
114+
});
115+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {
2+
CreateManagedControlPlaneResource,
3+
CreateManagedControlPlaneType,
4+
} from '../lib/api/types/crate/createManagedControlPlane.ts';
5+
import { useApiResourceMutation } from '../lib/api/useApiResource.ts';
6+
7+
export function useCreateManagedControlPlane(projectName: string, workspaceName: string) {
8+
const { trigger } = useApiResourceMutation<CreateManagedControlPlaneType>(
9+
CreateManagedControlPlaneResource(projectName, workspaceName),
10+
);
11+
12+
const mutate = async (data: CreateManagedControlPlaneType) => {
13+
return trigger(data);
14+
};
15+
16+
return { mutate };
17+
}

src/utils/test/vitest-utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { expect } from 'vitest';
2+
3+
/**
4+
* Asserts that `value` is neither `null` nor `undefined` (non-nullish).
5+
* Narrows the type, so that subsequent type assertions (!) become unnecessary.
6+
*/
7+
export function assertNonNullish<T>(value: T): asserts value is NonNullable<T> {
8+
expect(value).toBeDefined();
9+
expect(value).not.toBeNull();
10+
}
11+
12+
/**
13+
* Asserts that `value` is a `string`.
14+
* Narrows the type of `value` to `string` after this call.
15+
*/
16+
export function assertString(value: unknown): asserts value is string {
17+
expect(typeof value).toBe('string');
18+
}

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"noFallthroughCasesInSwitch": true,
2020
"types": ["node", "cypress"]
2121
},
22-
"include": ["src", "cypress.d.ts"],
22+
"include": ["src", "cypress.d.ts", "cypress/**/*.d.ts"],
2323
"references": [
2424
{
2525
"path": "./tsconfig.node.json"

0 commit comments

Comments
 (0)