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

Bugfix - Attachment max size rules #166

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
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);
});
});
});
2 changes: 1 addition & 1 deletion 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
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
Loading