Browse Source

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)
master
TheoryOfNekomata 3 years ago
parent
commit
b983c8568a
17 changed files with 190 additions and 176 deletions
  1. +1
    -0
      .gitignore
  2. +8
    -8
      package.json
  3. +3
    -6
      src/calculateHash.test.ts
  4. +1
    -1
      src/calculateHash.ts
  5. +24
    -10
      src/dataUriToBlob.ts
  6. +35
    -2
      src/formatFileSize.test.ts
  7. +10
    -4
      src/formatFileSize.ts
  8. +0
    -51
      src/formatRawFileSize.test.ts
  9. +0
    -20
      src/formatRawFileSize.ts
  10. +1
    -0
      src/global.d.ts
  11. +31
    -0
      src/index.test.ts
  12. +2
    -10
      src/index.ts
  13. +13
    -15
      src/isValidFileName.test.ts
  14. +8
    -16
      src/isValidFileName.ts
  15. +35
    -15
      src/isValidMimeType.test.ts
  16. +13
    -18
      src/isValidMimeType.ts
  17. +5
    -0
      yarn.lock

+ 1
- 0
.gitignore View File

@@ -3,3 +3,4 @@
node_modules
dist
.idea/
coverage/

+ 8
- 8
package.json View File

@@ -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 <allan.crisostomo@outlook.com>",
"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"
}
}

+ 3
- 6
src/calculateHash.test.ts View File

@@ -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')
})
)
})

+ 1
- 1
src/calculateHash.ts View File

@@ -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'



+ 24
- 10
src/dataUriToBlob.ts View File

@@ -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<NodeFileConfig>) {
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?

+ 35
- 2
src/formatFileSize.test.ts View File

@@ -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$/)
})
)
})
})

+ 10
- 4
src/formatFileSize.ts View File

@@ -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}`
}



+ 0
- 51
src/formatRawFileSize.test.ts View File

@@ -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$/)
}
)
)
})

+ 0
- 20
src/formatRawFileSize.ts View File

@@ -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

+ 1
- 0
src/global.d.ts View File

@@ -0,0 +1 @@
declare module 'node-blob'

+ 31
- 0
src/index.test.ts View File

@@ -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()
})
})

+ 2
- 10
src/index.ts View File

@@ -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 }

+ 13
- 15
src/isValidFileName.test.ts View File

@@ -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<IsValidFilenameConfig>({
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<IsValidFilenameConfig>({
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')


+ 8
- 16
src/isValidFileName.ts View File

@@ -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<boolean>(
(isValid, validExtension) => (
isValid
|| extensions.includes(validExtension)
),
(isValid, validExtension) => isValid || extensions.includes(validExtension),
false
)
}

return validExtensions.reduce<boolean>(
(isValid, validExtension) => (
isValid
|| extensions.join(EXTENSION_DELIMITER).endsWith(validExtension)
),
(isValid, validExtension) => isValid || extensions.join(EXTENSION_DELIMITER).endsWith(validExtension),
false
)
}


+ 35
- 15
src/isValidMimeType.test.ts View File

@@ -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<IsValidMimeTypeConfig>({
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<IsValidMimeTypeConfig>({
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)
}
)
)
})
})

+ 13
- 18
src/isValidMimeType.ts View File

@@ -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<boolean>(
(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<boolean>((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)
}
}



+ 5
- 0
yarn.lock View File

@@ -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"


Loading…
Cancel
Save