diff --git a/src/overview/tabs/Settings/components/ControllerTransferNetwork/EditControllerTransferNetwork.tsx b/src/overview/tabs/Settings/components/ControllerTransferNetwork/EditControllerTransferNetwork.tsx index 0598f1bb6..2d42632a9 100644 --- a/src/overview/tabs/Settings/components/ControllerTransferNetwork/EditControllerTransferNetwork.tsx +++ b/src/overview/tabs/Settings/components/ControllerTransferNetwork/EditControllerTransferNetwork.tsx @@ -70,6 +70,7 @@ const EditControllerTransferNetwork: FC = () => { blankOption={{ name: t('None'), }} + testId="controller-transfer-network-select" /> )} /> diff --git a/src/overview/tabs/Settings/components/SettingsCard.tsx b/src/overview/tabs/Settings/components/SettingsCard.tsx index acbfab3f6..05696b3f5 100644 --- a/src/overview/tabs/Settings/components/SettingsCard.tsx +++ b/src/overview/tabs/Settings/components/SettingsCard.tsx @@ -53,6 +53,7 @@ const SettingsCard: FC = ({ obj }) => { }} className="pf-v6-u-mb-md" headingLevel="h3" + data-testid="settings-edit-button" /> = ({ obj }) => { helpContent={} /> = ({ closeModal, controlle closeModal={closeModal} variant={ModalVariant.medium} isDisabled={!isDirty} + testId="settings-edit-modal" >
{t( diff --git a/src/overview/tabs/Settings/components/SettingsSelectInput.tsx b/src/overview/tabs/Settings/components/SettingsSelectInput.tsx index 61a9ec53a..b4566a691 100644 --- a/src/overview/tabs/Settings/components/SettingsSelectInput.tsx +++ b/src/overview/tabs/Settings/components/SettingsSelectInput.tsx @@ -38,6 +38,7 @@ type BlankOption = { * @property {(value: string) => void} onChange - Function to call when the value changes * @property {Option[]} options - The options to present to the user * @property {BlankOption} [blankOption] - Optional blank option that passes an empty value when selected + * @property {string} [testId] - Test ID for the select component */ type SettingsSelectInputProps = { value: number | string; @@ -45,6 +46,7 @@ type SettingsSelectInputProps = { options: Option[]; blankOption?: BlankOption; showKeyAsSelected?: boolean; // a flag to show selected value that's based on option key and not name + testId?: string; }; const BLANK_OPTION_KEY = '__blank__'; @@ -57,6 +59,7 @@ const SettingsSelectInput: FC = ({ onChange, options, showKeyAsSelected = false, + testId, value, }) => { const { t } = useForkliftTranslation(); @@ -100,6 +103,7 @@ const SettingsSelectInput: FC = ({ onClick={onToggleClick} isExpanded={isOpen} className="forklift-overview__settings-select" + data-testid={testId} > @@ -107,7 +111,12 @@ const SettingsSelectInput: FC = ({ const renderOptions = () => { const optionElements = options?.map(({ description, key, name }) => ( - + {name} )); @@ -118,6 +127,7 @@ const SettingsSelectInput: FC = ({ key={BLANK_OPTION_KEY} value={blankOption.name} description={blankOption.description} + data-testid={testId ? `${testId}-option-none` : undefined} > {blankOption.name} , diff --git a/testing/playwright/e2e/downstream/overview-page.spec.ts b/testing/playwright/e2e/downstream/overview-page.spec.ts index c5ce192eb..ba960d9c3 100644 --- a/testing/playwright/e2e/downstream/overview-page.spec.ts +++ b/testing/playwright/e2e/downstream/overview-page.spec.ts @@ -1,7 +1,10 @@ import { expect, test } from '@playwright/test'; +import { createTestNad } from '../../fixtures/helpers/resourceCreationHelpers'; import { TIPS_AND_TRICKS_TOPICS } from '../../fixtures/overview-page-topics'; import { OverviewPage } from '../../page-objects/OverviewPage'; +import { MTV_NAMESPACE } from '../../utils/resource-manager/constants'; +import { ResourceManager } from '../../utils/resource-manager/ResourceManager'; test.describe( 'Overview Page - Tips and Tricks', @@ -47,3 +50,44 @@ test.describe( }); }, ); + +test.describe( + 'Overview Page - Settings', + { + tag: '@downstream', + }, + () => { + const resourceManager = new ResourceManager(); + + test.beforeAll(async ({ browser }) => { + const context = await browser.newContext({ ignoreHTTPSErrors: true }); + const page = await context.newPage(); + + await createTestNad(page, resourceManager, { + namespace: MTV_NAMESPACE, + }); + + await context.close(); + }); + + test.afterAll(async () => { + await resourceManager.instantCleanup(); + }); + + test('should edit controller transfer network and verify save', async ({ page }) => { + const overviewPage = new OverviewPage(page); + + await test.step('Navigate to Settings tab', async () => { + await overviewPage.navigateToSettings(); + }); + + await test.step('Verify transfer network field is visible', async () => { + await overviewPage.verifyTransferNetworkFieldVisible(); + }); + + await test.step('Edit and save transfer network', async () => { + await overviewPage.editAndSaveTransferNetwork(); + }); + }); + }, +); diff --git a/testing/playwright/fixtures/helpers/resourceCreationHelpers.ts b/testing/playwright/fixtures/helpers/resourceCreationHelpers.ts index 0b8035562..517e8ac6a 100644 --- a/testing/playwright/fixtures/helpers/resourceCreationHelpers.ts +++ b/testing/playwright/fixtures/helpers/resourceCreationHelpers.ts @@ -6,8 +6,19 @@ import { CreateProviderPage } from '../../page-objects/CreateProviderPage'; import { PlanDetailsPage } from '../../page-objects/PlanDetailsPage/PlanDetailsPage'; import { EndpointType, ProviderType } from '../../types/enums'; import { createPlanTestData, type ProviderData } from '../../types/test-data'; +import { NavigationHelper } from '../../utils/NavigationHelper'; import { getProviderConfig } from '../../utils/providers'; -import { MTV_NAMESPACE } from '../../utils/resource-manager/constants'; +import { + MTV_NAMESPACE, + NAD_API_VERSION, + RESOURCE_KINDS, +} from '../../utils/resource-manager/constants'; +import { + createNad as createNadApi, + createProvider as createProviderApi, + createSecret as createSecretApi, + type V1NetworkAttachmentDefinition, +} from '../../utils/resource-manager/ResourceCreator'; import type { ResourceManager } from '../../utils/resource-manager/ResourceManager'; export const createSecretObject = ( @@ -78,10 +89,11 @@ const createOvaProviderViaApi = async ( const secretName = `${providerData.name}-secret`; const secret = createSecretObject(secretName, MTV_NAMESPACE, { url: providerData.hostname }); - const createdSecret = await resourceManager.createSecret(page, secret); + const createdSecret = await createSecretApi(page, secret, MTV_NAMESPACE); if (!createdSecret) { throw new Error(`Failed to create secret for OVA provider ${providerData.name}`); } + resourceManager.addSecret(secretName, MTV_NAMESPACE); const provider = createProviderObject(providerData.name, MTV_NAMESPACE, { type: ProviderType.OVA, @@ -90,11 +102,10 @@ const createOvaProviderViaApi = async ( settings: { applianceManagement: 'true' }, }); - const createdProvider = await resourceManager.createProvider(page, provider); + const createdProvider = await createProviderApi(page, provider, MTV_NAMESPACE); if (!createdProvider) { throw new Error(`Failed to create OVA provider ${providerData.name}`); } - resourceManager.addProvider(providerData.name, MTV_NAMESPACE); }; @@ -207,3 +218,45 @@ export const createPlan = async ( return buildTestPlanResult(testPlanData); }; + +export const createTestNad = async ( + page: Page, + resourceManager: ResourceManager, + options: { + name?: string; + namespace: string; + bridgeName?: string; + }, +): Promise => { + const { namespace, bridgeName = 'br0' } = options; + const nadName = options.name ?? `nad-test-${crypto.randomUUID().slice(0, 8)}`; + + const navigationHelper = new NavigationHelper(page); + await navigationHelper.navigateToConsole(); + + const nadConfig = { + cniVersion: '0.3.1', + name: nadName, + type: 'bridge', + bridge: bridgeName, + ipam: {}, + }; + + const nad: V1NetworkAttachmentDefinition = { + apiVersion: NAD_API_VERSION, + kind: RESOURCE_KINDS.NETWORK_ATTACHMENT_DEFINITION, + metadata: { name: nadName, namespace }, + spec: { config: JSON.stringify(nadConfig) }, + }; + + const createdNad = await createNadApi(page, nad, namespace); + if (!createdNad) { + throw new Error(`Failed to create NAD ${nadName}`); + } + resourceManager.addNad(nadName, namespace); + + return { + ...createdNad, + metadata: { ...createdNad.metadata, name: nadName, namespace }, + }; +}; diff --git a/testing/playwright/page-objects/OverviewPage.ts b/testing/playwright/page-objects/OverviewPage.ts index 546f0e0d8..2a550d2f7 100644 --- a/testing/playwright/page-objects/OverviewPage.ts +++ b/testing/playwright/page-objects/OverviewPage.ts @@ -17,26 +17,22 @@ export class OverviewPage { this.navigation = new NavigationHelper(page); } - get choosingMigrationTypeOption() { - return this.page.getByText('Choosing the right migration type', { exact: true }).first(); - } - get closeDrawerButton() { return this.page.getByRole('button', { name: 'Close drawer panel' }); } - async closeTipsAndTricks() { - await expect(this.closeDrawerButton).toBeVisible(); - await this.closeDrawerButton.click(); - await expect(this.tipsAndTricksDrawerTitle).not.toBeVisible(); + async editAndSaveTransferNetwork(): Promise { + await this.openSettingsEditModal(); + await this.toggleTransferNetworkValue(); + await this.saveSettings(); } - get keyTerminologyOption() { - return this.page.getByText('Key terminology', { exact: true }).first(); + async getTransferNetworkCurrentValue(): Promise { + return this.transferNetworkDropdown.textContent(); } - get migratingVMsOption() { - return this.page.getByText('Migrating your virtual machines', { exact: true }).first(); + get modalConfirmButton() { + return this.page.getByTestId('modal-confirm-button'); } async navigateDirectly() { @@ -44,16 +40,21 @@ export class OverviewPage { await this.waitForPageLoad(); } - async navigateFromMainMenu() { - await this.navigation.navigateToOverview(); - await this.waitForPageLoad(); - } - async navigateToNextTopic(currentTopicName: string, nextTopicName: string): Promise { await this.selectTopicButton.click(); await this.page.getByRole('option', { name: nextTopicName }).click(); } + async navigateToSettings() { + await this.navigation.navigateToOverviewSettings(); + await this.waitForSettingsPageLoad(); + } + + async openSettingsEditModal(): Promise { + await this.settingsEditButton.click(); + await expect(this.settingsEditModal).toBeVisible(); + } + async openTipsAndTricks() { await expect(this.tipsAndTricksButton).toBeVisible({ timeout: 10000 }); await this.tipsAndTricksButton.click(); @@ -77,23 +78,21 @@ export class OverviewPage { }; } + async openTransferNetworkDropdown(): Promise { + await this.transferNetworkDropdown.click(); + } + get pageTitle() { return this.page.getByRole('heading', { name: 'Migration Toolkit for Virtualization' }); } - async selectTopic( - topicName: 'migrating-vms' | 'migration-type' | 'troubleshooting' | 'terminology', - ) { - const topicMap = { - 'migrating-vms': this.migratingVMsOption, - 'migration-type': this.choosingMigrationTypeOption, - troubleshooting: this.troubleshootingOption, - terminology: this.keyTerminologyOption, - }; + async saveSettings(): Promise { + await this.modalConfirmButton.click(); + await expect(this.settingsEditModal).not.toBeVisible({ timeout: 10000 }); + } - const topic = topicMap[topicName]; - await expect(topic).toBeVisible(); - await topic.click(); + async selectFirstAvailableNetwork(): Promise { + await this.transferNetworkOptions.first().click(); } get selectTopicButton() { @@ -107,9 +106,20 @@ export class OverviewPage { ).toBeVisible(); } - async selectTopicByName(topicName: string): Promise { - await this.page.getByTestId('topic-card').filter({ hasText: topicName }).click(); - await this.verifyTopicHeading(topicName); + async selectTransferNetworkNone(): Promise { + await this.transferNetworkNoneOption.click(); + } + + get settingsEditButton() { + return this.page.getByTestId('settings-edit-button'); + } + + get settingsEditModal() { + return this.page.getByTestId('settings-edit-modal'); + } + + get settingsTab() { + return this.page.getByRole('tab', { name: 'Settings', selected: true }); } async testAccordionsStructure(minimumCount: number): Promise { @@ -126,8 +136,6 @@ export class OverviewPage { await toggleButton.scrollIntoViewIfNeeded(); await expect(toggleButton).toBeVisible(); - - // Expand then collapse (mimicking old behavior without state assertions) await toggleButton.click(); await toggleButton.click(); } @@ -141,8 +149,35 @@ export class OverviewPage { return this.page.getByRole('heading', { name: 'Tips and tricks', level: 2 }); } - get troubleshootingOption() { - return this.page.getByText('Troubleshooting', { exact: true }).first(); + async toggleTransferNetworkValue(): Promise { + const currentValue = await this.getTransferNetworkCurrentValue(); + await this.openTransferNetworkDropdown(); + + const hasNetworkSelected = currentValue?.includes('/'); + + if (hasNetworkSelected) { + await this.selectTransferNetworkNone(); + } else { + await this.selectFirstAvailableNetwork(); + } + } + + get transferNetworkDropdown() { + return this.page.getByTestId('controller-transfer-network-select'); + } + + get transferNetworkField() { + return this.page.getByTestId('settings-controller-transfer-network'); + } + + get transferNetworkNoneOption() { + return this.page.getByTestId('controller-transfer-network-select-option-none'); + } + + get transferNetworkOptions() { + return this.page + .locator('[data-testid^="controller-transfer-network-select-option-"]') + .filter({ hasNotText: 'None' }); } async verifyPicklist(topics: TopicConfig[]): Promise { @@ -166,7 +201,15 @@ export class OverviewPage { await expect(this.page.getByRole('heading', { name: topicName, level: 3 })).toBeVisible(); } + async verifyTransferNetworkFieldVisible(): Promise { + await expect(this.transferNetworkField).toBeVisible(); + } + async waitForPageLoad() { await expect(this.pageTitle).toBeVisible({ timeout: 30000 }); } + + async waitForSettingsPageLoad() { + await expect(this.settingsTab).toBeVisible({ timeout: 30000 }); + } } diff --git a/testing/playwright/page-objects/common/Table.ts b/testing/playwright/page-objects/common/Table.ts index 55cd88904..9c90c22b2 100644 --- a/testing/playwright/page-objects/common/Table.ts +++ b/testing/playwright/page-objects/common/Table.ts @@ -145,9 +145,10 @@ export class Table { const tableContainer = this.getTableContainer(); let rows = tableContainer.locator('tbody tr'); - for (const [_columnName, expectedValue] of Object.entries(options)) { - rows = rows.filter({ hasText: expectedValue }); + rows = rows.filter({ + has: this.page.getByText(expectedValue, { exact: true }), + }); } return rows.first(); diff --git a/testing/playwright/types/test-data.ts b/testing/playwright/types/test-data.ts index a31e6557b..9bf743815 100644 --- a/testing/playwright/types/test-data.ts +++ b/testing/playwright/types/test-data.ts @@ -87,12 +87,12 @@ export const createPlanTestData = ( isPreexisting: false, mappings: [ { - source: 'mtv-nfs-rhos-v8', + source: 'mtv-nfs-us-v8', target: 'ocs-storagecluster-ceph-rbd-virtualization', }, ], }, - virtualMachines: [{ sourceName: 'mtv-func-rhel9', folder: 'vm' }], + virtualMachines: [{ sourceName: 'mtv-tests-rhel8', folder: 'vm' }], }; return { diff --git a/testing/playwright/utils/NavigationHelper.ts b/testing/playwright/utils/NavigationHelper.ts index c70faf38d..20adfd15a 100644 --- a/testing/playwright/utils/NavigationHelper.ts +++ b/testing/playwright/utils/NavigationHelper.ts @@ -75,6 +75,14 @@ export class NavigationHelper { await disableGuidedTour(this.page); } + async navigateToOverviewSettings(): Promise { + await disableGuidedTour(this.page); + await this.page.goto('/mtv/overview/settings'); + await this.page.waitForLoadState('networkidle'); + + await disableGuidedTour(this.page); + } + async navigateToPlans(): Promise { await this.navigateToMigrationMenu(); await this.page.getByTestId('plans-nav-item').click(); diff --git a/testing/playwright/utils/resource-manager/BaseResourceManager.ts b/testing/playwright/utils/resource-manager/BaseResourceManager.ts index 1e806a4c5..3e5cb8519 100644 --- a/testing/playwright/utils/resource-manager/BaseResourceManager.ts +++ b/testing/playwright/utils/resource-manager/BaseResourceManager.ts @@ -1,28 +1,61 @@ +import type { Page } from '@playwright/test'; + import { API_PATHS, COOKIE_NAMES, HTTP_HEADERS, RESOURCE_KINDS, RESOURCE_TYPES } from './constants'; +type ApiResult = { success: true; data: T } | { success: false; error: string }; + /** * Base class providing shared functionality for resource manager classes */ // eslint-disable-next-line @typescript-eslint/no-extraneous-class export abstract class BaseResourceManager { - public static getCsrfTokenFromCookie(): string { - const cookies = document.cookie.split('; '); - const csrfCookie = cookies.find((cookie) => cookie.startsWith(`${COOKIE_NAMES.CSRF_TOKEN}=`)); - return csrfCookie ? csrfCookie.split('=')[1] : ''; - } - /** - * Returns a function string for extracting CSRF token from cookies. - * This is used in page.evaluate() contexts where functions cannot be serialized. - * - * @returns A string that defines getCsrfTokenFromCookie function + * Generic API call helper that handles CSRF token and common error handling. + * Reduces duplication across all resource creation functions. */ - public static getCsrfTokenFunctionString(): string { - return `const getCsrfTokenFromCookie = () => { - const cookies = document.cookie.split('; '); - const csrfCookie = cookies.find((cookie) => cookie.startsWith(evalConstants.CSRF_TOKEN_NAME + '=')); - return csrfCookie ? csrfCookie.split('=')[1] : ''; - };`; + public static async apiPost(page: Page, apiPath: string, data: unknown): Promise { + const constants = BaseResourceManager.getEvaluateConstants(); + + const result = await page.evaluate( + async ({ payload, path, evalConstants }): Promise> => { + try { + // Get CSRF token from cookies + const cookies = document.cookie.split('; '); + const csrfCookie = cookies.find((cookie) => + cookie.startsWith(`${evalConstants.CSRF_TOKEN_NAME}=`), + ); + const csrfToken = csrfCookie ? csrfCookie.split('=')[1] : ''; + + const response = await fetch(path, { + method: 'POST', + headers: { + [evalConstants.CONTENT_TYPE_HEADER]: evalConstants.APPLICATION_JSON, + [evalConstants.CSRF_TOKEN_HEADER]: csrfToken, + }, + credentials: 'include', + body: JSON.stringify(payload), + }); + + if (response.ok) { + return { success: true, data: await response.json() }; + } + + const errorText = await response.text().catch(() => response.statusText); + return { success: false, error: errorText }; + } catch (error: unknown) { + const err = error as Error; + return { success: false, error: err?.message ?? String(error) }; + } + }, + { payload: data, path: apiPath, evalConstants: constants }, + ); + + if (result.success) { + return result.data; + } + + console.error(`API POST to ${apiPath} failed: ${result.error}`); + return null; } public static getEvaluateConstants() { @@ -35,6 +68,7 @@ export abstract class BaseResourceManager { KUBEVIRT_PATH: API_PATHS.KUBEVIRT, KUBERNETES_CORE: API_PATHS.KUBERNETES_CORE, OPENSHIFT_PROJECT_PATH: API_PATHS.OPENSHIFT_PROJECT, + NAD_PATH: API_PATHS.NAD, VIRTUAL_MACHINES_TYPE: RESOURCE_TYPES.VIRTUAL_MACHINES, PROJECTS_TYPE: RESOURCE_TYPES.PROJECTS, NAMESPACES_TYPE: RESOURCE_TYPES.NAMESPACES, @@ -45,6 +79,7 @@ export abstract class BaseResourceManager { const kindToType: Record = { [RESOURCE_KINDS.MIGRATION]: RESOURCE_TYPES.MIGRATIONS, [RESOURCE_KINDS.NETWORK_MAP]: RESOURCE_TYPES.NETWORK_MAPS, + [RESOURCE_KINDS.NETWORK_ATTACHMENT_DEFINITION]: RESOURCE_TYPES.NETWORK_ATTACHMENT_DEFINITIONS, [RESOURCE_KINDS.PLAN]: RESOURCE_TYPES.PLANS, [RESOURCE_KINDS.PROVIDER]: RESOURCE_TYPES.PROVIDERS, [RESOURCE_KINDS.VIRTUAL_MACHINE]: RESOURCE_TYPES.VIRTUAL_MACHINES, diff --git a/testing/playwright/utils/resource-manager/ResourceCreator.ts b/testing/playwright/utils/resource-manager/ResourceCreator.ts index ab8040b49..acaa69334 100644 --- a/testing/playwright/utils/resource-manager/ResourceCreator.ts +++ b/testing/playwright/utils/resource-manager/ResourceCreator.ts @@ -2,70 +2,31 @@ import type { IoK8sApiCoreV1Secret, V1beta1Provider } from '@kubev2v/types'; import type { Page } from '@playwright/test'; import { BaseResourceManager } from './BaseResourceManager'; -import { MTV_NAMESPACE } from './constants'; +import { API_PATHS, MTV_NAMESPACE } from './constants'; + +/** + * NetworkAttachmentDefinition type for CNI network configuration + */ +export type V1NetworkAttachmentDefinition = { + apiVersion: 'k8s.cni.cncf.io/v1'; + kind: 'NetworkAttachmentDefinition'; + metadata: { + name: string; + namespace: string; + annotations?: Record; + }; + spec: { + config: string; + }; +}; export const createProvider = async ( page: Page, provider: V1beta1Provider, namespace = MTV_NAMESPACE, ): Promise => { - try { - const constants = BaseResourceManager.getEvaluateConstants(); - const result = await page.evaluate( - async ({ providerData, ns, evalConstants }) => { - try { - const getCsrfTokenFromCookie = () => { - const cookies = document.cookie.split('; '); - const csrfCookie = cookies.find((cookie) => - cookie.startsWith(`${evalConstants.CSRF_TOKEN_NAME}=`), - ); - return csrfCookie ? csrfCookie.split('=')[1] : ''; - }; - const csrfToken = getCsrfTokenFromCookie(); - - const apiPath = `${evalConstants.FORKLIFT_PATH}/namespaces/${ns}/providers`; - - const response = await fetch(apiPath, { - method: 'POST', - headers: { - [evalConstants.CONTENT_TYPE_HEADER]: evalConstants.APPLICATION_JSON, - [evalConstants.CSRF_TOKEN_HEADER]: csrfToken, - }, - credentials: 'include', - body: JSON.stringify(providerData), - }); - - if (response.ok) { - return { success: true, data: await response.json() }; - } - - const errorText = await response.text().catch(() => response.statusText); - return { success: false, error: errorText }; - } catch (error: unknown) { - const err = error as Error; - return { - success: false, - error: err?.message ?? String(error), - }; - } - }, - { - providerData: provider, - ns: namespace, - evalConstants: constants, - }, - ); - - if (result.success && result.data) { - return result.data as V1beta1Provider; - } - - console.error(`Failed to create provider: ${result.error}`); - return null; - } catch (error) { - console.error('Exception while creating provider:', error); - return null; - } + const apiPath = `${API_PATHS.FORKLIFT}/namespaces/${namespace}/providers`; + return BaseResourceManager.apiPost(page, apiPath, provider); }; export const createSecret = async ( @@ -73,61 +34,15 @@ export const createSecret = async ( secret: IoK8sApiCoreV1Secret, namespace = MTV_NAMESPACE, ): Promise => { - try { - const constants = BaseResourceManager.getEvaluateConstants(); - const result = await page.evaluate( - async ({ secretData, ns, evalConstants }) => { - try { - const getCsrfTokenFromCookie = () => { - const cookieList = document.cookie.split('; '); - const tokenCookie = cookieList.find((cookie) => - cookie.startsWith(`${evalConstants.CSRF_TOKEN_NAME}=`), - ); - return tokenCookie ? tokenCookie.split('=')[1] : ''; - }; - const csrfToken = getCsrfTokenFromCookie(); - - const apiPath = `${evalConstants.KUBERNETES_CORE}/namespaces/${ns}/secrets`; - - const response = await fetch(apiPath, { - method: 'POST', - headers: { - [evalConstants.CONTENT_TYPE_HEADER]: evalConstants.APPLICATION_JSON, - [evalConstants.CSRF_TOKEN_HEADER]: csrfToken, - }, - credentials: 'include', - body: JSON.stringify(secretData), - }); - - if (response.ok) { - return { success: true, data: await response.json() }; - } - - const errorText = await response.text().catch(() => response.statusText); - return { success: false, error: errorText }; - } catch (error: unknown) { - const err = error as Error; - return { - success: false, - error: err?.message ?? String(error), - }; - } - }, - { - secretData: secret, - ns: namespace, - evalConstants: constants, - }, - ); - - if (result.success && result.data) { - return result.data as IoK8sApiCoreV1Secret; - } + const apiPath = `${API_PATHS.KUBERNETES_CORE}/namespaces/${namespace}/secrets`; + return BaseResourceManager.apiPost(page, apiPath, secret); +}; - console.error(`Failed to create secret: ${result.error}`); - return null; - } catch (error) { - console.error('Exception while creating secret:', error); - return null; - } +export const createNad = async ( + page: Page, + nad: V1NetworkAttachmentDefinition, + namespace: string, +): Promise => { + const apiPath = `${API_PATHS.NAD}/namespaces/${namespace}/network-attachment-definitions`; + return BaseResourceManager.apiPost(page, apiPath, nad); }; diff --git a/testing/playwright/utils/resource-manager/ResourceManager.ts b/testing/playwright/utils/resource-manager/ResourceManager.ts index 47b7c74f9..ea3cc617d 100644 --- a/testing/playwright/utils/resource-manager/ResourceManager.ts +++ b/testing/playwright/utils/resource-manager/ResourceManager.ts @@ -13,6 +13,7 @@ import { FORKLIFT_API_VERSION, KUBEVIRT_API_VERSION, MTV_NAMESPACE, + NAD_API_VERSION, NAMESPACE_API_VERSION, NAMESPACE_KIND, OPENSHIFT_PROJECT_API_VERSION, @@ -20,10 +21,12 @@ import { RESOURCE_KINDS, } from './constants'; import { ResourceCleaner } from './ResourceCleaner'; -import { createProvider, createSecret } from './ResourceCreator'; import { ResourceFetcher } from './ResourceFetcher'; import { ResourcePatcher } from './ResourcePatcher'; +export type { V1NetworkAttachmentDefinition } from './ResourceCreator'; +import type { V1NetworkAttachmentDefinition } from './ResourceCreator'; + export type OpenshiftProject = IoK8sApiCoreV1Namespace & { kind: typeof OPENSHIFT_PROJECT_KIND; apiVersion: typeof OPENSHIFT_PROJECT_API_VERSION; @@ -35,7 +38,9 @@ export type SupportedResource = | V1beta1Plan | V1beta1Provider | V1VirtualMachine + | V1NetworkAttachmentDefinition | IoK8sApiCoreV1Namespace + | IoK8sApiCoreV1Secret | OpenshiftProject; /** @@ -44,6 +49,21 @@ export type SupportedResource = export class ResourceManager { private resources: SupportedResource[] = []; + addNad(name: string, namespace: string): void { + const nad: V1NetworkAttachmentDefinition = { + apiVersion: NAD_API_VERSION, + kind: RESOURCE_KINDS.NETWORK_ATTACHMENT_DEFINITION, + metadata: { + name, + namespace, + }, + spec: { + config: '', + }, + }; + this.addResource(nad); + } + addNetworkMap(name: string, namespace: string): void { const networkMap: V1beta1NetworkMap = { apiVersion: FORKLIFT_API_VERSION, @@ -104,6 +124,18 @@ export class ResourceManager { this.resources.push(resource); } + addSecret(name: string, namespace: string): void { + const secret: IoK8sApiCoreV1Secret = { + apiVersion: 'v1', + kind: 'Secret', + metadata: { + name, + namespace, + }, + }; + this.addResource(secret); + } + addVm(name: string, namespace: string): void { const vm: V1VirtualMachine = { apiVersion: KUBEVIRT_API_VERSION, @@ -124,22 +156,6 @@ export class ResourceManager { this.resources = []; } - async createProvider( - page: Page, - provider: V1beta1Provider, - namespace = MTV_NAMESPACE, - ): Promise { - return createProvider(page, provider, namespace); - } - - async createSecret( - page: Page, - secret: IoK8sApiCoreV1Secret, - namespace = MTV_NAMESPACE, - ): Promise { - return createSecret(page, secret, namespace); - } - async fetchProvider( page: Page, providerName: string, diff --git a/testing/playwright/utils/resource-manager/constants.ts b/testing/playwright/utils/resource-manager/constants.ts index f63a99896..552ecc884 100644 --- a/testing/playwright/utils/resource-manager/constants.ts +++ b/testing/playwright/utils/resource-manager/constants.ts @@ -4,6 +4,7 @@ export const MTV_NAMESPACE = 'openshift-mtv'; export const RESOURCE_KINDS = { MIGRATION: 'Migration', NETWORK_MAP: 'NetworkMap', + NETWORK_ATTACHMENT_DEFINITION: 'NetworkAttachmentDefinition', PLAN: 'Plan', PROVIDER: 'Provider', VIRTUAL_MACHINE: 'VirtualMachine', @@ -14,6 +15,7 @@ export const RESOURCE_KINDS = { export const RESOURCE_TYPES = { MIGRATIONS: 'migrations', NETWORK_MAPS: 'networkmaps', + NETWORK_ATTACHMENT_DEFINITIONS: 'network-attachment-definitions', PLANS: 'plans', PROVIDERS: 'providers', VIRTUAL_MACHINES: 'virtualmachines', @@ -28,6 +30,7 @@ export const NAMESPACE_API_VERSION = 'v1'; export const FORKLIFT_API_VERSION = 'forklift.konveyor.io/v1beta1'; export const KUBEVIRT_API_VERSION = 'kubevirt.io/v1'; +export const NAD_API_VERSION = 'k8s.cni.cncf.io/v1'; export const RESOURCES_FILE = 'playwright/.resources.json'; @@ -38,6 +41,7 @@ export const API_PATHS = { OPENSHIFT_PROJECT: '/api/kubernetes/apis/project.openshift.io/v1', KUBERNETES_CORE: '/api/kubernetes/api/v1', FORKLIFT: '/api/kubernetes/apis/forklift.konveyor.io/v1beta1', + NAD: '/api/kubernetes/apis/k8s.cni.cncf.io/v1', } as const; // HTTP headers and other constants