diff --git a/packages/mco/__mocks__/drplacementcontrol.tsx b/packages/mco/__mocks__/drplacementcontrol.tsx index f3677254d..dbc8e6b1b 100644 --- a/packages/mco/__mocks__/drplacementcontrol.tsx +++ b/packages/mco/__mocks__/drplacementcontrol.tsx @@ -78,3 +78,40 @@ export const mockDRPC2: DRPlacementControlKind = { lastGroupSyncTime: '2023-06-06T17:50:56Z', }, }; + +export const mockDRPC3: DRPlacementControlKind = { + apiVersion: `${DRPlacementControlModel.apiGroup}/${DRPlacementControlModel.apiVersion}`, + kind: DRPlacementControlModel.kind, + metadata: { + name: 'mock-drpc-3', + namespace: 'test-ns', + creationTimestamp: '2023-06-06T17:50:56Z', + annotations: { + [LAST_APP_DEPLOYMENT_CLUSTER_ANNOTATION]: getName(mockDRClusterEast1), + }, + }, + spec: { + drPolicyRef: createRefFromK8Resource(mockDRPolicy1), + placementRef: {}, + pvcSelector: { + matchLabels: { + pvc: 'pvc1', + }, + }, + }, + status: { + conditions: [ + { + type: 'PeerReady', + status: 'True', + }, + { + type: 'Available', + status: 'True', + }, + ], + phase: 'Deployed', + progression: Progression.WaitOnUserToCleanUp, + lastGroupSyncTime: new Date().toISOString(), + }, +}; diff --git a/packages/mco/components/modals/app-failover-relocate/parser/argo-application-set-parser.spec.tsx b/packages/mco/components/modals/app-failover-relocate/parser/argo-application-set-parser.spec.tsx new file mode 100644 index 000000000..0cea36e89 --- /dev/null +++ b/packages/mco/components/modals/app-failover-relocate/parser/argo-application-set-parser.spec.tsx @@ -0,0 +1,282 @@ +import * as React from 'react'; +import { DRActionType } from '@odf/mco/constants'; +import { DisasterRecoveryResourceKind } from '@odf/mco/hooks'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +/* eslint-disable jest/no-mocks-import */ +import { mockApplicationSet1 } from '../../../../__mocks__/applicationset'; +import { + mockDRClusterEast1, + mockDRClusterWest1, +} from '../../../../__mocks__/drcluster'; +import { mockDRPC1, mockDRPC3 } from '../../../../__mocks__/drplacementcontrol'; +import { mockDRPolicy1 } from '../../../../__mocks__/drpolicy'; +import { + mockManagedClusterEast1, + mockManagedClusterWest1, +} from '../../../../__mocks__/managedcluster'; +import { mockPlacement1 } from '../../../../__mocks__/placement'; +import { mockPlacementDecision1 } from '../../../../__mocks__/placementdecision'; +import { ArogoApplicationSetParser } from './argo-application-set-parser'; +/* eslint-enable jest/no-mocks-import */ + +let type = 1; +let patchObj = {}; + +const aroAppSetResources = (drPlacementControl) => ({ + formattedResources: [ + { + application: mockApplicationSet1, + managedClusters: [mockManagedClusterEast1, mockManagedClusterWest1], + siblingApplications: [], + placements: [ + { + placementDecision: mockPlacementDecision1, + drPlacementControl, + drClusters: [mockDRClusterEast1, mockDRClusterWest1], + drPolicy: mockDRPolicy1, + placement: mockPlacement1, + }, + ], + }, + ], +}); + +const drResources1: DisasterRecoveryResourceKind = { + formattedResources: [ + { + drClusters: [mockDRClusterEast1, mockDRClusterWest1], + drPolicy: mockDRPolicy1, + drPlacementControls: [mockDRPC1], + }, + ], +}; + +const drResources2: DisasterRecoveryResourceKind = { + formattedResources: [ + { + drClusters: [mockDRClusterEast1, mockDRClusterWest1], + drPolicy: mockDRPolicy1, + drPlacementControls: [mockDRPC3], + }, + ], +}; + +jest.mock('@odf/mco/hooks', () => ({ + ...jest.requireActual('@odf/mco/hooks'), + useArgoApplicationSetResourceWatch: jest.fn(() => [ + aroAppSetResources(type === 1 ? mockDRPC1 : mockDRPC3), + true, + '', + ]), +})); + +jest.mock('@odf/mco/hooks/disaster-recovery', () => ({ + __esModule: true, + useDisasterRecoveryResourceWatch: jest.fn(() => { + if (type === 1) { + return [drResources1, true, '']; + } else { + return [drResources2, true, '']; + } + }), +})); + +jest.mock( + '@openshift-console/dynamic-plugin-sdk/lib/api/dynamic-core-api', + () => ({ + ...jest.requireActual( + '@openshift-console/dynamic-plugin-sdk/lib/api/dynamic-core-api' + ), + useK8sWatchResource: jest.fn(() => { + return [[mockManagedClusterEast1, mockManagedClusterWest1], true, '']; + }), + k8sPatch: jest.fn(({ data }) => { + patchObj = data; + return Promise.resolve({ data: {} }); + }), + }) +); + +jest.mock('@odf/shared/hooks', () => ({ + ...jest.requireActual('@odf/shared/hooks'), + DOC_VERSION: '1.2', + useDocVersion: jest.fn(() => '1.2'), +})); + +describe('Discovered application failover/relocate modal test', () => { + test('Failover test', async () => { + type = 1; + + render( + + ); + + // Modal title + expect( + screen.getByRole('dialog', { name: /Failover application/i }) + ).toBeInTheDocument(); + + // Modal description + expect( + screen.getByRole('dialog', { + description: + /Failing over force stops active replication and deploys your application on the selected target cluster. Recommended only when the primary cluster is down./i, + }) + ).toBeInTheDocument(); + + // DR info + expect(screen.getByText(/Application:/i)).toBeInTheDocument(); + expect(screen.getByText(/mock-appset-1/i)).toBeInTheDocument(); + + expect(screen.getByText(/Target cluster:/i)).toBeInTheDocument(); + expect(screen.getByText(/west-1/i)).toBeInTheDocument(); + expect(screen.getByText(/Last available: /i)).toBeInTheDocument(); + // ToDo: Need to find why Date check failing on CI + //expect(screen.getByText(/29 Nov 2023, 4:30 am UTC/i)).toBeInTheDocument(); + + expect(screen.getByText(/{{actionType}} readiness:/i)).toBeInTheDocument(); + expect(screen.getByText(/Ready/i)).toBeInTheDocument(); + + // footer + expect(screen.getByRole('button', { name: /Cancel/i })).toBeEnabled(); + expect(screen.getByRole('button', { name: /Initiate/i })).toBeEnabled(); + + // Click initiate + fireEvent.click(screen.getByRole('button', { name: /Initiate/i })); + await waitFor(() => + expect( + JSON.stringify(patchObj) === + '[{"op":"replace","path":"/spec/action","value":"Failover"},{"op":"replace","path":"/spec/failoverCluster","value":"west-1"},{"op":"replace","path":"/spec/preferredCluster","value":"east-1"}]' + ).toBeTruthy() + ); + }); + + test('Relocate test', async () => { + type = 1; + + render( + + ); + + // Modal title + expect( + screen.getByRole('dialog', { name: /Relocate application/i }) + ).toBeInTheDocument(); + + // Modal description + expect( + screen.getByRole('dialog', { + description: + /Relocating terminates your application on its current cluster, syncs its most recent snapshot to the selected target cluster, and then brings up your application./i, + }) + ).toBeInTheDocument(); + + // DR info + expect(screen.getByText(/Application:/i)).toBeInTheDocument(); + expect(screen.getByText(/mock-appset-1/i)).toBeInTheDocument(); + + expect(screen.getByText(/Target cluster:/i)).toBeInTheDocument(); + expect(screen.getByText(/west-1/i)).toBeInTheDocument(); + expect(screen.getByText(/Last available: /i)).toBeInTheDocument(); + //expect(screen.getByText(/29 Nov 2023, 4:30 am UTC/i)).toBeInTheDocument(); + + expect(screen.getByText(/{{actionType}} readiness:/i)).toBeInTheDocument(); + expect(screen.getByText(/Ready/i)).toBeInTheDocument(); + + // footer + expect(screen.getByRole('button', { name: /Cancel/i })).toBeEnabled(); + expect(screen.getByRole('button', { name: /Initiate/i })).toBeEnabled(); + + // Click initiate + fireEvent.click(screen.getByRole('button', { name: /Initiate/i })); + await waitFor(() => + expect( + JSON.stringify(patchObj) === + '[{"op":"replace","path":"/spec/action","value":"Relocate"},{"op":"replace","path":"/spec/failoverCluster","value":"east-1"},{"op":"replace","path":"/spec/preferredCluster","value":"west-1"}]' + ).toBeTruthy() + ); + }); + + test('App set volume synchronization delay during relocate', async () => { + type = 1; + + render( + + ); + + expect( + screen.getByRole('dialog', { name: /Relocate application/i }) + ).toBeInTheDocument(); + + expect( + screen.getByRole('heading', { + name: /Warning alert: Inconsistent data on target cluster/i, + }) + ).toBeInTheDocument(); + expect( + screen.getByText( + /The target cluster's volumes contain data inconsistencies caused by synchronization delays. Performing relocate could lead to data loss./i + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + /Refer to the corresponding VolumeSynchronizationDelay OpenShift alert\(s\) for more information./i + ) + ).toBeInTheDocument(); + + expect(screen.getByRole('button', { name: /Cancel/i })).toBeEnabled(); + expect(screen.getByRole('button', { name: /Initiate/i })).toBeEnabled(); + }); + test('No volume synchronization delay during relocate', async () => { + type = 2; + + render( + + ); + + expect( + screen.getByRole('dialog', { name: /Relocate application/i }) + ).toBeInTheDocument(); + + expect( + screen.queryByRole('heading', { + name: /Warning alert: Inconsistent data on target cluster/i, + }) + ).not.toBeInTheDocument(); + + expect( + screen.queryByText( + /The target cluster's volumes contain data inconsistencies caused by synchronization delays. Performing relocate could lead to data loss./i + ) + ).not.toBeInTheDocument(); + + expect( + screen.queryByText( + /Refer to the corresponding VolumeSynchronizationDelay OpenShift alert\(s\) for more information./i + ) + ).not.toBeInTheDocument(); + + expect(screen.getByRole('button', { name: /Cancel/i })).toBeEnabled(); + expect(screen.getByRole('button', { name: /Initiate/i })).toBeEnabled(); + }); +}); diff --git a/packages/mco/components/modals/app-failover-relocate/parser/discovered-application-parser.spec.tsx b/packages/mco/components/modals/app-failover-relocate/parser/discovered-application-parser.spec.tsx index d02951b04..1163ccd01 100644 --- a/packages/mco/components/modals/app-failover-relocate/parser/discovered-application-parser.spec.tsx +++ b/packages/mco/components/modals/app-failover-relocate/parser/discovered-application-parser.spec.tsx @@ -9,7 +9,11 @@ import { mockDRClusterWest1, } from '../../../../__mocks__/drcluster'; // eslint-disable-next-line jest/no-mocks-import -import { mockDRPC1, mockDRPC2 } from '../../../../__mocks__/drplacementcontrol'; +import { + mockDRPC1, + mockDRPC2, + mockDRPC3, +} from '../../../../__mocks__/drplacementcontrol'; // eslint-disable-next-line jest/no-mocks-import import { mockDRPolicy1, mockDRPolicy2 } from '../../../../__mocks__/drpolicy'; // eslint-disable-next-line jest/no-mocks-import @@ -64,6 +68,16 @@ const drResources4: DisasterRecoveryResourceKind = { ], }; +const drResources5: DisasterRecoveryResourceKind = { + formattedResources: [ + { + drClusters: [mockDRClusterEast1, mockDRClusterWest1], + drPolicy: mockDRPolicy1, + drPlacementControls: [mockDRPC3], + }, + ], +}; + jest.mock('@odf/mco/hooks/disaster-recovery', () => ({ __esModule: true, useDisasterRecoveryResourceWatch: jest.fn(() => { @@ -75,6 +89,8 @@ jest.mock('@odf/mco/hooks/disaster-recovery', () => ({ return [drResources3, true, '']; } else if (type === 4) { return [drResources4, true, '']; + } else if (type === 5) { + return [drResources5, true, '']; } }), })); @@ -86,7 +102,7 @@ jest.mock( '@openshift-console/dynamic-plugin-sdk/lib/api/dynamic-core-api' ), useK8sWatchResource: jest.fn(() => { - if (type === 1) { + if (type === 1 || type === 5) { return [[mockManagedClusterEast1, mockManagedClusterWest1], true, '']; } else if (type === 4) { return [[mockManagedClusterEast1, mockManagedClusterEast2], true, '']; @@ -464,4 +480,76 @@ describe('Discovered application failover/relocate modal test', () => { expect(screen.getByRole('button', { name: /Cancel/i })).toBeEnabled(); expect(screen.getByRole('button', { name: /Initiate/i })).toBeDisabled(); }); + + test('Volume synchronization delay during relocate', async () => { + type = 1; + + render( + + ); + + expect( + screen.getByRole('dialog', { name: /Relocate application/i }) + ).toBeInTheDocument(); + + expect( + screen.getByRole('heading', { + name: /Warning alert: Inconsistent data on target cluster/i, + }) + ).toBeInTheDocument(); + expect( + screen.getByText( + /The target cluster's volumes contain data inconsistencies caused by synchronization delays. Performing relocate could lead to data loss./i + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + /Refer to the corresponding VolumeSynchronizationDelay OpenShift alert\(s\) for more information./i + ) + ).toBeInTheDocument(); + + expect(screen.getByRole('button', { name: /Cancel/i })).toBeEnabled(); + expect(screen.getByRole('button', { name: /Initiate/i })).toBeEnabled(); + }); + + test('No volume synchronization delay during relocate', async () => { + type = 5; + + render( + + ); + + expect( + screen.getByRole('dialog', { name: /Relocate application/i }) + ).toBeInTheDocument(); + + expect( + screen.queryByRole('heading', { + name: /Warning alert: Inconsistent data on target cluster/i, + }) + ).not.toBeInTheDocument(); + + expect( + screen.queryByText( + /The target cluster's volumes contain data inconsistencies caused by synchronization delays. Performing relocate could lead to data loss./i + ) + ).not.toBeInTheDocument(); + + expect( + screen.queryByText( + /Refer to the corresponding VolumeSynchronizationDelay OpenShift alert\(s\) for more information./i + ) + ).not.toBeInTheDocument(); + + expect(screen.getByRole('button', { name: /Cancel/i })).toBeEnabled(); + expect(screen.getByRole('button', { name: /Initiate/i })).toBeEnabled(); + }); }); diff --git a/packages/mco/components/modals/app-failover-relocate/parser/discovered-application-parser.tsx b/packages/mco/components/modals/app-failover-relocate/parser/discovered-application-parser.tsx index e5219313b..b242558ee 100644 --- a/packages/mco/components/modals/app-failover-relocate/parser/discovered-application-parser.tsx +++ b/packages/mco/components/modals/app-failover-relocate/parser/discovered-application-parser.tsx @@ -100,7 +100,7 @@ export const DiscoveredApplicationParser: React.FC< const placementControls: PlacementControlProps[] = React.useMemo(() => { if (loaded && !loadError) { - const { drClusters, drPlacementControls } = drResource || {}; + const { drClusters, drPlacementControls, drPolicy } = drResource || {}; const drPlacementControl = drPlacementControls?.[0]; // use "drplacementcontrol.ramendr.openshift.io/last-app-deployment-cluster" annotation @@ -159,6 +159,7 @@ export const DiscoveredApplicationParser: React.FC< isDRActionReady: checkDRActionReadiness(drPlacementControl, action), snapshotTakenTime: drPlacementControl?.status?.lastGroupSyncTime, replicationType: findDRType(drClusters), + schedulingInterval: drPolicy?.spec?.schedulingInterval, }, ]; }