Skip to content

Commit d01c5f6

Browse files
committed
test for controller transfer network
Resolves: MTV-3649 Signed-off-by: Pedro Abreu <[email protected]>
1 parent 7c0056a commit d01c5f6

File tree

14 files changed

+611
-4
lines changed

14 files changed

+611
-4
lines changed

src/overview/tabs/Settings/components/ControllerTransferNetwork/EditControllerTransferNetwork.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ const EditControllerTransferNetwork: FC = () => {
7070
blankOption={{
7171
name: t('None'),
7272
}}
73+
testId="controller-transfer-network-select"
7374
/>
7475
)}
7576
/>

src/overview/tabs/Settings/components/SettingsCard.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ const SettingsCard: FC<SettingsCardProps> = ({ obj }) => {
5353
}}
5454
className="pf-v6-u-mb-md"
5555
headingLevel="h3"
56+
data-testid="settings-edit-button"
5657
/>
5758
<DescriptionList>
5859
<DetailsItem
@@ -107,6 +108,7 @@ const SettingsCard: FC<SettingsCardProps> = ({ obj }) => {
107108
helpContent={<SnapshotPoolingIntervalHelpContent />}
108109
/>
109110
<DetailsItem
111+
testId="settings-controller-transfer-network"
110112
content={
111113
spec?.[SettingsFields.ControllerTransferNetwork] ??
112114
defaultValuesMap[SettingsFields.ControllerTransferNetwork]

src/overview/tabs/Settings/components/SettingsEdit.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ const SettingsEdit: ModalComponent<SettingsEditProps> = ({ closeModal, controlle
7272
closeModal={closeModal}
7373
variant={ModalVariant.medium}
7474
isDisabled={!isDirty}
75+
testId="settings-edit-modal"
7576
>
7677
<Form>
7778
{t(

src/overview/tabs/Settings/components/SettingsSelectInput.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,15 @@ type BlankOption = {
3838
* @property {(value: string) => void} onChange - Function to call when the value changes
3939
* @property {Option[]} options - The options to present to the user
4040
* @property {BlankOption} [blankOption] - Optional blank option that passes an empty value when selected
41+
* @property {string} [testId] - Test ID for the select component
4142
*/
4243
type SettingsSelectInputProps = {
4344
value: number | string;
4445
onChange: (value: number | string) => void;
4546
options: Option[];
4647
blankOption?: BlankOption;
4748
showKeyAsSelected?: boolean; // a flag to show selected value that's based on option key and not name
49+
testId?: string;
4850
};
4951

5052
const BLANK_OPTION_KEY = '__blank__';
@@ -57,6 +59,7 @@ const SettingsSelectInput: FC<SettingsSelectInputProps> = ({
5759
onChange,
5860
options,
5961
showKeyAsSelected = false,
62+
testId,
6063
value,
6164
}) => {
6265
const { t } = useForkliftTranslation();
@@ -100,14 +103,20 @@ const SettingsSelectInput: FC<SettingsSelectInputProps> = ({
100103
onClick={onToggleClick}
101104
isExpanded={isOpen}
102105
className="forklift-overview__settings-select"
106+
data-testid={testId}
103107
>
104108
<Truncate content={String(selected) || t('Select an option')} />
105109
</MenuToggle>
106110
);
107111

108112
const renderOptions = () => {
109113
const optionElements = options?.map(({ description, key, name }) => (
110-
<SelectOption key={key} value={showKeyAsSelected ? key : name} description={description}>
114+
<SelectOption
115+
key={key}
116+
value={showKeyAsSelected ? key : name}
117+
description={description}
118+
data-testid={testId ? `${testId}-option-${key}` : undefined}
119+
>
111120
{name}
112121
</SelectOption>
113122
));
@@ -118,6 +127,7 @@ const SettingsSelectInput: FC<SettingsSelectInputProps> = ({
118127
key={BLANK_OPTION_KEY}
119128
value={blankOption.name}
120129
description={blankOption.description}
130+
data-testid={testId ? `${testId}-option-none` : undefined}
121131
>
122132
{blankOption.name}
123133
</SelectOption>,

testing/playwright/e2e/downstream/overview-page.spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { expect, test } from '@playwright/test';
22

3+
import { createTestNad } from '../../fixtures/helpers/resourceCreationHelpers';
34
import { TIPS_AND_TRICKS_TOPICS } from '../../fixtures/overview-page-topics';
45
import { OverviewPage } from '../../page-objects/OverviewPage';
6+
import { MTV_NAMESPACE } from '../../utils/resource-manager/constants';
7+
import { ResourceManager } from '../../utils/resource-manager/ResourceManager';
58

69
test.describe(
710
'Overview Page - Tips and Tricks',
@@ -47,3 +50,44 @@ test.describe(
4750
});
4851
},
4952
);
53+
54+
test.describe(
55+
'Overview Page - Settings',
56+
{
57+
tag: '@downstream',
58+
},
59+
() => {
60+
const resourceManager = new ResourceManager();
61+
62+
test.beforeAll(async ({ browser }) => {
63+
const context = await browser.newContext({ ignoreHTTPSErrors: true });
64+
const page = await context.newPage();
65+
66+
await createTestNad(page, resourceManager, {
67+
namespace: MTV_NAMESPACE,
68+
});
69+
70+
await context.close();
71+
});
72+
73+
test.afterAll(async () => {
74+
await resourceManager.instantCleanup();
75+
});
76+
77+
test('should edit controller transfer network and verify save', async ({ page }) => {
78+
const overviewPage = new OverviewPage(page);
79+
80+
await test.step('Navigate to Settings tab', async () => {
81+
await overviewPage.navigateToSettings();
82+
});
83+
84+
await test.step('Verify transfer network field is visible', async () => {
85+
await overviewPage.verifyTransferNetworkFieldVisible();
86+
});
87+
88+
await test.step('Edit and save transfer network', async () => {
89+
await overviewPage.editAndSaveTransferNetwork();
90+
});
91+
});
92+
},
93+
);
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import type { Page } from '@playwright/test';
2+
3+
import { NavigationHelper } from '../../utils/NavigationHelper';
4+
import { NAD_API_VERSION, RESOURCE_KINDS } from '../../utils/resource-manager/constants';
5+
import type { V1NetworkAttachmentDefinition } from '../../utils/resource-manager/ResourceCreator';
6+
import type { ResourceManager } from '../../utils/resource-manager/ResourceManager';
7+
8+
export type TestNad = V1NetworkAttachmentDefinition & {
9+
metadata: {
10+
name: string;
11+
namespace: string;
12+
};
13+
};
14+
15+
/**
16+
* Creates a NetworkAttachmentDefinition (NAD) for testing purposes.
17+
*
18+
* NADs are used by the Settings tab's "Controller transfer network" dropdown
19+
* to select a network for data transfer during migrations.
20+
*
21+
* @param page - Playwright page instance (needed for CSRF token)
22+
* @param resourceManager - Resource manager for API calls and cleanup registration
23+
* @param options - Configuration options for the NAD
24+
* @returns Promise that resolves to the created TestNad
25+
*
26+
* @example
27+
* const nad = await createTestNad(page, resourceManager, {
28+
* name: 'my-test-nad',
29+
* namespace: 'openshift-mtv',
30+
* });
31+
*/
32+
export const createTestNad = async (
33+
page: Page,
34+
resourceManager: ResourceManager,
35+
options: {
36+
/** Name for the NAD (auto-generated if not provided) */
37+
name?: string;
38+
/** Namespace for the NAD */
39+
namespace: string;
40+
/** Bridge name for the CNI config (defaults to 'br0') */
41+
bridgeName?: string;
42+
},
43+
): Promise<TestNad> => {
44+
const { namespace, bridgeName = 'br0' } = options;
45+
46+
// Generate NAD name if not provided
47+
const nadName = options.name ?? `nad-test-${crypto.randomUUID().slice(0, 8)}`;
48+
49+
// Navigate to console to establish session (needed for CSRF token)
50+
const navigationHelper = new NavigationHelper(page);
51+
await navigationHelper.navigateToConsole();
52+
53+
// Create the NAD with a simple bridge CNI config
54+
const nadConfig = {
55+
cniVersion: '0.3.1',
56+
name: nadName,
57+
type: 'bridge',
58+
bridge: bridgeName,
59+
ipam: {},
60+
};
61+
62+
const nad: V1NetworkAttachmentDefinition = {
63+
apiVersion: NAD_API_VERSION,
64+
kind: RESOURCE_KINDS.NETWORK_ATTACHMENT_DEFINITION,
65+
metadata: {
66+
name: nadName,
67+
namespace,
68+
},
69+
spec: {
70+
config: JSON.stringify(nadConfig),
71+
},
72+
};
73+
74+
const createdNad = await resourceManager.createNad(page, nad, namespace);
75+
if (!createdNad) {
76+
throw new Error(`Failed to create NAD ${nadName}`);
77+
}
78+
79+
// Register NAD for cleanup
80+
resourceManager.addNad(nadName, namespace);
81+
82+
const result: TestNad = {
83+
...createdNad,
84+
metadata: {
85+
...createdNad.metadata,
86+
name: nadName,
87+
namespace,
88+
},
89+
};
90+
91+
return result;
92+
};
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import type { V1beta1NetworkMap, V1beta1NetworkMapSpecMap, V1beta1Provider } from '@kubev2v/types';
2+
import type { Page } from '@playwright/test';
3+
4+
import { CreateProviderPage } from '../../page-objects/CreateProviderPage';
5+
import { FORKLIFT_API_VERSION, MTV_NAMESPACE } from '../../utils/resource-manager/constants';
6+
import type { ResourceManager } from '../../utils/resource-manager/ResourceManager';
7+
8+
export type TestNetworkMap = V1beta1NetworkMap & {
9+
metadata: {
10+
name: string;
11+
namespace: string;
12+
};
13+
};
14+
15+
/**
16+
* Network mapping configuration for creating network maps
17+
*/
18+
export type NetworkMapMappingConfig = {
19+
/** Source network ID (from provider inventory) */
20+
sourceId?: string;
21+
/** Source network name */
22+
sourceName: string;
23+
/** Source network type (optional, e.g., 'DistributedVirtualPortgroup') */
24+
sourceType?: string;
25+
/** Destination type: 'pod' for default pod network, 'multus' for NAD */
26+
destinationType: 'pod' | 'multus';
27+
/** Destination name (e.g., 'Default network' for pod, or NAD name for multus) */
28+
destinationName?: string;
29+
/** Destination namespace (required for multus type) */
30+
destinationNamespace?: string;
31+
};
32+
33+
/**
34+
* Creates a network map directly via API based on provider references.
35+
*/
36+
export const createNetworkMap = async (
37+
page: Page,
38+
resourceManager: ResourceManager,
39+
options: {
40+
sourceProvider: V1beta1Provider;
41+
targetProvider: V1beta1Provider;
42+
mappings: NetworkMapMappingConfig[];
43+
name?: string;
44+
namespace?: string;
45+
},
46+
): Promise<TestNetworkMap> => {
47+
const { sourceProvider, targetProvider, mappings, name, namespace = MTV_NAMESPACE } = options;
48+
49+
const sourceProviderName = sourceProvider.metadata?.name;
50+
const targetProviderName = targetProvider.metadata?.name;
51+
52+
if (!sourceProviderName || !targetProviderName) {
53+
throw new Error('Source and target providers must have metadata.name defined');
54+
}
55+
56+
const networkMapName = name ?? `${sourceProviderName}-netmap-${crypto.randomUUID().slice(0, 8)}`;
57+
58+
const createProviderPage = new CreateProviderPage(page, resourceManager);
59+
await createProviderPage.navigationHelper.navigateToConsole();
60+
61+
const specMappings: V1beta1NetworkMapSpecMap[] = mappings.map((mapping) => {
62+
const destination =
63+
mapping.destinationType === 'pod'
64+
? { type: 'pod' as const, name: mapping.destinationName ?? 'Default network' }
65+
: {
66+
type: 'multus' as const,
67+
name: mapping.destinationName,
68+
namespace: mapping.destinationNamespace,
69+
};
70+
71+
return {
72+
destination,
73+
source: { id: mapping.sourceId, name: mapping.sourceName, type: mapping.sourceType },
74+
};
75+
});
76+
77+
const networkMap: V1beta1NetworkMap = {
78+
apiVersion: FORKLIFT_API_VERSION,
79+
kind: 'NetworkMap',
80+
metadata: { name: networkMapName, namespace },
81+
spec: {
82+
map: specMappings,
83+
provider: {
84+
destination: {
85+
name: targetProviderName,
86+
namespace: targetProvider.metadata?.namespace ?? namespace,
87+
},
88+
source: {
89+
name: sourceProviderName,
90+
namespace: sourceProvider.metadata?.namespace ?? namespace,
91+
},
92+
},
93+
},
94+
};
95+
96+
const createdNetworkMap = await resourceManager.createNetworkMap(page, networkMap, namespace);
97+
if (!createdNetworkMap) {
98+
throw new Error(`Failed to create network map ${networkMapName}`);
99+
}
100+
101+
resourceManager.addNetworkMap(networkMapName, namespace);
102+
103+
return {
104+
...createdNetworkMap,
105+
metadata: { ...createdNetworkMap.metadata, name: networkMapName, namespace },
106+
};
107+
};
108+
109+
/**
110+
* Creates a simple network map with default pod networking for all source networks.
111+
*/
112+
export const createSimpleNetworkMap = async (
113+
page: Page,
114+
resourceManager: ResourceManager,
115+
sourceProvider: V1beta1Provider,
116+
targetProvider: V1beta1Provider,
117+
sourceNetworks: { id?: string; name: string; type?: string }[],
118+
): Promise<TestNetworkMap> => {
119+
const mappings: NetworkMapMappingConfig[] = sourceNetworks.map((network) => ({
120+
sourceId: network.id,
121+
sourceName: network.name,
122+
sourceType: network.type,
123+
destinationType: 'pod' as const,
124+
}));
125+
126+
return createNetworkMap(page, resourceManager, { sourceProvider, targetProvider, mappings });
127+
};

testing/playwright/fixtures/helpers/resourceCreationHelpers.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ const buildTestProviderResult = (providerData: ProviderData): TestProvider => {
4646
return provider;
4747
};
4848

49+
// Re-export helpers from separate files for backwards compatibility
50+
export { createTestNad, type TestNad } from './nadHelpers';
51+
export {
52+
createNetworkMap,
53+
createSimpleNetworkMap,
54+
type NetworkMapMappingConfig,
55+
type TestNetworkMap,
56+
} from './networkMapHelpers';
57+
4958
export type TestProvider = V1beta1Provider & {
5059
metadata: {
5160
name: string;

0 commit comments

Comments
 (0)