From 4802d082142e63873d82c63c79aaa6c4d5868456 Mon Sep 17 00:00:00 2001 From: regexowl Date: Wed, 15 Jan 2025 09:54:04 +0100 Subject: [PATCH] Wizard: Firewall ports input This adds chipping input for ports on the Firewall step. --- .../steps/Firewall/components/PortsInput.tsx | 32 ++++++++++ .../steps/Firewall/index.tsx | 3 + .../utilities/requestMapper.ts | 18 +++++- .../CreateImageWizard/validators.ts | 4 ++ src/store/wizardSlice.ts | 23 +++++++ .../steps/Firewall/Firewall.test.tsx | 62 +++++++++++++++++++ 6 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 src/Components/CreateImageWizard/steps/Firewall/components/PortsInput.tsx diff --git a/src/Components/CreateImageWizard/steps/Firewall/components/PortsInput.tsx b/src/Components/CreateImageWizard/steps/Firewall/components/PortsInput.tsx new file mode 100644 index 00000000..7527f4a4 --- /dev/null +++ b/src/Components/CreateImageWizard/steps/Firewall/components/PortsInput.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +import { FormGroup } from '@patternfly/react-core'; + +import { useAppSelector } from '../../../../../store/hooks'; +import { + addPort, + removePort, + selectPorts, +} from '../../../../../store/wizardSlice'; +import ChippingInput from '../../../ChippingInput'; +import { isPortValid } from '../../../validators'; + +const PortsInput = () => { + const ports = useAppSelector(selectPorts); + + return ( + + + + ); +}; + +export default PortsInput; diff --git a/src/Components/CreateImageWizard/steps/Firewall/index.tsx b/src/Components/CreateImageWizard/steps/Firewall/index.tsx index 2877f8df..1795bc86 100644 --- a/src/Components/CreateImageWizard/steps/Firewall/index.tsx +++ b/src/Components/CreateImageWizard/steps/Firewall/index.tsx @@ -2,6 +2,8 @@ import React from 'react'; import { Text, Form, Title } from '@patternfly/react-core'; +import PortsInput from './components/PortsInput'; + const FirewallStep = () => { return (
@@ -9,6 +11,7 @@ const FirewallStep = () => { Firewall Customize firewall settings for your image. + ); }; diff --git a/src/Components/CreateImageWizard/utilities/requestMapper.ts b/src/Components/CreateImageWizard/utilities/requestMapper.ts index 5a05c3d0..f0511a28 100644 --- a/src/Components/CreateImageWizard/utilities/requestMapper.ts +++ b/src/Components/CreateImageWizard/utilities/requestMapper.ts @@ -83,6 +83,7 @@ import { selectHostname, selectUsers, selectMetadata, + selectPorts, } from '../../../store/wizardSlice'; import { FileSystemConfigurationType } from '../steps/FileSystem'; import { @@ -323,6 +324,9 @@ function commonRequestToState( ntpservers: request.customizations.timezone?.ntpservers || [], }, hostname: request.customizations.hostname || '', + firewall: { + ports: request.customizations.firewall?.ports || [], + }, }; } @@ -523,7 +527,7 @@ const getCustomizations = (state: RootState, orgID: string): Customizations => { groups: undefined, timezone: getTimezone(state), locale: getLocale(state), - firewall: undefined, + firewall: getFirewall(state), installation_device: undefined, fdo: undefined, ignition: undefined, @@ -689,6 +693,18 @@ const getLocale = (state: RootState) => { } }; +const getFirewall = (state: RootState) => { + const ports = selectPorts(state); + + if (ports.length === 0) { + return undefined; + } else { + return { + ports: ports, + }; + } +}; + const getCustomRepositories = (state: RootState) => { const customRepositories = selectCustomRepositories(state); const recommendedRepositories = selectRecommendedRepositories(state); diff --git a/src/Components/CreateImageWizard/validators.ts b/src/Components/CreateImageWizard/validators.ts index 9704b36e..f65d26fb 100644 --- a/src/Components/CreateImageWizard/validators.ts +++ b/src/Components/CreateImageWizard/validators.ts @@ -103,3 +103,7 @@ export const isHostnameValid = (hostname: string) => { ) ); }; + +export const isPortValid = (port: string) => { + return /^(\d{1,5}|[a-z]{1,6})(-\d{1,5})?:[a-z]{1,6}$/.test(port); +}; diff --git a/src/store/wizardSlice.ts b/src/store/wizardSlice.ts index eb00d938..216f78c1 100644 --- a/src/store/wizardSlice.ts +++ b/src/store/wizardSlice.ts @@ -136,6 +136,9 @@ export type wizardState = { }; timezone: Timezone; hostname: string; + firewall: { + ports: string[]; + }; metadata?: { parent_id: string | null; exported_at: string; @@ -216,6 +219,9 @@ export const initialState: wizardState = { ntpservers: [], }, hostname: '', + firewall: { + ports: [], + }, firstBoot: { script: '' }, users: [], }; @@ -418,6 +424,10 @@ export const selectHostname = (state: RootState) => { return state.wizard.hostname; }; +export const selectPorts = (state: RootState) => { + return state.wizard.firewall.ports; +}; + export const wizardSlice = createSlice({ name: 'wizard', initialState, @@ -838,6 +848,17 @@ export const wizardSlice = createSlice({ setUserSshKeyByIndex: (state, action: PayloadAction) => { state.users[action.payload.index].ssh_key = action.payload.sshKey; }, + addPort: (state, action: PayloadAction) => { + if (!state.firewall.ports.some((port) => port === action.payload)) { + state.firewall.ports.push(action.payload); + } + }, + removePort: (state, action: PayloadAction) => { + state.firewall.ports.splice( + state.firewall.ports.findIndex((port) => port === action.payload), + 1 + ); + }, }, }); @@ -906,6 +927,8 @@ export const { addNtpServer, removeNtpServer, changeHostname, + addPort, + removePort, addUser, setUserNameByIndex, setUserPasswordByIndex, diff --git a/src/test/Components/CreateImageWizard/steps/Firewall/Firewall.test.tsx b/src/test/Components/CreateImageWizard/steps/Firewall/Firewall.test.tsx index f5a44ceb..40d6816b 100644 --- a/src/test/Components/CreateImageWizard/steps/Firewall/Firewall.test.tsx +++ b/src/test/Components/CreateImageWizard/steps/Firewall/Firewall.test.tsx @@ -2,9 +2,14 @@ import type { Router as RemixRouter } from '@remix-run/router'; import { screen, waitFor } from '@testing-library/react'; import { userEvent } from '@testing-library/user-event'; +import { CREATE_BLUEPRINT } from '../../../../../constants'; import { + blueprintRequest, clickBack, clickNext, + enterBlueprintName, + interceptBlueprintRequest, + openAndDismissSaveAndBuildModal, verifyCancelButton, } from '../../wizardTestUtils'; import { clickRegisterLater, renderCreateMode } from '../../wizardTestUtils'; @@ -32,6 +37,19 @@ const goToFirewallStep = async () => { await clickNext(); // Firewall }; +const goToReviewStep = async () => { + await clickNext(); // First boot script + await clickNext(); // Details + await enterBlueprintName(); + await clickNext(); // Review +}; + +const addPort = async (port: string) => { + const user = userEvent.setup(); + const portsInput = await screen.findByPlaceholderText(/add port/i); + await waitFor(() => user.type(portsInput, port.concat(','))); +}; + describe('Step Firewall', () => { beforeEach(() => { vi.clearAllMocks(); @@ -59,6 +77,50 @@ describe('Step Firewall', () => { await goToFirewallStep(); await verifyCancelButton(router); }); + + test('duplicate ports cannnot be added', async () => { + await renderCreateMode(); + await goToFirewallStep(); + expect(screen.queryByText('Port already exists.')).not.toBeInTheDocument(); + await addPort('22:tcp'); + await addPort('22:tcp'); + await screen.findByText('Port already exists.'); + }); + + test('port in an invalid format cannot be added', async () => { + await renderCreateMode(); + await goToFirewallStep(); + expect(screen.queryByText('Invalid format.')).not.toBeInTheDocument(); + await addPort('00:wrongFormat'); + await screen.findByText('Invalid format.'); + }); +}); + +describe('Firewall request generated correctly', () => { + test('with ports added', async () => { + await renderCreateMode(); + await goToFirewallStep(); + await addPort('22:tcp'); + await addPort('imap:tcp'); + await goToReviewStep(); + // informational modal pops up in the first test only as it's tied + // to a 'imageBuilder.saveAndBuildModalSeen' variable in localStorage + await openAndDismissSaveAndBuildModal(); + const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT); + + const expectedRequest = { + ...blueprintRequest, + customizations: { + firewall: { + ports: ['22:tcp', 'imap:tcp'], + }, + }, + }; + + await waitFor(() => { + expect(receivedRequest).toEqual(expectedRequest); + }); + }); }); // TO DO Step Firewall -> revisit step button on Review works