V2Wizard: add oscap to wizard2

This adds the OpenSCAP step to the V2Wizard,
adds new values to the wizardSlice and enables relevant tests in the `CreateImageWizard.test.tsx` test suite
This commit is contained in:
mgold1234 2024-01-11 13:19:03 +02:00 committed by Klara Simickova
parent c7a80d0e85
commit c4e1709de8
6 changed files with 610 additions and 1 deletions

View file

@ -10,6 +10,7 @@ import {
import { useNavigate } from 'react-router-dom';
import ImageOutputStep from './steps/ImageOutput';
import OscapStep from './steps/Oscap';
import RegistrationStep from './steps/Registration';
import Aws from './steps/TargetEnvironment/Aws';
import Gcp from './steps/TargetEnvironment/Gcp';
@ -159,6 +160,13 @@ const CreateImageWizard = () => {
>
<RegistrationStep />
</WizardStep>
<WizardStep
name="OpenSCAP"
id="step-oscap"
footer={<CustomWizardFooter disableNext={false} />}
>
<OscapStep />
</WizardStep>
<WizardStep
name="Review"
id="step-review"

View file

@ -0,0 +1,257 @@
import React, { useEffect, useState } from 'react';
import {
Alert,
FormGroup,
Spinner,
Popover,
TextContent,
Text,
Button,
} from '@patternfly/react-core';
import {
Select,
SelectOption,
SelectVariant,
} from '@patternfly/react-core/deprecated';
import { HelpIcon } from '@patternfly/react-icons';
import OscapProfileInformation from './OscapProfileInformation';
import { useAppDispatch, useAppSelector } from '../../../../store/hooks';
import {
DistributionProfileItem,
useGetOscapCustomizationsQuery,
useGetOscapProfilesQuery,
} from '../../../../store/imageBuilderApi';
import {
changeOscapProfile,
selectDistribution,
selectProfile,
} from '../../../../store/wizardSlice';
const ProfileSelector = () => {
const oscapProfile = useAppSelector((state) => selectProfile(state));
const release = useAppSelector((state) => selectDistribution(state));
const dispatch = useAppDispatch();
const [profileName, setProfileName] = useState<string | undefined>('None');
const [isOpen, setIsOpen] = useState(false);
const {
data: profiles,
isFetching,
isSuccess,
isError,
refetch,
} = useGetOscapProfilesQuery({
distribution: release,
});
const { data } = useGetOscapCustomizationsQuery(
{
distribution: release,
// @ts-ignore if oscapProfile is undefined the query is going to get skipped, so it's safe here to ignore the linter here
profile: oscapProfile,
},
{
skip: !oscapProfile,
}
);
useEffect(() => {
if (
data &&
data.openscap &&
typeof data.openscap.profile_name === 'string'
) {
setProfileName(data.openscap.profile_name);
}
}, [data]);
const handleToggle = () => {
if (!isOpen) {
refetch();
}
setIsOpen(!isOpen);
};
const handleClear = () => {
dispatch(changeOscapProfile(undefined));
setProfileName(undefined);
};
const handleSelect = (
_event: React.MouseEvent<Element, MouseEvent>,
selection: DistributionProfileItem
) => {
dispatch(changeOscapProfile(selection));
setIsOpen(false);
};
const options = [
<OScapNoneOption setProfileName={setProfileName} key="oscap-none-option" />,
];
if (isSuccess) {
options.concat(
profiles.map((profile_id) => {
return (
<OScapSelectOption
key={profile_id}
profile_id={profile_id}
setProfileName={setProfileName}
/>
);
})
);
}
if (isFetching) {
options.push(
<SelectOption
isNoResultsOption={true}
data-testid="policies-loading"
key={'None'}
>
<Spinner size="md" />
</SelectOption>
);
}
return (
<FormGroup
isRequired={true}
data-testid="profiles-form-group"
label={
<>
OpenSCAP profile
<Popover
maxWidth="30rem"
position="left"
bodyContent={
<TextContent>
<Text>
To run a manual compliance scan in OpenSCAP, download this
image.
</Text>
</TextContent>
}
>
<Button variant="plain" aria-label="About OpenSCAP" isInline>
<HelpIcon />
</Button>
</Popover>
</>
}
>
<Select
ouiaId="profileSelect"
variant={SelectVariant.typeahead}
onToggle={handleToggle}
onSelect={handleSelect}
onClear={handleClear}
selections={profileName}
isOpen={isOpen}
placeholderText="Select a profile"
typeAheadAriaLabel="Select a profile"
isDisabled={!isSuccess}
onFilter={(_event, value) => {
if (profiles) {
return [
<OScapNoneOption
setProfileName={setProfileName}
key="oscap-none-option"
/>,
].concat(
profiles.map((profile_id, index) => {
return (
<OScapSelectOption
key={index}
profile_id={profile_id}
setProfileName={setProfileName}
input={value}
/>
);
})
);
}
}}
>
{options}
</Select>
{isError && (
<Alert
title="Error fetching the profiles"
variant="danger"
isPlain
isInline
>
Cannot get the list of profiles
</Alert>
)}
</FormGroup>
);
};
type OScapNoneOptionPropType = {
setProfileName: (name: string) => void;
};
const OScapNoneOption = ({ setProfileName }: OScapNoneOptionPropType) => {
return (
<SelectOption
value={undefined}
onClick={() => {
setProfileName('None');
}}
>
<p>{'None'}</p>
</SelectOption>
);
};
type OScapSelectOptionPropType = {
profile_id: DistributionProfileItem;
setProfileName: (name: string) => void;
input?: string;
};
const OScapSelectOption = ({
profile_id,
setProfileName,
input,
}: OScapSelectOptionPropType) => {
const release = useAppSelector((state) => selectDistribution(state));
const { data } = useGetOscapCustomizationsQuery({
distribution: release,
profile: profile_id,
});
if (
input &&
!data?.openscap?.profile_name?.toLowerCase().includes(input.toLowerCase())
) {
return null;
}
return (
<SelectOption
key={profile_id}
value={profile_id}
onClick={() => {
if (data?.openscap?.profile_name) {
setProfileName(data?.openscap?.profile_name);
}
}}
>
<p>{data?.openscap?.profile_name}</p>
</SelectOption>
);
};
export const Oscap = () => {
return (
<>
<ProfileSelector />
<OscapProfileInformation />
</>
);
};

View file

@ -0,0 +1,84 @@
import React from 'react';
import {
Spinner,
TextContent,
TextList,
TextListItem,
TextListItemVariants,
TextListVariants,
} from '@patternfly/react-core';
import { RELEASES } from '../../../../constants';
import { useAppSelector } from '../../../../store/hooks';
import { useGetOscapCustomizationsQuery } from '../../../../store/imageBuilderApi';
import {
selectDistribution,
selectProfile,
} from '../../../../store/wizardSlice';
const OscapProfileInformation = (): JSX.Element => {
const release = useAppSelector((state) => selectDistribution(state));
const oscapProfile = useAppSelector((state) => selectProfile(state));
const {
data: oscapProfileInfo,
isFetching: isFetchingOscapProfileInfo,
isSuccess: isSuccessOscapProfileInfo,
} = useGetOscapCustomizationsQuery(
{
distribution: release,
// @ts-ignore if oscapProfile is undefined the query is going to get skipped, so it's safe here to ignore the linter here
profile: oscapProfile,
},
{
skip: !oscapProfile,
}
);
return (
<>
{isFetchingOscapProfileInfo && <Spinner size="lg" />}
{isSuccessOscapProfileInfo && (
<TextContent>
<br />
<TextList component={TextListVariants.dl}>
<TextListItem
component={TextListItemVariants.dt}
className="pf-u-min-width"
>
Profile description:
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{oscapProfileInfo.openscap?.profile_description}
</TextListItem>
</TextList>
<TextList component={TextListVariants.dl}>
<TextListItem
component={TextListItemVariants.dt}
className="pf-u-min-width"
>
Operating system:
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{RELEASES.get(release)}
</TextListItem>
</TextList>
<TextList component={TextListVariants.dl}>
<TextListItem
component={TextListItemVariants.dt}
className="pf-u-min-width"
>
Reference ID:
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{oscapProfileInfo.openscap?.profile_id}
</TextListItem>
</TextList>
</TextContent>
)}
</>
);
};
export default OscapProfileInformation;

View file

@ -0,0 +1,31 @@
import React from 'react';
import { Form, Text, Title } from '@patternfly/react-core';
import { Oscap } from './Oscap';
import { imageBuilderApi } from '../../../../store/enhancedImageBuilderApi';
import { useAppSelector } from '../../../../store/hooks';
import { selectDistribution } from '../../../../store/wizardSlice';
import DocumentationButton from '../../../sharedComponents/DocumentationButton';
const OscapStep = () => {
const prefetchOscapProfile = imageBuilderApi.usePrefetch('getOscapProfiles');
const release = useAppSelector((state) => selectDistribution(state));
prefetchOscapProfile({ distribution: release });
return (
<Form>
<Title headingLevel="h2">OpenSCAP profile</Title>
<Text>
Use OpenSCAP to monitor the adherence of your registered RHEL systems to
a selected regulatory compliance profile
<br />
<DocumentationButton />
</Text>
<Oscap />
</Form>
);
};
export default OscapStep;

View file

@ -1,6 +1,11 @@
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import { Distributions, ImageRequest, ImageTypes } from './imageBuilderApi';
import {
DistributionProfileItem,
Distributions,
ImageRequest,
ImageTypes,
} from './imageBuilderApi';
import { ActivationKeys } from './rhsmApi';
import {
@ -37,6 +42,9 @@ type wizardState = {
registrationType: string;
activationKey: ActivationKeys['name'];
};
openScap: {
profile: DistributionProfileItem | undefined;
};
};
const initialState: wizardState = {
@ -61,6 +69,9 @@ const initialState: wizardState = {
registrationType: 'register-now-rhc',
activationKey: '',
},
openScap: {
profile: undefined,
},
};
export const selectServerUrl = (state: RootState) => {
@ -117,6 +128,10 @@ export const selectActivationKey = (state: RootState) => {
return state.wizard.registration.activationKey;
};
export const selectProfile = (state: RootState) => {
return state.wizard.openScap.profile;
};
export const wizardSlice = createSlice({
name: 'wizard',
initialState,
@ -190,6 +205,12 @@ export const wizardSlice = createSlice({
) => {
state.registration.activationKey = action.payload;
},
changeOscapProfile: (
state,
action: PayloadAction<DistributionProfileItem | undefined>
) => {
state.openScap.profile = action.payload;
},
},
});
@ -210,5 +231,6 @@ export const {
changeGcpEmail,
changeRegistrationType,
changeActivationKey,
changeOscapProfile,
} = wizardSlice.actions;
export default wizardSlice.reducer;

View file

@ -0,0 +1,207 @@
import React from 'react';
import '@testing-library/jest-dom';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import CreateImageWizard from '../../../Components/CreateImageWizardV2/CreateImageWizard';
import ShareImageModal from '../../../Components/ShareImageModal/ShareImageModal';
import { clickNext, renderCustomRoutesWithReduxRouter } from '../../testUtils';
const routes = [
{
path: 'insights/image-builder/*',
element: <div />,
},
{
path: 'insights/image-builder/imagewizard/:composeId?',
element: <CreateImageWizard />,
},
{
path: 'insights/image-builder/share/:composeId',
element: <ShareImageModal />,
},
];
jest.mock('@redhat-cloud-services/frontend-components/useChrome', () => ({
useChrome: () => ({
auth: {
getUser: () => {
return {
identity: {
internal: {
org_id: 5,
},
},
};
},
},
isBeta: () => true,
isProd: () => false,
getEnvironment: () => 'stage',
}),
}));
jest.mock('@unleash/proxy-client-react', () => ({
useUnleashContext: () => jest.fn(),
useFlag: jest.fn((flag) =>
flag === 'image-builder.wizard.oscap.enabled' ? true : false
),
}));
beforeAll(() => {
// scrollTo is not defined in jsdom
window.HTMLElement.prototype.scrollTo = function () {};
});
afterEach(() => {
jest.clearAllMocks();
});
describe('Step Compliance', () => {
const user = userEvent.setup();
const setup = async () => {
renderCustomRoutesWithReduxRouter('imagewizard', {}, routes);
};
test('create an image with None oscap profile', async () => {
await setup();
// select aws as upload destination
await user.click(await screen.findByTestId('upload-aws'));
await clickNext();
// aws step
await user.click(
await screen.findByRole('radio', {
name: /manually enter an account id\./i,
})
);
await user.type(
await screen.findByRole('textbox', {
name: 'aws account id',
}),
'012345678901'
);
await clickNext();
// skip registration
await user.click(await screen.findByLabelText('Register later'));
await clickNext();
// Now we should be in the Compliance step
await screen.findByRole('heading', { name: /OpenSCAP/i });
await user.click(
await screen.findByRole('textbox', { name: /select a profile/i })
);
await user.click(await screen.findByText(/none/i));
// check that the FSC does not contain a /tmp partition
await clickNext();
// await screen.findByRole('heading', { name: /File system configuration/i });
// expect(
// screen.queryByRole('cell', {
// name: /tmp/i,
// })
// ).not.toBeInTheDocument();
// check that there are no Packages contained when selecting the "None" profile option
// await clickNext();
// await screen.findByRole('heading', {
// name: /Additional Red Hat packages/i,
// });
// await screen.findByText(/no packages added/i);
});
test('create an image with an oscap profile', async () => {
await setup();
// select aws as upload destination
await user.click(await screen.findByTestId('upload-aws'));
await clickNext();
// aws step
await user.click(
await screen.findByRole('radio', {
name: /manually enter an account id\./i,
})
);
await user.type(
await screen.findByRole('textbox', {
name: 'aws account id',
}),
'012345678901'
);
await clickNext();
// skip registration
await user.click(await screen.findByLabelText('Register later'));
await clickNext();
// Now we should be at the OpenSCAP step
await screen.findByRole('heading', { name: /OpenSCAP/i });
await user.click(
await screen.findByRole('textbox', {
name: /select a profile/i,
})
);
await user.click(
await screen.findByText(
/cis red hat enterprise linux 8 benchmark for level 1 - workstation/i
)
);
// check that the FSC contains a /tmp partition
await clickNext();
// await screen.findByRole('heading', { name: /File system configuration/i });
// await screen.findByText(/tmp/i);
// check that the Packages contain a nftable package
// await clickNext();
// await screen.findByRole('heading', {
// name: /Additional Red Hat packages/i,
// });
// await screen.findByText(/nftables/i);
// await screen.findByText(/libselinux/i);
});
});
//
// TO DO - check if the correct version of Wizard is tested
//
//describe('On Recreate', () => {
// const setup = async () => {
// renderWithReduxRouter('imagewizard/1679d95b-8f1d-4982-8c53-8c2afa4ab04c');
// };
// test('with oscap profile', async () => {
// const user = userEvent.setup();
// await setup();
// await screen.findByRole('button', {
// name: /review/i,
// });
// const createImageButton = await screen.findByRole('button', {
// name: /create image/i,
// });
// await waitFor(() => expect(createImageButton).toBeEnabled());
// check that the FSC contains a /tmp partition
// There are two buttons with the same name but cannot easily select the DDF rendered sidenav.
// The sidenav will be the first node found out of all buttons.
// const buttonsFSC = await screen.findAllByRole('button', {
// name: /file system configuration/i,
// });
// await user.click(buttonsFSC[0]);
// await screen.findByRole('heading', { name: /file system configuration/i });
// await screen.findByText('/tmp');
// check that the Packages contain a nftable package
// await clickNext();
// await screen.findByRole('heading', {
// name: /Additional Red Hat packages/i,
// });
// await screen.findByText('nftables');
// });
//});