Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add validation to fileupload and fileuploadwithtag when editing component id #13445

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7a06c67
Add validation to fileupload and fileuploadwithtag when editing compo…
Jondyr Aug 29, 2024
1511731
Use metadataQuery instead of metadataIdsQuery
Jondyr Aug 29, 2024
e292f9b
Add optional chaining
Jondyr Sep 2, 2024
7980fab
Move appMetadataQuery to app-shared
Jondyr Sep 2, 2024
64b5ba9
Refactor validation code
Jondyr Sep 2, 2024
84828d3
Update test to test for attachment component type
Jondyr Sep 2, 2024
c65f58d
Merge branch 'main' into fix/check-for-duplicate-datamodel-id-when-re…
Jondyr Sep 2, 2024
9bf1e8a
Rename arrow func param
Jondyr Sep 2, 2024
cf5e857
Merge branch 'main' into fix/check-for-duplicate-datamodel-id-when-re…
Jondyr Sep 9, 2024
6eea146
Refactor function and add lowercase comparison to name validation
Jondyr Sep 9, 2024
cfcfae9
Rename validation function name
Jondyr Sep 9, 2024
92cb064
Refactor datamodel name validation
Jondyr Sep 9, 2024
7772575
Move `userEvent` setup function to start of each unit test
Jondyr Sep 9, 2024
1608731
Remove unused userEvent const use app-development renderWithProviders
Jondyr Sep 9, 2024
feaee36
Add test case for datatype name existing in applicationmetadata when …
Jondyr Sep 9, 2024
202159d
Add shared function for button query
Jondyr Sep 9, 2024
e8174c0
Merge branch 'main' into fix/check-for-duplicate-datamodel-id-when-re…
Jondyr Sep 10, 2024
21645ac
Merge branch 'main' into fix/check-for-duplicate-datamodel-id-when-re…
Jondyr Sep 10, 2024
563f4ca
Update nb.json error messages for invalid datamodel name
Jondyr Sep 12, 2024
95c788e
Merge branch 'main' into fix/check-for-duplicate-datamodel-id-when-re…
Jondyr Sep 16, 2024
8062a71
Update error texts for datatype ID collisions
Jondyr Sep 16, 2024
6db8e08
Update text keys in tests
Jondyr Sep 16, 2024
dfae4fc
Add validation function in studioFileUploader
standeren Sep 17, 2024
692ac10
Remove `debugger;`
Jondyr Sep 17, 2024
66126d0
Add validation to xsd file upload for duplicate datatypes
Jondyr Sep 17, 2024
a546396
Merge branch 'main' into fix/check-for-duplicate-datamodel-id-when-re…
Jondyr Sep 17, 2024
bd7a630
Add test cases for xsd file upload name validation
Jondyr Sep 17, 2024
7de0a87
Merge branch 'main' into fix/check-for-duplicate-datamodel-id-when-re…
Jondyr Sep 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { render as renderRtl, screen } from '@testing-library/react';
import { screen } from '@testing-library/react';
import userEvent, { PointerEventsCheckLevel } from '@testing-library/user-event';
import type { CreateNewWrapperProps } from './CreateNewWrapper';
import { CreateNewWrapper } from './CreateNewWrapper';
Expand All @@ -8,8 +8,11 @@ import {
dataModel1NameMock,
jsonMetadata1Mock,
} from '../../../../../packages/schema-editor/test/mocks/metadataMocks';

const user = userEvent.setup();
import { renderWithProviders } from '../../../../test/testUtils';
import { app, org } from '@studio/testing/testids';
import { APP_DEVELOPMENT_BASENAME } from 'app-shared/constants';
import { QueryKey } from 'app-shared/types/QueryKey';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';

// Test data:
const handleCreateSchema = jest.fn();
Expand All @@ -26,6 +29,7 @@ describe('CreateNewWrapper', () => {
afterEach(jest.clearAllMocks);

it('should open the popup when clicking "new" button', async () => {
const user = userEvent.setup();
render();

expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
Expand All @@ -45,13 +49,10 @@ describe('CreateNewWrapper', () => {
});

it('should close the popup when clicking "new" button', async () => {
const user = userEvent.setup();
render({ createNewOpen: true });
expect(screen.getByRole('textbox')).toBeInTheDocument();
expect(
screen.getByRole('button', {
name: textMock('schema_editor.create_model_confirm_button'),
}),
).toBeInTheDocument();
expect(okButton()).toBeInTheDocument();

const newButton = screen.getByRole('button', {
name: textMock('general.create_new'),
Expand All @@ -64,21 +65,20 @@ describe('CreateNewWrapper', () => {

describe('createAction', () => {
it('should call handleCreateSchema callback when ok button is clicked', async () => {
const user = userEvent.setup();
render({ createNewOpen: true });

const textInput = screen.getByRole('textbox');
const okButton = screen.getByRole('button', {
name: textMock('schema_editor.create_model_confirm_button'),
});
await user.type(textInput, 'new-model');
await user.click(okButton);
await user.click(okButton());
expect(handleCreateSchema).toHaveBeenCalledWith({
name: 'new-model',
relativePath: undefined,
});
});

it('should call handleCreateSchema callback when input is focused and Enter key is pressed', async () => {
const user = userEvent.setup();
render({ createNewOpen: true });

const textInput = screen.getByRole('textbox');
Expand All @@ -92,35 +92,30 @@ describe('CreateNewWrapper', () => {
});

it('should call handleCreateSchema callback with relativePath when createPathOption is set and ok button is clicked', async () => {
const user = userEvent.setup();
render({ createNewOpen: true, createPathOption: true });

const textInput = screen.getByRole('textbox');
const okButton = screen.getByRole('button', {
name: textMock('schema_editor.create_model_confirm_button'),
});
await user.type(textInput, 'new-model');
await user.click(okButton);
await user.click(okButton());
expect(handleCreateSchema).toHaveBeenCalledWith({
name: 'new-model',
relativePath: '',
});
});

it('should not call handleCreateSchema callback and show error message when trying to create a new model with the same name as an existing one when ok button is clicked', async () => {
const user = userEvent.setup();
const newModelName = dataModel1NameMock;
const errMessage = textMock('schema_editor.error_model_name_exists', { newModelName });
render({ createNewOpen: true, dataModels: [jsonMetadata1Mock] });

const textInput = screen.getByRole('textbox');
const okButton = screen.getByRole('button', {
name: textMock('schema_editor.create_model_confirm_button'),
});

await user.type(textInput, newModelName);
expect(screen.queryByText(errMessage)).not.toBeInTheDocument();
await user.type(textInput, newModelName);

await user.click(okButton);

expect(okButton()).toBeDisabled();
expect(handleCreateSchema).not.toHaveBeenCalled();
expect(screen.getByText(errMessage)).toBeInTheDocument();
});
Expand All @@ -131,16 +126,43 @@ describe('CreateNewWrapper', () => {
});
render({ createNewOpen: true, dataModels: [jsonMetadata1Mock] });

const okButton = screen.getByRole('button', {
name: textMock('schema_editor.create_model_confirm_button'),
await userWithNoPointerEventCheck.click(okButton());

expect(handleCreateSchema).not.toHaveBeenCalled();
});

it('should not allow a name already in use in applicationmetadata json file', async () => {
const user = userEvent.setup();

const dataTypeName = 'testmodel';
const queryClient = createQueryClientMock();
queryClient.setQueryData([QueryKey.AppMetadata, org, app], {
dataTypes: [{ id: dataTypeName }],
});
render({ createNewOpen: true, dataModels: [jsonMetadata1Mock] }, queryClient);

await userWithNoPointerEventCheck.click(okButton);
await user.type(screen.getByRole('textbox'), dataTypeName);
expect(
screen.getByText(textMock('schema_editor.error_data_type_name_exists')),
).toBeInTheDocument();

expect(handleCreateSchema).not.toHaveBeenCalled();
expect(okButton()).toBeDisabled();
});
});
});

const render = (props: Partial<CreateNewWrapperProps> = {}) =>
renderRtl(<CreateNewWrapper {...defaultProps} {...props} />);
const okButton = () => {
return screen.getByRole('button', {
name: textMock('schema_editor.create_model_confirm_button'),
});
};

const render = (
props: Partial<CreateNewWrapperProps> = {},
queryClient = createQueryClientMock(),
) => {
renderWithProviders(<CreateNewWrapper {...defaultProps} {...props} />, {
startUrl: `${APP_DEVELOPMENT_BASENAME}/${org}/${app}/ui-editor`,
queryClient,
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { PlusIcon } from '@studio/icons';
import { extractModelNamesFromMetadataList } from '../../../../utils/metadataUtils';
import type { DataModelMetadata } from 'app-shared/types/DataModelMetadata';
import { StudioButton, StudioPopover } from '@studio/components';
import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
import { useAppMetadataQuery } from 'app-shared/hooks/queries';

export interface CreateNewWrapperProps {
disabled: boolean;
Expand All @@ -26,52 +28,54 @@ export function CreateNewWrapper({
const { t } = useTranslation();
const [newModelName, setNewModelName] = useState('');
const [nameError, setNameError] = useState('');
const [confirmedWithReturn, setConfirmedWithReturn] = useState(false);

const { org, app } = useStudioEnvironmentParams();
const { data: appMetadata } = useAppMetadataQuery(org, app);
const modelNames = extractModelNamesFromMetadataList(dataModels);

const relativePath = createPathOption ? '' : undefined;

const nameIsValid = () => newModelName.match(/^[a-zA-Z][a-zA-Z0-9_.\-æÆøØåÅ ]*$/);
const validateName = () => setNameError(!nameIsValid() ? 'Invalid name' : '');

const onInputBlur = () => {
if (confirmedWithReturn) {
setConfirmedWithReturn(false);
return;
}
validateName();
};
const onNameChange = (e: any) => {
const name = e.target.value || '';
if (nameError) {
setNameError('');
}
setNewModelName(name);
validateName(name);
};
const onCreateConfirmClick = () => {
if (nameError || !newModelName || !nameIsValid()) {

const dataTypeWithNameExists = (id: string) => {
return appMetadata.dataTypes?.find(
(dataType) => dataType.id.toLowerCase() === id.toLowerCase(),
);
};

const nameValidationRegex = /^[a-zA-Z][a-zA-Z0-9_.\-æÆøØåÅ ]*$/;
const validateName = (name: string) => {
if (!name || !name.match(nameValidationRegex)) {
setNameError(t('schema_editor.invalid_datamodel_name'));
return;
}
if (modelNames.includes(newModelName)) {
setNameError(t('schema_editor.error_model_name_exists', { newModelName }));
if (modelNames.includes(name)) {
setNameError(t('schema_editor.error_model_name_exists', { newModelName: name }));
return;
}
if (dataTypeWithNameExists(name)) {
setNameError(t('schema_editor.error_data_type_name_exists'));
return;
}
setNameError('');
};

const onCreateConfirmClick = () => {
handleCreateSchema({
name: newModelName,
relativePath,
});
setNewModelName('');
setNameError('');
};
const handleReturnButtonConfirm = () => {
validateName();
onCreateConfirmClick();
setConfirmedWithReturn(true);
};

const onKeyUp = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleReturnButtonConfirm();
onCreateConfirmClick();
}
};

Expand All @@ -92,13 +96,13 @@ export function CreateNewWrapper({
id='newModelInput'
label={t('schema_editor.create_model_description')}
onChange={onNameChange}
onBlur={onInputBlur}
onKeyUp={onKeyUp}
error={nameError && <ErrorMessage>{nameError}</ErrorMessage>}
/>
<StudioButton
color='second'
onClick={onCreateConfirmClick}
disabled={!newModelName || !!nameError}
style={{ marginTop: 22 }}
variant='secondary'
size='small'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import userEvent from '@testing-library/user-event';
import { textMock } from '@studio/testing/mocks/i18nMock';
import type { QueryClient } from '@tanstack/react-query';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
import { fileSelectorInputId } from '@studio/testing/testids';
import { app, fileSelectorInputId, org } from '@studio/testing/testids';
import { renderWithProviders } from '../../../../test/mocks';
import type { ServicesContextProps } from 'app-shared/contexts/ServicesContext';
import { createApiErrorMock } from 'app-shared/mocks/apiErrorMock';
import { QueryKey } from 'app-shared/types/QueryKey';

const user = userEvent.setup();

Expand Down Expand Up @@ -100,6 +101,42 @@ describe('XSDUpload', () => {
expect(await screen.findByRole('alert')).toHaveTextContent(textMock(`api_errors.${errorCode}`));
});

it('does not allow uploading with duplicate datatypes', async () => {
const file = new File(['hello'], 'hello.xsd', { type: 'text/xml' });
const queryClient = createQueryClientMock();
queryClient.setQueryData([QueryKey.AppMetadata, org, app], {
dataTypes: [{ id: 'hello' }],
});
renderXsdUpload({
queryClient: queryClient,
});

await clickUploadButton();

const fileInput = screen.getByTestId(fileSelectorInputId);

await user.upload(fileInput, file);

expect(await screen.findByRole('alert')).toHaveTextContent(
textMock('schema_editor.error_data_type_name_exists'),
);
});

it('does not allow uploading with invalid name', async () => {
const file = new File(['$-_123'], '$-_123.xsd', { type: 'text/xml' });
renderXsdUpload();

await clickUploadButton();

const fileInput = screen.getByTestId(fileSelectorInputId);

await user.upload(fileInput, file);

expect(await screen.findByRole('alert')).toHaveTextContent(
textMock('app_data_modelling.upload_xsd_invalid_error'),
);
});

it('shows a custom generic error message', async () => {
const file = new File(['hello'], 'hello.xsd', { type: 'text/xml' });
renderXsdUpload({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type { ApiError } from 'app-shared/types/api/ApiError';
import { toast } from 'react-toastify';
import type { MetadataOption } from '../../../../types/MetadataOption';
import { fileSelectorInputId } from '@studio/testing/testids';
import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
import { useAppMetadataQuery } from 'app-shared/hooks/queries';

export interface XSDUploadProps {
selectedOption?: MetadataOption;
Expand All @@ -21,6 +23,8 @@ export const XSDUpload = ({
uploaderButtonVariant,
}: XSDUploadProps) => {
const { t } = useTranslation();
const { org, app } = useStudioEnvironmentParams();
const { data: appMetadata } = useAppMetadataQuery(org, app);
const { mutate: uploadDataModel, isPending: uploading } = useUploadDataModelMutation(
selectedOption?.value?.repositoryRelativeUrl,
{
Expand All @@ -40,7 +44,27 @@ export const XSDUpload = ({
});
};

const handleInvalidFileName = () => toast.error(t('app_data_modelling.upload_xsd_invalid_error'));
const fileNameRegEx: RegExp = /^[a-zA-Z][a-zA-Z0-9_.\-æÆøØåÅ ]*$/;

const validateFileName = (fileName: string): boolean => {
const nameFollowsRegexRules = Boolean(fileName.match(fileNameRegEx));
if (!nameFollowsRegexRules) {
toast.error(t('app_data_modelling.upload_xsd_invalid_error'));
return false;
}

const fileNameWithoutExtension = fileName.split('.').slice(0, -1).join('.');
const duplicateDataType = Boolean(
appMetadata.dataTypes?.find(
(dataType) => dataType.id.toLowerCase() === fileNameWithoutExtension.toLowerCase(),
),
);
if (duplicateDataType) {
toast.error(t('schema_editor.error_data_type_name_exists'));
return false;
}
return true;
};

return (
<span ref={uploadButton}>
Expand All @@ -53,8 +77,10 @@ export const XSDUpload = ({
variant={uploaderButtonVariant}
ref={fileInputRef}
uploaderButtonText={uploadButtonText}
fileNameRegEx={/^[a-zA-Z][a-zA-Z0-9_.\-æÆøØåÅ ]*$/}
onInvalidFileName={handleInvalidFileName}
customFileNameValidation={{
validateFileName,
onInvalidFileName: () => {},
}}
dataTestId={fileSelectorInputId}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmen
import { toast } from 'react-toastify';
import { StudioPageSpinner } from '@studio/components';
import { useTranslation } from 'react-i18next';
import { useAppVersionQuery } from 'app-shared/hooks/queries';
import { useAppMetadataQuery, useAppVersionQuery } from 'app-shared/hooks/queries';
import { useUpdateLayoutSetIdMutation } from '../../hooks/mutations/useUpdateLayoutSetIdMutation';
import { useAddLayoutSetMutation } from '../../hooks/mutations/useAddLayoutSetMutation';
import { useCustomReceiptLayoutSetName } from 'app-shared/hooks/useCustomReceiptLayoutSetName';
Expand All @@ -19,7 +19,7 @@ import type { MetadataForm } from 'app-shared/types/BpmnMetadataForm';
import { useAddDataTypeToAppMetadata } from '../../hooks/mutations/useAddDataTypeToAppMetadata';
import { useDeleteDataTypeFromAppMetadata } from '../../hooks/mutations/useDeleteDataTypeFromAppMetadata';
import { useSettingsModalContext } from '../../contexts/SettingsModalContext';
import { useAppMetadataQuery, useAppPolicyQuery } from '../../hooks/queries';
import { useAppPolicyQuery } from '../../hooks/queries';
import type { OnProcessTaskEvent } from '@altinn/process-editor/types/OnProcessTask';
import { OnProcessTaskAddHandler } from './handlers/OnProcessTaskAddHandler';
import { OnProcessTaskRemoveHandler } from './handlers/OnProcessTaskRemoveHandler';
Expand Down
Loading