From b983c8568a96362b96750b7a7a4923758f211a4d Mon Sep 17 00:00:00 2001 From: TheoryOfNekomata Date: Wed, 16 Sep 2020 23:33:31 +0800 Subject: [PATCH] Add tests Improve test cases. Also merged formatFileSize and formatRawFileSize through specifying a separate boolean argument to switch between modes. Add Node support for File and Blob (TODO test each implementation) --- .gitignore | 1 + package.json | 16 +++++------ src/calculateHash.test.ts | 9 +++---- src/calculateHash.ts | 2 +- src/dataUriToBlob.ts | 34 ++++++++++++++++------- src/formatFileSize.test.ts | 37 +++++++++++++++++++++++-- src/formatFileSize.ts | 14 +++++++--- src/formatRawFileSize.test.ts | 51 ----------------------------------- src/formatRawFileSize.ts | 20 -------------- src/global.d.ts | 1 + src/index.test.ts | 31 +++++++++++++++++++++ src/index.ts | 12 ++------- src/isValidFileName.test.ts | 28 +++++++++---------- src/isValidFileName.ts | 24 ++++++----------- src/isValidMimeType.test.ts | 50 +++++++++++++++++++++++----------- src/isValidMimeType.ts | 31 +++++++++------------ yarn.lock | 5 ++++ 17 files changed, 190 insertions(+), 176 deletions(-) delete mode 100644 src/formatRawFileSize.test.ts delete mode 100644 src/formatRawFileSize.ts create mode 100644 src/global.d.ts create mode 100644 src/index.test.ts diff --git a/.gitignore b/.gitignore index 787e0fc..e336964 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules dist .idea/ +coverage/ diff --git a/package.json b/package.json index b189bf3..b5c87da 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -19,7 +19,8 @@ }, "peerDependencies": { "crypto-js": "^4.0.0", - "numeral": "^2.0.6" + "numeral": "^2.0.6", + "node-blob": "^0.0.2" }, "husky": { "hooks": { @@ -30,20 +31,19 @@ "author": "TheoryOfNekomata ", "repository": { "type": "git", - "url":"https://code.modal.sh/TheoryOfNekomata/file-commons.git" + "url": "https://code.modal.sh/TheoryOfNekomata/file-commons.git" }, "module": "dist/file-commons.esm.js", "devDependencies": { "@types/crypto-js": "^3.1.47", "@types/numeral": "^0.0.28", + "crypto-js": "^4.0.0", "fast-check": "^2.3.0", "husky": "^4.3.0", + "numeral": "^2.0.6", "tsdx": "^0.13.3", "tslib": "^2.0.1", - "typescript": "^4.0.2" - }, - "dependencies": { - "crypto-js": "^4.0.0", - "numeral": "^2.0.6" + "typescript": "^4.0.2", + "node-blob": "^0.0.2" } } diff --git a/src/calculateHash.test.ts b/src/calculateHash.test.ts index 1b05336..dfec410 100644 --- a/src/calculateHash.test.ts +++ b/src/calculateHash.test.ts @@ -15,11 +15,8 @@ it('should accept a minimum of 1 argument', () => { it('should return a string', () => { fc.assert( - fc.property( - fc.string(), - s => { - expect(typeof calculateHash(s)).toBe('string') - } - ) + fc.property(fc.string(), s => { + expect(typeof calculateHash(s)).toBe('string') + }) ) }) diff --git a/src/calculateHash.ts b/src/calculateHash.ts index 33fbdc2..28c5622 100644 --- a/src/calculateHash.ts +++ b/src/calculateHash.ts @@ -1,4 +1,4 @@ -import {LibWordArray} from 'crypto-js' +import { LibWordArray } from 'crypto-js' import sha512 from 'crypto-js/sha512' import Hex from 'crypto-js/enc-hex' diff --git a/src/dataUriToBlob.ts b/src/dataUriToBlob.ts index 868ef86..64a2720 100644 --- a/src/dataUriToBlob.ts +++ b/src/dataUriToBlob.ts @@ -1,30 +1,44 @@ +import NodeBlob from 'node-blob' + type DataUriToBlob = (dataUri: string, name?: string) => Blob const DATA_URI_PREFIX = 'data:' const DATA_TYPE_DELIMITER = ';' const DATA_START = ',' -const dataUriToBlob: DataUriToBlob = (dataUri, name) => { - if (typeof dataUri as unknown !== 'string') { +interface NodeFileConfig { + lastModified: number +} + +class NodeFile extends NodeBlob { + public readonly name: string + public readonly lastModified: number + constructor(blobParts: unknown[], name: string, config: Partial) { + super(blobParts, config) + this.name = name + this.lastModified = typeof config.lastModified! === 'number' ? config.lastModified : Date.now() + } +} + +const dataUriToBlob: DataUriToBlob = (dataUri, name?) => { + if ((typeof dataUri as unknown) !== 'string') { throw TypeError('Argument should be a string.') } - const [encoding, base64] = dataUri - .slice(DATA_URI_PREFIX.length) - .split(DATA_START) + const [encoding, base64] = dataUri.slice(DATA_URI_PREFIX.length).split(DATA_START) const binary = atob(base64) - const [type,] = encoding.split(DATA_TYPE_DELIMITER) + const [type] = encoding.split(DATA_TYPE_DELIMITER) const ab = new ArrayBuffer(binary.length) const ia = new Uint8Array(ab) for (let i = 0; i < binary.length; i++) { ia[i] = binary.charCodeAt(i) } if (typeof name! === 'string') { - return new File([ab], name, { type, }) + const FileCtor = typeof window !== 'undefined' ? window.File : NodeFile + return new FileCtor([ab], name, { type }) } - return new Blob([ab], { type, }) + const BlobCtor = typeof window !== 'undefined' ? window.Blob : NodeBlob + return new BlobCtor([ab], { type }) } export default dataUriToBlob - -// TODO make code portable to Node! Maybe return a buffer instead of blob? diff --git a/src/formatFileSize.test.ts b/src/formatFileSize.test.ts index bb73e76..3bfe1bb 100644 --- a/src/formatFileSize.test.ts +++ b/src/formatFileSize.test.ts @@ -9,8 +9,8 @@ it('should be a function', () => { expect(typeof formatFileSize).toBe('function') }) -it('should take 1 argument', () => { - expect(formatFileSize).toHaveLength(1) +it('should accept 2 arguments', () => { + expect(formatFileSize).toHaveLength(2) }) describe('on numeric arguments', () => { @@ -53,3 +53,36 @@ describe('on non-numeric arguments', () => { ) }) }) + +describe('on raw', () => { + it('should throw an error on non-numeric arguments', () => { + fc.assert( + fc.property( + fc.anything().filter(v => typeof v !== 'number'), + v => { + expect(() => formatFileSize(v as number, true)).toThrow(TypeError) + } + ) + ) + }) + + it('should throw an error on NaN', () => { + expect(() => formatFileSize(NaN, true)).toThrow(RangeError) + }) + + it('should return string on numeric values', () => { + fc.assert( + fc.property(fc.integer(), v => { + expect(typeof formatFileSize(v, true)).toBe('string') + }) + ) + }) + + it('should format numeric values', () => { + fc.assert( + fc.property(fc.integer(), v => { + expect(formatFileSize(v, true)).toMatch(/^-?\d[,\d]* .?B$/) + }) + ) + }) +}) diff --git a/src/formatFileSize.ts b/src/formatFileSize.ts index 3a7c1a5..41c5873 100644 --- a/src/formatFileSize.ts +++ b/src/formatFileSize.ts @@ -1,9 +1,9 @@ import numeral from 'numeral' -type FormatFileSize = (n: number) => string +type FormatFileSize = (n: number, raw?: boolean) => string -const formatFileSize: FormatFileSize = n => { - if (typeof n as unknown !== 'number') { +const formatFileSize: FormatFileSize = (n, raw = false) => { + if ((typeof n as unknown) !== 'number') { throw TypeError('Argument should be a number.') } @@ -11,8 +11,14 @@ const formatFileSize: FormatFileSize = n => { throw RangeError('Cannot format NaN.') } - const base = numeral(Math.abs(n)).format(Math.abs(n) < 1000 ? '0 b' : '0.00 b') + if (raw) { + const absValue = Math.abs(n) + const base = numeral(absValue < 1000 ? absValue : 999).format('0 b') + const suffix = base.slice(base.indexOf(' ') + ' '.length) + return `${n < 0 ? '-' : ''}${numeral(absValue).format('0,0')} ${suffix}` + } + const base = numeral(Math.abs(n)).format(Math.abs(n) < 1000 ? '0 b' : '0.00 b') return `${n < 0 ? '-' : ''}${base}` } diff --git a/src/formatRawFileSize.test.ts b/src/formatRawFileSize.test.ts deleted file mode 100644 index 303edbf..0000000 --- a/src/formatRawFileSize.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import * as fc from 'fast-check' -import formatRawFileSize from './formatRawFileSize' - -it('should exist', () => { - expect(formatRawFileSize).toBeDefined() -}) - -it('should be a function', () => { - expect(typeof formatRawFileSize).toBe('function') -}) - -it('should take 1 argument', () => { - expect(formatRawFileSize).toHaveLength(1) -}) - -it('should throw an error on non-numeric arguments', () => { - fc.assert( - fc.property( - fc.anything().filter(v => typeof v !== 'number'), - v => { - expect(() => formatRawFileSize(v as number)).toThrow(TypeError) - } - ) - ) -}) - -it('should throw an error on NaN', () => { - expect(() => formatRawFileSize(NaN)).toThrow(RangeError) -}) - -it('should return string on numeric values', () => { - fc.assert( - fc.property( - fc.integer(), - v => { - expect(typeof formatRawFileSize(v)).toBe('string') - } - ) - ) -}) - -it('should format numeric values', () => { - fc.assert( - fc.property( - fc.integer(), - v => { - expect(formatRawFileSize(v)).toMatch(/^-?\d[,\d]* .?B$/) - } - ) - ) -}) diff --git a/src/formatRawFileSize.ts b/src/formatRawFileSize.ts deleted file mode 100644 index 4ab00c8..0000000 --- a/src/formatRawFileSize.ts +++ /dev/null @@ -1,20 +0,0 @@ -import numeral from 'numeral' - -type FormatRawFileSize = (n: number) => string - -const formatRawFileSize: FormatRawFileSize = n => { - if (typeof n as unknown !== 'number') { - throw TypeError('Argument should be a number.') - } - - if (isNaN(n)) { - throw RangeError('Cannot format NaN.') - } - - const absValue = Math.abs(n) - const base = numeral(absValue < 1000 ? absValue : 999).format('0 b') - const suffix = base.slice(base.indexOf(' ') + ' '.length) - return `${n < 0 ? '-' : ''}${numeral(absValue).format('0,0')} ${suffix}` -} - -export default formatRawFileSize diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 0000000..8ad70fb --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1 @@ +declare module 'node-blob' diff --git a/src/index.test.ts b/src/index.test.ts new file mode 100644 index 0000000..a1a27cc --- /dev/null +++ b/src/index.test.ts @@ -0,0 +1,31 @@ +import * as index from './index' + +describe('calculateHash', () => { + it('should exist', () => { + expect(index.calculateHash).toBeDefined() + }) +}) + +describe('dataUriToBlob', () => { + it('should exist', () => { + expect(index.dataUriToBlob).toBeDefined() + }) +}) + +describe('formatFileSize', () => { + it('should exist', () => { + expect(index.formatFileSize).toBeDefined() + }) +}) + +describe('isValidFileName', () => { + it('should exist', () => { + expect(index.isValidFileName).toBeDefined() + }) +}) + +describe('isValidMimeType', () => { + it('should exist', () => { + expect(index.isValidMimeType).toBeDefined() + }) +}) diff --git a/src/index.ts b/src/index.ts index 0dac6d4..9f422b1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,7 @@ import calculateHash from './calculateHash' import dataUriToBlob from './dataUriToBlob' import formatFileSize from './formatFileSize' -import formatRawFileSize from './formatRawFileSize' -import isValidFilename from './isValidFileName' +import isValidFileName from './isValidFileName' import isValidMimeType from './isValidMimeType' -export { - calculateHash, - dataUriToBlob, - formatFileSize, - formatRawFileSize, - isValidFilename, - isValidMimeType, -} +export { calculateHash, dataUriToBlob, formatFileSize, isValidFileName, isValidMimeType } diff --git a/src/isValidFileName.test.ts b/src/isValidFileName.test.ts index c07013c..3e619da 100644 --- a/src/isValidFileName.test.ts +++ b/src/isValidFileName.test.ts @@ -18,7 +18,7 @@ it('should throw an error given invalid param shapes', () => { fc.property( fc.object().filter(o => !('validExtensions' in o)), params => { - expect(() => isValidFilename(params as unknown as IsValidFilenameConfig)).toThrow(TypeError) + expect(() => isValidFilename((params as unknown) as IsValidFilenameConfig)).toThrow(TypeError) } ) ) @@ -29,10 +29,10 @@ describe('on valid extensions', () => { fc.assert( fc.property( fc.record({ - validExtensions: fc.anything().filter(e => !(typeof e === 'string' || Array.isArray(e))) + validExtensions: fc.anything().filter(e => !(typeof e === 'string' || Array.isArray(e))), }), params => { - expect(() => isValidFilename(params as unknown as IsValidFilenameConfig)).toThrow(TypeError) + expect(() => isValidFilename((params as unknown) as IsValidFilenameConfig)).toThrow(TypeError) } ) ) @@ -44,10 +44,14 @@ describe('on valid extensions', () => { fc.assert( fc.property( fc.record({ - validExtensions: fc.array(fc.anything().filter(e => typeof e !== 'string'), 1, 20), + validExtensions: fc.array( + fc.anything().filter(e => typeof e !== 'string'), + 1, + 20 + ), }), params => { - expect(() => isValidFilename(params as unknown as IsValidFilenameConfig)).toThrow(TypeError) + expect(() => isValidFilename((params as unknown) as IsValidFilenameConfig)).toThrow(TypeError) } ) ) @@ -84,12 +88,9 @@ describe('on main callable', () => { fc.property( fc.tuple( fc.record({ - validExtensions: fc.oneof( - fc.string(), - fc.array(fc.string(), 1, 20), - ), + validExtensions: fc.oneof(fc.string(), fc.array(fc.string(), 1, 20)), }), - fc.anything().filter(v => typeof v !== 'string'), + fc.anything().filter(v => typeof v !== 'string') ), ([params, maybeFileName]) => { expect(() => isValidFilename(params)(maybeFileName as string)).toThrow(TypeError) @@ -103,12 +104,9 @@ describe('on main callable', () => { fc.property( fc.tuple( fc.record({ - validExtensions: fc.oneof( - fc.string(), - fc.array(fc.string(), 1, 20), - ), + validExtensions: fc.oneof(fc.string(), fc.array(fc.string(), 1, 20)), }), - fc.string(), + fc.string() ), ([params, maybeFileName]) => { expect(typeof isValidFilename(params)(maybeFileName)).toBe('boolean') diff --git a/src/isValidFileName.ts b/src/isValidFileName.ts index a8352b1..5e5124f 100644 --- a/src/isValidFileName.ts +++ b/src/isValidFileName.ts @@ -16,8 +16,8 @@ const isValidFileName: IsValidFileName = config => { .split(MULTIPLE_VALID_EXTENSION_DELIMITER) .filter(p => p.trim().length > 0) } else if ( - Array.isArray(maybeValidExtensions) - && (config.validExtensions as unknown[]).every(s => typeof s === 'string') + Array.isArray(maybeValidExtensions) && + (config.validExtensions as unknown[]).every(s => typeof s === 'string') ) { if ((config.validExtensions as unknown[]).length < 1) { throw RangeError('There must be at least 1 valid extension defined.') @@ -28,14 +28,12 @@ const isValidFileName: IsValidFileName = config => { } // strip . in valid extensions for easier matching - const validExtensions = validExtensionsArray.map(e => ( - e.startsWith(EXTENSION_DELIMITER) - ? e.slice(EXTENSION_DELIMITER.length) - : e - )) + const validExtensions = validExtensionsArray.map(e => + e.startsWith(EXTENSION_DELIMITER) ? e.slice(EXTENSION_DELIMITER.length) : e + ) return maybeFileName => { - if (typeof maybeFileName as unknown !== 'string') { + if ((typeof maybeFileName as unknown) !== 'string') { throw TypeError('Argument should be a string.') } @@ -44,19 +42,13 @@ const isValidFileName: IsValidFileName = config => { if (config.allowMultipleExtensions) { return validExtensions.reduce( - (isValid, validExtension) => ( - isValid - || extensions.includes(validExtension) - ), + (isValid, validExtension) => isValid || extensions.includes(validExtension), false ) } return validExtensions.reduce( - (isValid, validExtension) => ( - isValid - || extensions.join(EXTENSION_DELIMITER).endsWith(validExtension) - ), + (isValid, validExtension) => isValid || extensions.join(EXTENSION_DELIMITER).endsWith(validExtension), false ) } diff --git a/src/isValidMimeType.test.ts b/src/isValidMimeType.test.ts index 46902ce..54f9897 100644 --- a/src/isValidMimeType.test.ts +++ b/src/isValidMimeType.test.ts @@ -18,7 +18,7 @@ it('should throw an error given invalid param shapes', () => { fc.property( fc.object().filter(o => !('validMimeTypes' in o)), params => { - expect(() => isValidMimeType(params as unknown as IsValidMimeTypeConfig)).toThrow(TypeError) + expect(() => isValidMimeType((params as unknown) as IsValidMimeTypeConfig)).toThrow(TypeError) } ) ) @@ -29,10 +29,10 @@ describe('on valid MIME types', () => { fc.assert( fc.property( fc.record({ - validMimeTypes: fc.anything().filter(e => !(typeof e === 'string' || Array.isArray(e))) + validMimeTypes: fc.anything().filter(e => !(typeof e === 'string' || Array.isArray(e))), }), params => { - expect(() => isValidMimeType(params as unknown as IsValidMimeTypeConfig)).toThrow(TypeError) + expect(() => isValidMimeType((params as unknown) as IsValidMimeTypeConfig)).toThrow(TypeError) } ) ) @@ -44,10 +44,14 @@ describe('on valid MIME types', () => { fc.assert( fc.property( fc.record({ - validMimeTypes: fc.array(fc.anything().filter(e => typeof e !== 'string'), 1, 20), + validMimeTypes: fc.array( + fc.anything().filter(e => typeof e !== 'string'), + 1, + 20 + ), }), params => { - expect(() => isValidMimeType(params as unknown as IsValidMimeTypeConfig)).toThrow(TypeError) + expect(() => isValidMimeType((params as unknown) as IsValidMimeTypeConfig)).toThrow(TypeError) } ) ) @@ -84,12 +88,9 @@ describe('on main callable', () => { fc.property( fc.tuple( fc.record({ - validMimeTypes: fc.oneof( - fc.string(), - fc.array(fc.string(), 1, 20), - ), + validMimeTypes: fc.oneof(fc.string(), fc.array(fc.string(), 1, 20)), }), - fc.anything().filter(v => typeof v !== 'string'), + fc.anything().filter(v => typeof v !== 'string') ), ([params, maybeFileName]) => { expect(() => isValidMimeType(params)(maybeFileName as string)).toThrow(TypeError) @@ -103,12 +104,9 @@ describe('on main callable', () => { fc.property( fc.tuple( fc.record({ - validMimeTypes: fc.oneof( - fc.string(), - fc.array(fc.string(), 1, 20), - ), + validMimeTypes: fc.oneof(fc.string(), fc.array(fc.string(), 1, 20)), }), - fc.string(), + fc.string() ), ([params, maybeFileName]) => { expect(typeof isValidMimeType(params)(maybeFileName)).toBe('boolean') @@ -116,4 +114,26 @@ describe('on main callable', () => { ) ) }) + + it('should return true when catch-all MIME type is specified', () => { + fc.assert( + fc.property(fc.tuple(fc.array(fc.string(), 1, 20), fc.string()), ([validMimeTypes, maybeFileName]) => { + expect(isValidMimeType({ validMimeTypes: [...validMimeTypes, '*/*'] })(maybeFileName)).toBe(true) + }) + ) + }) + + it('should return true when catch-all for specified MIME type class is specified', () => { + fc.assert( + fc.property( + fc.tuple( + fc.string().filter(s => !s.includes('/')), + fc.string().filter(s => !s.includes('/')) + ), + ([mimeTypeClass, test]) => { + expect(isValidMimeType({ validMimeTypes: [`${mimeTypeClass}/*`] })(`${mimeTypeClass}/${test}`)).toBe(true) + } + ) + ) + }) }) diff --git a/src/isValidMimeType.ts b/src/isValidMimeType.ts index 3fe1b8e..7935c1b 100644 --- a/src/isValidMimeType.ts +++ b/src/isValidMimeType.ts @@ -19,8 +19,8 @@ const isValidMimeType: IsValidMimeType = config => { .split(MULTIPLE_VALID_MIME_TYPE_DELIMITER) .filter(p => p.trim().length > 0) } else if ( - Array.isArray(maybeValidMimeTypes) - && (config.validMimeTypes as unknown[]).every(s => typeof s === 'string') + Array.isArray(maybeValidMimeTypes) && + (config.validMimeTypes as unknown[]).every(s => typeof s === 'string') ) { if ((config.validMimeTypes as unknown[]).length < 1) { throw RangeError('There must be at least 1 valid extension defined.') @@ -31,7 +31,7 @@ const isValidMimeType: IsValidMimeType = config => { } return mimeType => { - if (typeof mimeType as unknown !== 'string') { + if ((typeof mimeType as unknown) !== 'string') { throw TypeError('Argument should be a string.') } @@ -40,21 +40,16 @@ const isValidMimeType: IsValidMimeType = config => { return true } - return ( - validMimeTypes.reduce( - (isValid, validMimeType) => { - const [type, format] = validMimeType.split(MIME_TYPE_FORMAT_DELIMITER) - - // maybe short circuit matching format catch-all valid MIME types? - if (format === MIME_TYPE_FORMAT_CATCH_ALL) { - return isValid || mimeType.startsWith(type + MIME_TYPE_FORMAT_DELIMITER) - } - - return isValid || mimeType === validMimeType - }, - false, - ) - ) + return validMimeTypes.reduce((isValid, validMimeType) => { + const [type, format] = validMimeType.split(MIME_TYPE_FORMAT_DELIMITER) + + // maybe short circuit matching format catch-all valid MIME types? + if (format === MIME_TYPE_FORMAT_CATCH_ALL) { + return isValid || mimeType.startsWith(type + MIME_TYPE_FORMAT_DELIMITER) + } + + return isValid || mimeType === validMimeType + }, false) } } diff --git a/yarn.lock b/yarn.lock index b3a4041..045d753 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4215,6 +4215,11 @@ no-case@^2.2.0: dependencies: lower-case "^1.1.1" +node-blob@^0.0.2: + version "0.0.2" + resolved "https://kabahagi-922648072964.d.codeartifact.ap-southeast-1.amazonaws.com:443/npm/core/node-blob/-/node-blob-0.0.2.tgz#12abb5e722dc4bc396f85c2c2f0073e25eafc0fd" + integrity sha512-82wiGzMht96gPQDUYaZBdZEVvYD9aEhU6Bt9KLCr4rADZPRd7dQVY2Yj0ZG/1vp4DhVkL49nJT/M3CiMTAt3ag== + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"