Skip to content

Commit

Permalink
Bug fix: Attachment max file size validation
Browse files Browse the repository at this point in the history
Fix on attachment file size: Rules: 1) Questionnaire Item max rules 2) Props 3) Refero Constant
  • Loading branch information
einett2039121 committed Feb 12, 2024
1 parent 2256e2c commit 677e24e
Show file tree
Hide file tree
Showing 10 changed files with 417 additions and 10 deletions.
6 changes: 6 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 14.0.7

---

- Bugfix: Attachment Validation based on props max value, questionnaire item max attachment size rules and refero constant

## 14.0.5

---
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@helsenorge/refero",
"version": "14.0.5",
"version": "14.0.7",
"engines": {
"node": "^18.0.0",
"npm": ">=9.0.0"
Expand Down
215 changes: 215 additions & 0 deletions src/components/formcomponents/attachment/__tests__/attachment-spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import * as React from 'react';

import { Matcher, render, screen } from '@testing-library/react';

import userEvent from '@testing-library/user-event';

import '@testing-library/jest-dom';

import {
MimeType_For_Test_Util as MIME_TYPES_TEST,
createMockAttachmentProps,
createMockFile,
createMockQuestionnaireItem,
createMockQuestionnaireItemWithEmptyValue,
} from '../mockUtil';
import { Resources } from '../../../../util/resources';
import { convertBytesToMBString, convertMBToBytes } from '../attachmentUtil';
import constants from '../../../../constants';
import { AttachmentComponent } from '../attachment';

beforeAll(() => {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // Deprecated but included for compatibility
removeListener: jest.fn(), // Deprecated but included for compatibility
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
});

const mockFileTooLarge = 'Filstørrelsen må være mindre enn {0} MB';
const wrongFileTypeMsg = 'Feil filtype';
const mockFileName = 'testFile.txt';
const defaulMockSize = 3;
const qItemMockName = 'qItem';
const PLAIN_TEXT_3_MB = createMockFile(mockFileName, MIME_TYPES_TEST.PlainText, convertMBToBytes(defaulMockSize));
const PLAIN_TEXT_4_MB = createMockFile(mockFileName, MIME_TYPES_TEST.PlainText, convertMBToBytes(4));
const PLAIN_TEXT_5_MB = createMockFile(mockFileName, MIME_TYPES_TEST.PlainText, convertMBToBytes(5));
const JPEG_5_MB = createMockFile(mockFileName, MIME_TYPES_TEST.JPG, convertMBToBytes(5));
const PLAIN_TEXT_6_MB = createMockFile(mockFileName, MIME_TYPES_TEST.PlainText, convertMBToBytes(6));
const PLAIN_TEXT_30_MB = createMockFile(mockFileName, MIME_TYPES_TEST.PlainText, convertMBToBytes(30));

const mockResources: Partial<Resources> = {
validationFileMax: mockFileTooLarge,
validationFileType: wrongFileTypeMsg,
};

const expectReplacedFileSizeError = (number: any) => {
const resourceStringWithNumber = mockFileTooLarge.replace('{0}', number);
expect(screen.getByText(resourceStringWithNumber)).toBeInTheDocument();
};

async function uploadMockFile(mockFile: File | File[], label = 'Last opp fil') {
const input = screen.getByLabelText(label);
await userEvent.upload(input, mockFile);
}

export const expectNotToFindByText = (text: Matcher) => {
expect(screen.queryByText(text)).toBeNull();
};

function expectNoFileErrors() {
expect(screen.queryByText(wrongFileTypeMsg)).toBe(null);
expect(screen.queryByText(mockFileTooLarge)).toBe(null);
}

describe('<AttachmentComponent />', () => {
describe('File Type validation', () => {
it('When uploading a file - Show error if mime type is NOT among valid types', async () => {
const qItem = createMockQuestionnaireItem(qItemMockName, undefined, true);
const validTypes = [MIME_TYPES_TEST.PNG, MIME_TYPES_TEST.JPG, MIME_TYPES_TEST.PDF];
const mockProps = createMockAttachmentProps(qItem, mockResources, undefined, undefined, validTypes);
render(<AttachmentComponent {...mockProps} />);
await uploadMockFile(PLAIN_TEXT_3_MB);
expect(screen.getByText(wrongFileTypeMsg)).toBeInTheDocument();
});

it('When uploading a file - Do NOT show error file type error message when valid mime', async () => {
const qItem = createMockQuestionnaireItem(qItemMockName, undefined, false);
const validTypes = [MIME_TYPES_TEST.PNG, MIME_TYPES_TEST.JPG, MIME_TYPES_TEST.PDF, MIME_TYPES_TEST.PlainText];
const mockProps = createMockAttachmentProps(qItem, mockResources, undefined, undefined, validTypes);
render(<AttachmentComponent {...mockProps} />);
await uploadMockFile(PLAIN_TEXT_3_MB);
expectNotToFindByText(wrongFileTypeMsg);
});
});

describe('File Size validation - Questionnaire Extension', () => {
it('When uploading a file - Show resource size error if size > max rule in qItem', async () => {
const qItem = createMockQuestionnaireItem(qItemMockName, 5, true);
const validTypes = [MIME_TYPES_TEST.PlainText];
const mockProps = createMockAttachmentProps(qItem, mockResources, undefined, undefined, validTypes);
render(<AttachmentComponent {...mockProps} />);

await uploadMockFile(PLAIN_TEXT_6_MB);

screen.debug(undefined, 6000000);
expectReplacedFileSizeError(5);
expectNotToFindByText(wrongFileTypeMsg);
});

it('When uploading a file - Do NOT show resource size error if size <= max rule in qItem', async () => {
const qItem = createMockQuestionnaireItem(qItemMockName, 5, true);
const validTypes = [MIME_TYPES_TEST.PlainText];
const mockProps = createMockAttachmentProps(qItem, mockResources, undefined, undefined, validTypes);
render(<AttachmentComponent {...mockProps} />);
await uploadMockFile(PLAIN_TEXT_4_MB);
expectNoFileErrors();
});

it('When uploading a file - Do NOT show resource size error if file size excactly max rule from qItem', async () => {
const qItem = createMockQuestionnaireItem(qItemMockName, 5, true);
const validTypes = [MIME_TYPES_TEST.PlainText];
const mockProps = createMockAttachmentProps(qItem, mockResources, undefined, undefined, validTypes);
render(<AttachmentComponent {...mockProps} />);
await uploadMockFile(PLAIN_TEXT_5_MB);
expectNoFileErrors();
});

it('When uploading a file - And not set Questionnaire item max rule will be read as null and should be skipped', async () => {
const qItem = createMockQuestionnaireItemWithEmptyValue('test', null);
const validTypes = [MIME_TYPES_TEST.PlainText];
const mockProps = createMockAttachmentProps(qItem, mockResources, convertMBToBytes(4), undefined, validTypes);
render(<AttachmentComponent {...mockProps} />);
await uploadMockFile(PLAIN_TEXT_5_MB);
expectReplacedFileSizeError(4);
});

it('When uploading a file - And not set Questionnaire item max rule with undefined value should be skipped', async () => {
const qItem = createMockQuestionnaireItemWithEmptyValue('test', undefined);
const validTypes = [MIME_TYPES_TEST.PlainText];
const mockProps = createMockAttachmentProps(qItem, mockResources, convertMBToBytes(4), undefined, validTypes);
render(<AttachmentComponent {...mockProps} />);
await uploadMockFile(PLAIN_TEXT_5_MB);
expectReplacedFileSizeError(4);
});
});

describe('File Size validation - Max Setttings From Props', () => {
it('When uploading a file - Show resource size error if filesize > Props Max', async () => {
const qItem = createMockQuestionnaireItem(qItemMockName, undefined, false);
const validTypes = [MIME_TYPES_TEST.PlainText];
const mockProps = createMockAttachmentProps(qItem, mockResources, convertMBToBytes(5), undefined, validTypes);
render(<AttachmentComponent {...mockProps} />);
await uploadMockFile(PLAIN_TEXT_6_MB);
expectReplacedFileSizeError(5);
expectNotToFindByText(wrongFileTypeMsg);
});

it('When uploading a file - Do NOT show size error message - when file size excatly == props max value', async () => {
const qItem = createMockQuestionnaireItem(qItemMockName, undefined, false);
const validTypes = [MIME_TYPES_TEST.PlainText];
const mockProps = createMockAttachmentProps(qItem, mockResources, convertMBToBytes(4), undefined, validTypes);
render(<AttachmentComponent {...mockProps} />);
await uploadMockFile(PLAIN_TEXT_4_MB);
expectNoFileErrors();
});

it('When uploading a file - Do NOT show resource size error if size == excactly props max value', async () => {
const qItem = createMockQuestionnaireItem(qItemMockName, undefined, false);
const validTypes = [MIME_TYPES_TEST.PlainText];
const mockProps = createMockAttachmentProps(qItem, mockResources, convertMBToBytes(5), undefined, validTypes);
render(<AttachmentComponent {...mockProps} />);
await uploadMockFile(PLAIN_TEXT_5_MB);
expectNoFileErrors();
});
});

describe('File validation - Prioritiy of rules', () => {
it('When uploading a file - File type errors should have priority over other errors', async () => {
const qItem = createMockQuestionnaireItem(qItemMockName, 2, true);
const validTypes = [MIME_TYPES_TEST.PlainText];
const mockProps = createMockAttachmentProps(qItem, mockResources, convertMBToBytes(4), undefined, validTypes);
render(<AttachmentComponent {...mockProps} />);
await uploadMockFile(JPEG_5_MB);
expect(screen.getByText(wrongFileTypeMsg)).toBeInTheDocument();
});

it('When uploading a file - Questionniare Item Max Rule has priority over props if both set', async () => {
const qItem = createMockQuestionnaireItem(qItemMockName, 2, true);
const validTypes = [MIME_TYPES_TEST.PlainText];
const mockProps = createMockAttachmentProps(qItem, mockResources, convertMBToBytes(4), undefined, validTypes);
render(<AttachmentComponent {...mockProps} />);
await uploadMockFile(PLAIN_TEXT_3_MB);
expectReplacedFileSizeError(2);
expectNotToFindByText(wrongFileTypeMsg);
});

it('When uploading a file - And questionnaire max rule is not set, use props max value if set', async () => {
const qItem = createMockQuestionnaireItem(qItemMockName, undefined, false);
const validTypes = [MIME_TYPES_TEST.PlainText];
const mockProps = createMockAttachmentProps(qItem, mockResources, convertMBToBytes(4), undefined, validTypes);
render(<AttachmentComponent {...mockProps} />);
await uploadMockFile(PLAIN_TEXT_5_MB);
expectReplacedFileSizeError(4);
expectNotToFindByText(wrongFileTypeMsg);
});

it('When uploading a file - Refero constant should be fallback if neither qItem rule or props', async () => {
const qItem = createMockQuestionnaireItem(qItemMockName, 2, false);
const validTypes = [MIME_TYPES_TEST.PlainText];
const mockProps = createMockAttachmentProps(qItem, mockResources, undefined, undefined, validTypes);
render(<AttachmentComponent {...mockProps} />);
await uploadMockFile(PLAIN_TEXT_30_MB);
expectReplacedFileSizeError(convertBytesToMBString(constants.MAX_FILE_SIZE));
expectNotToFindByText(wrongFileTypeMsg);
});
});
});
4 changes: 2 additions & 2 deletions src/components/formcomponents/attachment/attachment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export interface Props {
onRenderMarkdown?: (item: QuestionnaireItem, markdown: string) => string;
}

class AttachmentComponent extends React.Component<Props & ValidationProps> {
export class AttachmentComponent extends React.Component<Props & ValidationProps> {
onUpload = (files: File[], cb: (success: boolean, errormessage: TextMessage | null, uploadedFile?: UploadedFile) => void): void => {
const { uploadAttachment, path, item, onAnswerChange } = this.props;
if (uploadAttachment) {
Expand Down Expand Up @@ -203,4 +203,4 @@ class AttachmentComponent extends React.Component<Props & ValidationProps> {

const withCommonFunctionsComponent = withCommonFunctions(AttachmentComponent);
const connectedComponent = connect(mapStateToProps, mapDispatchToProps, mergeProps)(withCommonFunctionsComponent);
export default connectedComponent;
export default connectedComponent;
38 changes: 38 additions & 0 deletions src/components/formcomponents/attachment/attachmentUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export function convertMBToBytes(mb: number): number {
if (typeof mb !== 'number' || isNaN(mb)) {
throw new Error('Input must be a valid number.');
}
if (mb < 0) {
throw new Error('Input cannot be a negative number.');
}
if (!isFinite(mb)) {
throw new Error('Input must be a finite number.');
}
return Math.round(mb * 1024 * 1024);
}

export function convertBytesToMBString(bytes: number, precision: number = 0): string {
if (typeof bytes !== 'number' || isNaN(bytes)) {
throw new Error('Input must be a valid number.');
}
if (bytes < 0) {
throw new Error('Input cannot be a negative number.');
}
if (!isFinite(bytes)) {
throw new Error('Input must be a finite number.');
}
return (bytes / 1024 / 1024).toFixed(precision);
}

export function convertBytesToMB(bytes: number): number {
if (typeof bytes !== 'number' || isNaN(bytes)) {
throw new Error('Input must be a valid number.');
}
if (bytes < 0) {
throw new Error('Input cannot be a negative number.');
}
if (!isFinite(bytes)) {
throw new Error('Input must be a finite number.');
}
return bytes / 1024 / 1024;
}
26 changes: 21 additions & 5 deletions src/components/formcomponents/attachment/attachmenthtml.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import { UploadedFile } from '@helsenorge/file-upload/components/dropzone';
import { sizeIsValid, mimeTypeIsValid } from '@helsenorge/file-upload/components/dropzone/validation';
import Validation, { ValidationProps } from '@helsenorge/form/components/form/validation';

import { convertBytesToMBString, convertMBToBytes } from './attachmentUtil';
import constants, { VALID_FILE_TYPES } from '../../../constants';
import { getValidationTextExtension } from '../../../util/extension';
import { getMaxSizeExtensionValue, getValidationTextExtension } from '../../../util/extension';
import { Resources } from '../../../util/resources';

interface Props {
Expand Down Expand Up @@ -66,7 +67,8 @@ const attachmentHtml: React.SFC<Props & ValidationProps> = ({
children,
...other
}) => {
const maxFilesize = attachmentMaxFileSize ? attachmentMaxFileSize : constants.MAX_FILE_SIZE;
const getMaxValueBytes = getAttachmentMaxSizeBytesToUse(attachmentMaxFileSize, item);
const getMaxValueMBToReplace = convertBytesToMBString(getMaxValueBytes);
const validFileTypes = attachmentValidTypes ? attachmentValidTypes : VALID_FILE_TYPES;
const deleteText = resources ? resources.deleteAttachmentText : undefined;

Expand All @@ -82,13 +84,13 @@ const attachmentHtml: React.SFC<Props & ValidationProps> = ({
onOpenFile={onOpen}
uploadButtonText={uploadButtonText}
uploadedFiles={uploadedFiles}
maxFileSize={maxFilesize}
maxFileSize={getMaxValueBytes}
validMimeTypes={validFileTypes}
dontShowHardcodedText={!!deleteText}
deleteText={deleteText}
supportedFileFormatsText={resources ? resources.supportedFileFormats : undefined}
errorMessage={(file: File): string => {
return getErrorMessage(validFileTypes, maxFilesize, item, errorText, file, resources);
return getErrorMessage(validFileTypes, getMaxValueBytes, getMaxValueMBToReplace, item, errorText, file, resources);
}}
isRequired={isRequired}
wrapperClasses="page_refero__input"
Expand All @@ -107,9 +109,23 @@ const attachmentHtml: React.SFC<Props & ValidationProps> = ({
);
};

export function getAttachmentMaxSizeBytesToUse(defaultMaxProps: number | undefined, item: QuestionnaireItem): number {
if (item) {
const questionnaireMaxRuleSizeMB = getMaxSizeExtensionValue(item);
if (questionnaireMaxRuleSizeMB !== undefined) {
return convertMBToBytes(questionnaireMaxRuleSizeMB);
}
}
if (defaultMaxProps !== undefined) {
return defaultMaxProps;
}
return constants.MAX_FILE_SIZE;
}

function getErrorMessage(
validFileTypes: Array<string>,
maxFileSize: number,
maxFileSizeMBStringToReplace: string,
item: QuestionnaireItem,
genericErrorText?: string,
file?: File,
Expand All @@ -119,7 +135,7 @@ function getErrorMessage(
if (!mimeTypeIsValid(file, validFileTypes)) {
return resources.validationFileType;
} else if (!sizeIsValid(file, maxFileSize)) {
return resources.validationFileMax;
return resources.validationFileMax.replace('{0}', maxFileSizeMBStringToReplace);
}
}

Expand Down
Loading

0 comments on commit 677e24e

Please sign in to comment.