소스 검색

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
부모
커밋
b983c8568a
17개의 변경된 파일190개의 추가작업 그리고 176개의 파일을 삭제
  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 파일 보기

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

+ 8
- 8
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 <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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

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

+ 31
- 0
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()
})
})

+ 2
- 10
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 }

+ 13
- 15
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<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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

@@ -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 파일 보기

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


불러오는 중...
취소
저장