UNPKG

@adobe/aio-lib-files

Version:

An abstraction on top of blob cloud storage exposing a file like API

1,108 lines (938 loc) 47.8 kB
/* eslint-disable jest/expect-expect */ const { Files, FilePermissions, UrlType } = require('../lib/Files') const upath = require('upath') const stream = require('stream') const fs = require('fs-extra') jest.mock('fs-extra') const ujoin = (...args) => upath.toUnix(upath.join(...args)) const ujoinFiles = (dirs, fakeFiles) => { if (!Array.isArray(dirs)) dirs = [dirs] return fakeFiles.map(f => ujoin(...dirs, f)) } const fakeFile = 'fake/file.txt' let initWithNewCredsMock = jest.spyOn(Files.prototype, '_initWithNewCreds') beforeEach(() => { initWithNewCredsMock.mockImplementation(() => {}) }) afterEach(() => { initWithNewCredsMock.mockReset() }) describe('init', () => { test('missing implementation', async () => { await global.expectToThrowNotImplemented(Files.init.bind(Files), 'init') }) }) describe('FilePermissions', () => { test('check available permissions', async () => { const obj = { READ: 'r', WRITE: 'w', DELETE: 'd' } expect(FilePermissions).toEqual(obj) }) }) describe('UrlType', () => { test('check available UrlType', async () => { const obj = { internal: 'internal', external: 'external' } expect(UrlType).toEqual(obj) }) }) describe('missing implementation', () => { const files = new Files() test('list', async () => { await expect(files.list('anything')).rejects.toThrow('not implemented') await expect(files.list('/')).rejects.toThrow('not implemented') }) test('getFileInfo', async () => { await expect(files.getFileInfo('hello/../afile.txt')).rejects.toThrow('not implemented') }) test('delete', async () => { await expect(files.delete('anything')).rejects.toThrow('not implemented') }) test('createReadStream', async () => { await expect(files.createReadStream('anything')).rejects.toThrow('not implemented') }) test('createWriteStream', async () => { await expect(files.createWriteStream('anything')).rejects.toThrow('not implemented') }) test('getProperties', async () => { await expect(files.getProperties('anything')).rejects.toThrow('not implemented') }) test('read', async () => { await expect(files.read('anything')).rejects.toThrow('not implemented') }) test('write', async () => { await expect(files.write('anything', 'else')).rejects.toThrow('not implemented') }) test('copy', async () => { await expect(files.copy('anything', 'somewheres')).rejects.toThrow('not implemented') }) test('generatePresignURL', async () => { await expect(files.generatePresignURL('anything', { // Why call them options if they are required? expiryInSeconds: 100 })).rejects.toThrow('not implemented') }) test('_getPresignUrl', async () => { await expect(files._getPresignUrl('anything')).rejects.toThrow('not implemented') }) test('_initWithNewCreds', async () => { initWithNewCredsMock.mockRestore() await expect(files._initWithNewCreds()).rejects.toThrow('not implemented') initWithNewCredsMock = jest.spyOn(Files.prototype, '_initWithNewCreds') }) }) describe('_isRemoteRoot + _isRemotePublic protected static base class methods', () => { test('return expected booleans', () => { class Implementor extends Files { isRemotePublic (usePath) { return Files._isRemotePublic(usePath) } isRemoteRoot (usePath) { return Files._isRemoteRoot(usePath) } } const instance = new Implementor() expect(instance.isRemotePublic).toBeDefined() expect(instance.isRemotePublic('')).toBe(false) expect(instance.isRemotePublic('/')).toBe(false) expect(instance.isRemotePublic(Files.publicPrefix)).toBe(true) expect(instance.isRemotePublic('not public')).toBe(false) expect(instance.isRemoteRoot).toBeDefined() expect(instance.isRemoteRoot('/')).toBe(false) expect(instance.isRemoteRoot('')).toBe(true) expect(instance.isRemoteRoot('not the root')).toBe(false) }) }) describe('_wrapProviderRequest', () => { test('return expected results', async () => { class Implementor extends Files { async wrapProviderRequest (requestPromise, details, filePathToThrowOn404) { return this._wrapProviderRequest(requestPromise, details, filePathToThrowOn404) } } const instance = new Implementor() expect(instance.wrapProviderRequest).toBeDefined() await expect(instance.wrapProviderRequest(Promise.resolve('ok'), 'details', true)).resolves.toBe('ok') await expect(instance.wrapProviderRequest(Promise.reject(new Error('just because')), 'details', true)).rejects.toThrow('ERROR_NOT_IMPLEMENTED') }) }) describe('list :: getInfo and _listFolder mock implementations', () => { const listFolderMock = jest.spyOn(Files.prototype, '_listFolder') const getFileInfo = jest.spyOn(Files.prototype, 'getFileInfo') const fakeFiles = (dir) => ujoinFiles(dir, ['a/b/c/d.txt', 'e.jpg', 'f/g/h.html']) let files beforeEach(() => { files = new Files() listFolderMock.mockReset() getFileInfo.mockReset() // defaults that work initWithNewCredsMock.mockImplementation(() => {}) listFolderMock.mockImplementation(async (...args) => fakeFiles(...args)) getFileInfo.mockImplementation(async (...args) => { return { path: args[0] } }) }) afterAll(() => { listFolderMock.mockRestore() }) test('when path is not a valid string', async () => { await global.expectToThrowBadArg(files.list.bind(files, 123), ['filePath', 'string'], { filePath: 123 }) }) test('when list(path) is an existing file', async () => { getFileInfo.mockImplementation(async () => { return { a: 9 } }) let res = await files.list('hey.txt') expect(res).toEqual([{ a: 9 }]) getFileInfo.mockImplementation(async () => null) res = await files.list('hey.txt') expect(res).toEqual([]) expect(initWithNewCredsMock).toHaveBeenCalled() }) test('when path is an existing file with a non normalized path', async () => { const res = await files.list('hello/../afile.txt') expect(res).toEqual([expect.objectContaining({ path: 'afile.txt' })]) expect(getFileInfo).toHaveBeenCalledWith('afile.txt') expect(listFolderMock).toHaveBeenCalledTimes(0) expect(initWithNewCredsMock).toHaveBeenCalled() }) test('when path is a non existing file', async () => { const { codes } = require('../lib/./FilesError') getFileInfo.mockRejectedValue(new codes.ERROR_FILE_NOT_EXISTS({ messageValues: ['file'] })) const res = await files.list('afile.txt') expect(res).toEqual([]) expect(listFolderMock).toHaveBeenCalledTimes(0) expect(initWithNewCredsMock).toHaveBeenCalled() }) test('when path is a non existing file (2)', async () => { getFileInfo.mockImplementation(async (...args) => { return null }) const res = await files.list('afile.txt') expect(res).toEqual([]) expect(listFolderMock).toHaveBeenCalledTimes(0) expect(initWithNewCredsMock).toHaveBeenCalled() }) const testRootFolder = async (dir) => { const res = await files.list(dir) expect(res).toEqual(fakeFiles('')) expect(listFolderMock).toHaveBeenCalledTimes(1) expect(initWithNewCredsMock).toHaveBeenCalled() } test('when path is undefined (root folder)', async () => testRootFolder()) // eslint-disable-line jest/expect-expect test('when path is / (root folder)', async () => testRootFolder('/')) // eslint-disable-line jest/expect-expect test('when path is \'\' (root folder)', async () => testRootFolder('')) // eslint-disable-line jest/expect-expect test('when path is \'.\' (root folder)', async () => testRootFolder('.')) // eslint-disable-line jest/expect-expect test('when path is a non normalized directory', async () => { const res = await files.list('hello/../hi/') expect(res).toEqual(fakeFiles('hi/')) expect(listFolderMock).toHaveBeenCalledWith('hi/') }) test('when path is an empty directory', async () => { listFolderMock.mockResolvedValue([]) const res = await files.list('hello/../hi/') expect(res).toEqual([]) }) }) describe('delete', () => { test('missing _deleteFile implementation', async () => { const files = new Files() files.list = jest.fn().mockResolvedValue([fakeFile]) await global.expectToThrowNotImplemented(files.delete.bind(files, fakeFile), '_deleteFile') }) describe('list and _deleteFile mock implementations', () => { const listMock = jest.spyOn(Files.prototype, 'list') const deleteFileMock = jest.spyOn(Files.prototype, '_deleteFile') let files beforeEach(() => { files = new Files() listMock.mockReset() deleteFileMock.mockReset() deleteFileMock.mockImplementation(f => Promise.resolve(f)) }) afterAll(() => { deleteFileMock.mockRestore() listMock.mockRestore() }) test('when path is not a valid string', async () => { await global.expectToThrowBadArg(files.delete.bind(files, 123), ['filePath', 'string'], { filePath: 123, options: {} }) }) test('when progressCallback is not a valid function', async () => { await global.expectToThrowBadArg(files.delete.bind(files, 'afile', { progressCallback: 'astring' }), ['progressCallback', 'function'], { filePath: 'afile', options: { progressCallback: 'astring' } }) }) test('with a bad option', async () => { await global.expectToThrowBadArg(files.delete.bind(files, 'afile', { some__wrong__option: 'astring' }), ['some__wrong__option'], { filePath: 'afile', options: { some__wrong__option: 'astring' } }) }) const testDelete = async (listedFiles, hasPgCb) => { const progressCallback = jest.fn() const fakeRes = ujoinFiles('some/dir/', listedFiles) listMock.mockResolvedValue(fakeRes) const res = await files.delete('some/dir/', hasPgCb ? { progressCallback } : undefined) expect(res).toEqual(fakeRes) fakeRes.forEach(f => { expect(deleteFileMock).toHaveBeenCalledWith(f) }) if (hasPgCb) expect(progressCallback).toHaveBeenCalledTimes(fakeRes.length) // test logs expect(global.mockLogDebug).toHaveBeenCalledWith(expect.stringContaining('' + listedFiles.length)) } test('delete a single file without progress callback', async () => testDelete(['a/b/c/d.txt'], false)) // eslint-disable-line jest/expect-expect test('delete multiple files without progress callback', async () => testDelete(['a/b/c/d.txt', 'e.jpg', 'f/g/h.html'], false)) // eslint-disable-line jest/expect-expect test('delete a single file with progress callback', async () => testDelete(['a/b/c/d.txt'], true)) // eslint-disable-line jest/expect-expect test('delete multiple files with progress callback', async () => testDelete(['a/b/c/d.txt', 'e.jpg', 'f/g/h.html'], true)) // eslint-disable-line jest/expect-expect }) }) describe('createReadStream', () => { test('missing _createReadStream implementation', async () => { const files = new Files() await global.expectToThrowNotImplemented(files.createReadStream.bind(files, fakeFile), '_createReadStream') }) describe('_createReadStream mock implementations', () => { const createReadStreamMock = jest.spyOn(Files.prototype, '_createReadStream') let fakeRdStream let files beforeEach(() => { files = new Files() fakeRdStream = new stream.Readable() fakeRdStream.push(null) createReadStreamMock.mockReset() createReadStreamMock.mockResolvedValue(fakeRdStream) }) afterAll(() => { createReadStreamMock.mockRestore() }) test('when path is not a valid string', async () => { await global.expectToThrowBadArg(files.createReadStream.bind(files, 123), ['filePath', 'string'], { filePath: 123, options: {} }) }) test('when path is undefined', async () => { await global.expectToThrowBadArg(files.createReadStream.bind(files), ['filePath', 'required'], { filePath: undefined, options: {} }) }) test('when path is a dir (not allowed)', async () => { await global.expectToThrowBadFileType(files.createReadStream.bind(files, 'a/dir/'), 'a/dir/', { filePath: 'a/dir/', options: {} }) }) test('when options.position is not a number', async () => { await global.expectToThrowBadArg(files.createReadStream.bind(files, fakeFile, { position: 'astring' }), ['position', 'number'], { filePath: fakeFile, options: { position: 'astring' } }) }) test('when options.length is not a number', async () => { await global.expectToThrowBadArg(files.createReadStream.bind(files, fakeFile, { length: 'astring' }), ['length', 'number'], { filePath: fakeFile, options: { length: 'astring' } }) }) test('when options.position is smaller than 0', async () => { await global.expectToThrowBadArg(files.createReadStream.bind(files, fakeFile, { position: -1 }), ['position', 'greater', '0'], { filePath: fakeFile, options: { position: -1 } }) }) test('when options.length is smaller than 0', async () => { await global.expectToThrowBadArg(files.createReadStream.bind(files, fakeFile, { length: -1 }), ['length', 'greater', '0'], { filePath: fakeFile, options: { length: -1 } }) }) test('with a bad option', async () => { await global.expectToThrowBadArg(files.createReadStream.bind(files, fakeFile, { some__wrong__option: 'astring' }), ['some__wrong__option'], { filePath: fakeFile, options: { some__wrong__option: 'astring' } }) }) const testCreateReadStream = async (options) => { const res = await files.createReadStream('hello/../file', options) // test with non normalized path expect(res).toEqual(fakeRdStream) // must set defaults: if (!options) options = {} options.position = options.position || 0 expect(createReadStreamMock).toHaveBeenCalledWith('file', options) expect(initWithNewCredsMock).toHaveBeenCalled() } test('when options are undefined and path is non normalized', async () => testCreateReadStream()) // eslint-disable-line jest/expect-expect test('when options.position is a number', async () => testCreateReadStream({ position: 1 })) // eslint-disable-line jest/expect-expect test('when options.length is a number', async () => testCreateReadStream({ length: 10 })) // eslint-disable-line jest/expect-expect test('when options.length and options.positions are numbers', async () => testCreateReadStream({ position: 1, length: 10 })) // eslint-disable-line jest/expect-expect }) }) describe('createWriteStream', () => { test('missing _createWriteStream implementation', async () => { const files = new Files() await global.expectToThrowNotImplemented(files.createWriteStream.bind(files, fakeFile), '_createWriteStream') }) describe('_createWriteStream mock implementations', () => { const createWriteStreamMock = jest.spyOn(Files.prototype, '_createWriteStream') let files const fakeWriteStream = new stream.Writable() beforeEach(() => { files = new Files() createWriteStreamMock.mockReset() createWriteStreamMock.mockResolvedValue(fakeWriteStream) }) afterAll(() => { createWriteStreamMock.mockRestore() }) test('when path is not a valid string', async () => { await global.expectToThrowBadArg(files.createWriteStream.bind(files, 123), ['filePath', 'string'], { filePath: 123 }) }) test('when path is undefined', async () => { await global.expectToThrowBadArg(files.createWriteStream.bind(files, undefined), ['filePath', 'required'], { filePath: undefined }) }) test('when path is a dir (not allowed)', async () => { await global.expectToThrowBadFileType(files.createWriteStream.bind(files, 'a/dir/'), 'a/dir/', { filePath: 'a/dir/' }) }) test('when file is a non normalized path', async () => { const res = await files.createWriteStream('hello/../file') expect(res).toEqual(fakeWriteStream) expect(createWriteStreamMock).toHaveBeenCalledWith('file') expect(initWithNewCredsMock).toHaveBeenCalled() }) }) }) describe('read', () => { test('missing _createReadStream implementation', async () => { const files = new Files() await global.expectToThrowNotImplemented(files.read.bind(files, fakeFile), '_createReadStream') }) describe('createReadStream mock implementation', () => { // here we mock createReadStream which is used by read, while this test exposes implementation details that might // change (e.g. in case read doesn't use createReadStream anymore), it is easier to test for now (e.g. no need to repeat // checks of createReadStream) const createReadStreamMock = jest.spyOn(Files.prototype, 'createReadStream') const fakeContent = 'some fake content @#$%^&*()@!12-=][;"\n\trewq' const fakeOptions = { position: 1, length: 10 } let fakeRdStream let files beforeEach(() => { files = new Files() fakeRdStream = new stream.Readable() fakeRdStream.push(fakeContent) fakeRdStream.push(null) createReadStreamMock.mockReset() createReadStreamMock.mockResolvedValue(fakeRdStream) }) afterAll(() => { createReadStreamMock.mockRestore() }) test('read file', async () => { const res = await files.read(fakeFile, fakeOptions) expect(res).toBeInstanceOf(Buffer) expect(res.toString()).toEqual(fakeContent) expect(createReadStreamMock).toHaveBeenCalledWith(fakeFile, fakeOptions) }) }) }) describe('write', () => { test('missing _writeBuffer implementation', async () => { const files = new Files() await global.expectToThrowNotImplemented(files.write.bind(files, fakeFile, 'content'), '_writeBuffer') }) test('missing _writeStream implementation', async () => { const files = new Files() await global.expectToThrowNotImplemented(files.write.bind(files, fakeFile, global.createStream('hello')), '_writeStream') }) describe('_writeBuffer and _writeStream mock implementations', () => { const writeBufferMock = jest.spyOn(Files.prototype, '_writeBuffer') const writeStreamMock = jest.spyOn(Files.prototype, '_writeStream') let files beforeEach(() => { files = new Files() writeBufferMock.mockReset() writeStreamMock.mockReset() }) afterAll(() => { writeBufferMock.mockRestore() writeStreamMock.mockRestore() }) test('when path is not a valid string', async () => { await global.expectToThrowBadArg(files.write.bind(files, 123, 'content'), ['filePath', 'string'], { filePath: 123, contentType: 'String' }) }) test('when path undefined', async () => { await global.expectToThrowBadArg(files.write.bind(files, undefined, 'content'), ['filePath', 'required'], { filePath: undefined, contentType: 'String' }) }) test('when content is undefined', async () => { await global.expectToThrowBadArg(files.write.bind(files, fakeFile, undefined), ['content', 'required'], { filePath: fakeFile, contentType: undefined }) }) test('when content is null', async () => { await global.expectToThrowBadArg(files.write.bind(files, fakeFile, null), ['content', 'string', 'binary'], { filePath: fakeFile, contentType: undefined }) }) test('when content is a number', async () => { await global.expectToThrowBadArg(files.write.bind(files, fakeFile, 123), ['content', 'string', 'binary'], { filePath: fakeFile, contentType: 'Number' }) }) test('when path is a dir (not allowed)', async () => { await global.expectToThrowBadFileType(files.write.bind(files, 'a/dir/', 'content'), 'a/dir/', { filePath: 'a/dir/', contentType: 'String' }) }) const testWrite = async (content) => { writeBufferMock.mockResolvedValue(42) writeStreamMock.mockResolvedValue(42) const res = await files.write('hello/../file', content) // test with non normalized string expect(res).toEqual(42) if (content instanceof stream.Readable) { expect(writeStreamMock).toHaveBeenCalledWith('file', content) } else if (typeof content === 'string') { expect(writeBufferMock).toHaveBeenCalledWith('file', expect.any(Buffer)) expect(global.mockLogDebug).toHaveBeenCalledWith(expect.stringContaining('' + content.length)) } else { expect(writeBufferMock).toHaveBeenCalledWith('file', content) expect(global.mockLogDebug).toHaveBeenCalledWith(expect.stringContaining('' + content.length)) } expect(initWithNewCredsMock).toHaveBeenCalled() } test('when file is a non normalized path and content is a string', async () => testWrite('hello')) // eslint-disable-line jest/expect-expect test('when content is a buffer', async () => testWrite(Buffer.from('hello'))) // eslint-disable-line jest/expect-expect test('when content is a readable stream', async () => testWrite(new stream.Readable())) // eslint-disable-line jest/expect-expect }) }) describe('getProperties', () => { test('missing _getUrl implementation', async () => { const files = new Files() await global.expectToThrowNotImplemented(files._getUrl.bind(files, fakeFile), '_getUrl') }) }) describe('_getUrl mock implementation', () => { const getUrlMock = jest.spyOn(Files.prototype, '_getUrl') const getFileInfoMock = jest.spyOn(Files.prototype, 'getFileInfo') let files const fakeUrl = 'http://fake.com' beforeEach(() => { files = new Files() getFileInfoMock.mockImplementation(() => {}) getUrlMock.mockReturnValue(fakeUrl) }) afterEach(() => { getFileInfoMock.mockReset() getUrlMock.mockReset() }) afterAll(() => { getUrlMock.mockRestore() getFileInfoMock.mockRestore() }) test('when path is not a valid string', async () => { await global.expectToThrowBadArg(files.write.bind(files, 123), ['filePath', 'string'], { filePath: 123 }) }) test('when path undefined', async () => { await global.expectToThrowBadArg(files.write.bind(files, undefined), ['filePath', 'required'], { filePath: undefined }) }) test('when filePath is non normalized', async () => { await files.getProperties('hello/../file') expect(getFileInfoMock).toHaveBeenCalledWith('file') }) test('when filePath is a private file', async () => { getFileInfoMock.mockReturnValue({ isDirectory: false, isPublic: false, url: fakeUrl, internalUrl: 'http://fake.com' }) const res = await files.getProperties('a/private/file') expect(res).toEqual({ isDirectory: false, isPublic: false, url: fakeUrl, internalUrl: 'http://fake.com' }) }) test('when filePath is a public file', async () => { getFileInfoMock.mockReturnValue({ isDirectory: false, isPublic: true, url: fakeUrl }) const res = await files.getProperties('public/file') expect(res).toEqual({ isDirectory: false, isPublic: true, url: fakeUrl }) }) }) describe('_statusFromProviderError', () => { test('missing implementation', async () => { const files = new Files() await global.expectToThrowNotImplemented(files._statusFromProviderError.bind(files, 'error'), '_statusFromProviderError') }) }) describe('copy', () => { test('missing _copyRemoteToRemoteFile (for remote/remote case)', async () => { const fakeFile2 = 'fake/file2.txt' const files = new Files() files.list = jest.fn().mockImplementation(f => { if (f === fakeFile) return [fakeFile]; else return [] }) await global.expectToThrowNotImplemented(files._copyRemoteToRemoteFile.bind(files, fakeFile, fakeFile2), '_copyRemoteToRemoteFile') }) describe('bad input', () => { let files beforeEach(() => { files = new Files() }) test('when srcPath is not a valid string', async () => { await global.expectToThrowBadArg(files.copy.bind(files, 123, 'dest'), ['srcPath', 'string'], { srcPath: 123, destPath: 'dest', options: {} }) }) test('when destPath is not a valid string', async () => { await global.expectToThrowBadArg(files.copy.bind(files, 'src', 123), ['destPath', 'string'], { srcPath: 'src', destPath: 123, options: {} }) }) test('when srcPath is undefined', async () => { await global.expectToThrowBadArg(files.copy.bind(files, undefined, 'dest'), ['srcPath', 'required'], { srcPath: undefined, destPath: 'dest', options: {} }) }) test('when destPath undefined', async () => { await global.expectToThrowBadArg(files.copy.bind(files, 'src', undefined), ['destPath', 'required'], { srcPath: 'src', destPath: undefined, options: {} }) }) test('when options.noOverwrite is not a boolean', async () => { await global.expectToThrowBadArg(files.copy.bind(files, 'src', 'dest', { noOverwrite: 1234 }), ['noOverwrite', 'boolean'], { srcPath: 'src', destPath: 'dest', options: { noOverwrite: 1234 } }) }) test('when options.localSrc is not a boolean', async () => { await global.expectToThrowBadArg(files.copy.bind(files, 'src', 'dest', { localSrc: 1234 }), ['localSrc', 'boolean'], { srcPath: 'src', destPath: 'dest', options: { localSrc: 1234 } }) }) test('when options.localDest is not a boolean', async () => { await global.expectToThrowBadArg(files.copy.bind(files, 'src', 'dest', { localDest: 1234 }), ['localDest', 'boolean'], { srcPath: 'src', destPath: 'dest', options: { localDest: 1234 } }) }) test('when options.progressCallback is not a function', async () => { await global.expectToThrowBadArg(files.copy.bind(files, 'src', 'dest', { progressCallback: 1234 }), ['progressCallback', 'function'], { srcPath: 'src', destPath: 'dest', options: { progressCallback: 1234 } }) }) test('when both options.localSrc and options.localDest are specified', async () => { await global.expectToThrowBadArg(files.copy.bind(files, 'src', 'dest', { localDest: true, localSrc: true }), ['localDest', 'localSrc'], { srcPath: 'src', destPath: 'dest', options: { localDest: true, localSrc: true } }) }) }) describe('copy logic', () => { /** @type {Files} */ let files // local and remote files let remoteFakeDirFiles const fakeFs = global.fakeFs() // with local src or dest const fsStatMock = jest.spyOn(fs, 'stat') const fsReaddirMock = jest.spyOn(fs, 'readdir') const fsPathExistsMock = jest.spyOn(fs, 'pathExists') // local -> remote const fsCreateReadStreamMock = jest.spyOn(fs, 'createReadStream') // remote -> local const fsCreateWriteStreamMock = jest.spyOn(fs, 'createWriteStream') beforeEach(() => { /* **** MOCKS **** */ fakeFs.reset() files = new Files() // all cases - list remoteFakeDirFiles = new Set([]) files.list = jest.fn().mockImplementation(async file => { file = file || '' file = upath.toUnix(file) if (file.endsWith('/')) { // is dir return [...remoteFakeDirFiles].filter(f => f.startsWith(file)) } return (remoteFakeDirFiles.has(file) && [file]) || [] }) // remote <-> remote files._copyRemoteToRemoteFile = jest.fn() // remote -> local files.createReadStream = jest.fn().mockResolvedValue(global.createStream('hello')) // local -> remote files.write = jest.fn() fsStatMock.mockReset() fsReaddirMock.mockReset() fsPathExistsMock.mockReset() fsCreateReadStreamMock.mockReset() fsCreateWriteStreamMock.mockReset() fsStatMock.mockImplementation(fakeFs.stat) fsReaddirMock.mockImplementation(fakeFs.readdir) fsPathExistsMock.mockImplementation(fakeFs.pathExists) fsCreateReadStreamMock.mockImplementation(fakeFs.createReadStream) fsCreateWriteStreamMock.mockImplementation(() => new stream.Writable({ write: (chunk, enc, next) => { next() } })) }) afterAll(() => { fsStatMock.mockRestore() fsReaddirMock.mockRestore() fsPathExistsMock.mockRestore() fsCreateReadStreamMock.mockRestore() fsCreateWriteStreamMock.mockRestore() }) /* **** HELPERS **** */ const arraysToObject = (a, b) => a.reduce((obj, curr, i) => { obj[curr] = b[i]; return obj }, {}) const addFiles = (filesToAdd = [], options = { local: false, prefix: '', isSymlink: false }) => { if (!Array.isArray(filesToAdd)) filesToAdd = [filesToAdd] filesToAdd = ujoinFiles(options.prefix || '', filesToAdd) if (options.local) { filesToAdd.map(f => fakeFs.addFile(f, options.isSymlink ? 'SYMLINK' : 'fake content')) } else { filesToAdd.map(f => remoteFakeDirFiles.add(f)) } return filesToAdd } /* **** CONST **** */ const fakeSrcDir = 'src/subsrc/' // !! important must end with / const fakeDestDir = 'dest/subdest/' // !! important must end with / const fakeSrcFile = ujoin(fakeSrcDir, 'file1') const fakeDestFile = ujoin(fakeDestDir, 'file2') const fakeFiles = ['a', 'b/c/d/e.txt', 'f/g/h', 'f/g/ii.iiiiii'] // !! important must at least contain 4 elements const fakeFiles2 = ['j', 'k/l/m/n.txt', 'o/p/q', 'o/p/rr.rrrrr'] // !! important must at least contain 4 elements /* **** GENERIC TESTS **** */ const testCopyOk = async (src, dest, options, expected) => { // transform expected to absolute path if local const uResolve = p => upath.toUnix(upath.resolve(p)) if (options.localDest) { expected = Object.keys(expected).reduce((obj, k) => { obj[k] = uResolve(expected[k]) return obj }, {}) } if (options.localSrc) { expected = Object.keys(expected).reduce((obj, k) => { obj[uResolve(k)] = expected[k] return obj }, {}) } const expectedEntries = Object.entries(expected) const numberOfFiles = expectedEntries.length // expected is a key value src -> dest const res = await files.copy(src, dest, options) expect(res).toEqual(expected) if (options.progressCallback) { expect(options.progressCallback).toHaveBeenCalledTimes(numberOfFiles) if (numberOfFiles) expect(options.progressCallback).toHaveBeenCalledWith(expectedEntries[0][0], expectedEntries[0][1]) // just check first key value } if (options.localSrc) { expect(files.write).toHaveBeenCalledTimes(numberOfFiles) if (numberOfFiles) expect(files.write).toHaveBeenCalledWith(expectedEntries[0][1], expect.any(stream.Readable)) // make sure called on dest and with stream (for perf reasons) expect(fsCreateReadStreamMock).toHaveBeenCalledTimes(numberOfFiles) if (numberOfFiles) expect(fsCreateReadStreamMock).toHaveBeenCalledWith(expectedEntries[0][0]) // called on src file return } if (options.localDest) { expect(files.createReadStream).toHaveBeenCalledTimes(numberOfFiles) if (numberOfFiles) expect(files.createReadStream).toHaveBeenCalledWith(expectedEntries[0][0]) expect(fsCreateWriteStreamMock).toHaveBeenCalledTimes(numberOfFiles) if (numberOfFiles) expect(fsCreateWriteStreamMock).toHaveBeenCalledWith(expectedEntries[0][1]) return } // remote <-> remote expect(files._copyRemoteToRemoteFile).toHaveBeenCalledTimes(numberOfFiles) if (numberOfFiles) expect(files._copyRemoteToRemoteFile).toHaveBeenCalledWith(expectedEntries[0][0], expectedEntries[0][1]) expect(initWithNewCredsMock).toHaveBeenCalled() } const allGenericCopyTests = (localSrc = undefined, localDest = undefined) => { const localOptions = { localSrc, localDest } /* ** src does not exist ** */ test('when src does not exist', async () => { addFiles([], { local: localSrc }) await global.expectToThrowFileNotExists(files.copy.bind(files, fakeSrcFile, fakeDestFile, localOptions), fakeSrcFile, { srcPath: fakeSrcFile, destPath: fakeDestFile, options: localOptions }) }) /* ** dest does not exist ** */ test('when src is a file and dest does not exist', async () => { addFiles([fakeSrcFile], { local: localSrc }) await testCopyOk(fakeSrcFile, fakeDestFile, localOptions, { [fakeSrcFile]: fakeDestFile }) }) test('when src is a dir and dest does not exist (dir/)', async () => { const srcFiles = addFiles(fakeFiles, { local: localSrc, prefix: fakeSrcDir }) await testCopyOk(fakeSrcDir, fakeDestDir, localOptions, arraysToObject(srcFiles, ujoinFiles([fakeDestDir, upath.basename(fakeSrcDir)], fakeFiles))) }) test('when src is a dir and dest does not exist (file)', async () => { const srcFiles = addFiles(fakeFiles, { local: localSrc, prefix: fakeSrcDir }) await testCopyOk(fakeSrcDir, fakeDestFile, localOptions, arraysToObject(srcFiles, ujoinFiles(fakeDestFile, fakeFiles))) }) /* ** src and dest are files ** */ test('when src and dest are files', async () => { addFiles([fakeSrcFile], { local: localSrc }) addFiles([fakeDestFile], { local: localDest }) await testCopyOk(fakeSrcFile, fakeDestFile, localOptions, { [fakeSrcFile]: fakeDestFile }) }) test('when src and dest are files and noOverwrite set to true and progressCallback is set', async () => { addFiles([fakeSrcFile], { local: localSrc }) addFiles([fakeDestFile], { local: localDest }) await testCopyOk(fakeSrcFile, fakeDestFile, { ...localOptions, noOverwrite: true, progressCallback: jest.fn() }, {}) }) test('when src and dest are files and progressCallback is set', async () => { addFiles([fakeSrcFile], { local: localSrc }) addFiles([fakeDestFile], { local: localDest }) await testCopyOk(fakeSrcFile, fakeDestFile, { ...localOptions, progressCallback: jest.fn() }, { [fakeSrcFile]: fakeDestFile }) }) /* ** src is a file and dest is a dir ** */ test('when src is a file and dest is an existing dir containing a different file', async () => { addFiles(['a'], { local: localSrc }) addFiles(['b'], { local: localDest, prefix: fakeDestDir }) await testCopyOk('a', fakeDestDir, localOptions, { a: ujoin(fakeDestDir, 'a') }) }) test('when src is a file and dest is an existing dir containing a different file and noOverwrite and progressCallback', async () => { addFiles(['a'], { local: localSrc }) addFiles(['b'], { local: localDest, prefix: fakeDestDir }) await testCopyOk('a', fakeDestDir, { ...localOptions, noOverwrite: true, progressCallback: jest.fn() }, { a: ujoin(fakeDestDir, 'a') }) }) test('when src is a file and dest is an existing dir with same name as file', async () => { addFiles(['a'], { local: localSrc }) addFiles(['b'], { local: localDest, prefix: 'a' }) await testCopyOk('a', 'a/', localOptions, { a: 'a/a' }) }) test('when src is a file and dest is an existing dir which contain the same file name', async () => { addFiles(['a'], { local: localSrc }) addFiles(['a'], { local: localDest, prefix: fakeDestDir }) await testCopyOk('a', fakeDestDir, localOptions, { a: ujoin(fakeDestDir, 'a') }) }) test('when src is a file and dest is an existing dir which contain the same file name and noOverwrite = true', async () => { addFiles(['a'], { local: localSrc }) addFiles(['a'], { local: localDest, prefix: fakeDestDir }) await testCopyOk('a', fakeDestDir, { ...localOptions, noOverwrite: true }, {}) }) test('when src is a file and dest has a subdir with the same file name and noOverwrite = true', async () => { addFiles(['a'], { local: localSrc }) addFiles(['b/a'], { local: localDest, prefix: fakeDestDir }) await testCopyOk('a', fakeDestDir, { ...localOptions, noOverwrite: true }, { a: ujoin(fakeDestDir, 'a') }) }) /* ** src and dest are dirs ** */ test('when src and dest are dirs containing different files', async () => { const srcFiles = addFiles(fakeFiles, { local: localSrc, prefix: fakeSrcDir }) addFiles(fakeFiles2, { local: localDest, prefix: fakeDestDir }) await testCopyOk(fakeSrcDir, fakeDestDir, localOptions, arraysToObject(srcFiles, ujoinFiles([fakeDestDir, upath.basename(fakeSrcDir)], fakeFiles))) }) test('when src and dest are dirs containing same files and dest ends with /', async () => { const srcFiles = addFiles(fakeFiles, { local: localSrc, prefix: fakeSrcDir }) addFiles(fakeFiles, { local: localDest, prefix: fakeDestDir }) await testCopyOk(fakeSrcDir, fakeDestDir, localOptions, arraysToObject(srcFiles, ujoinFiles([fakeDestDir, upath.basename(fakeSrcDir)], fakeFiles))) }) test('when src and dest are dirs containing same files and dest ends with / and noOverwrite and progressCallback are set', async () => { // is still ok to copy as we copy to dest/src/files const srcFiles = addFiles(fakeFiles, { local: localSrc, prefix: fakeSrcDir }) addFiles(fakeFiles, { local: localDest, prefix: fakeDestDir }) await testCopyOk(fakeSrcDir, fakeDestDir, { ...localOptions, progressCallback: jest.fn(), noOverwrite: true }, arraysToObject(srcFiles, ujoinFiles([fakeDestDir, upath.basename(fakeSrcDir)], fakeFiles))) }) test('when src and dest are dirs and dest contains subfolder with src basname with same files and dest ends with /', async () => { const srcFiles = addFiles(fakeFiles, { local: localSrc, prefix: fakeSrcDir }) addFiles(ujoinFiles(upath.basename(fakeSrcDir), fakeFiles), { local: localDest, prefix: fakeDestDir }) await testCopyOk(fakeSrcDir, fakeDestDir, localOptions, arraysToObject(srcFiles, ujoinFiles([fakeDestDir, upath.basename(fakeSrcDir)], fakeFiles))) }) test('when src and dest are dirs and dest contains subfolder with src basename with same files and dest ends with / and noOverwrite and progressCallback are set', async () => { addFiles(fakeFiles, { local: localSrc, prefix: fakeSrcDir }) addFiles(ujoinFiles(upath.basename(fakeSrcDir), fakeFiles), { local: localDest, prefix: fakeDestDir }) await testCopyOk(fakeSrcDir, fakeDestDir, { ...localOptions, progressCallback: jest.fn(), noOverwrite: true }, {}) }) test('when src and dest are dirs containing same files but dest does not end with `/`', async () => { const srcFiles = addFiles(fakeFiles, { local: localSrc, prefix: fakeSrcDir }) addFiles(fakeFiles, { local: localDest, prefix: fakeDestDir }) // if dest is an existing **LOCAL** dir behaviour is like ending with a / const expected = localDest ? arraysToObject(srcFiles, ujoinFiles([fakeDestDir, upath.basename(fakeSrcDir)], fakeFiles)) : arraysToObject(srcFiles, ujoinFiles(fakeDestDir, fakeFiles)) await testCopyOk(fakeSrcDir, fakeDestDir.slice(0, -1), localOptions, expected) }) test('when src and dest are dirs containing same files but dest does not end with `/` and **noOverwrite** and progressCallback are set', async () => { // this time does not copy as we copy to dest/files const srcFiles = addFiles(fakeFiles, { local: localSrc, prefix: fakeSrcDir }) addFiles(fakeFiles, { local: localDest, prefix: fakeDestDir }) // if dest is an existing **LOCAL** dir behaviour is like ending with a / so no overwrite const expected = localDest ? arraysToObject(srcFiles, ujoinFiles([fakeDestDir, upath.basename(fakeSrcDir)], fakeFiles)) : {} await testCopyOk(fakeSrcDir, fakeDestDir.slice(0, -1), { ...localOptions, progressCallback: jest.fn(), noOverwrite: true }, expected) }) test('when src and dest are dirs and dest does not end with `/` and contains 2 files that are contained by src and **noOverwrite** and progressCallback are set', async () => { // here we test that just some files are copied const srcFiles = addFiles(fakeFiles, { local: localSrc, prefix: fakeSrcDir }) addFiles(fakeFiles.slice(0, 2), { local: localDest, prefix: fakeDestDir }) // if dest is an existing **LOCAL** dir behaviour is like ending with a / so no overwrite const expected = localDest ? arraysToObject(srcFiles, ujoinFiles([fakeDestDir, upath.basename(fakeSrcDir)], fakeFiles)) : arraysToObject(srcFiles.slice(2), ujoinFiles(fakeDestDir, fakeFiles.slice(2))) await testCopyOk(fakeSrcDir, fakeDestDir.slice(0, -1), { ...localOptions, progressCallback: jest.fn(), noOverwrite: true }, expected) }) /* ** src is a dir and dest is a file ** */ test('when src is a dir and dest an existing file and noOverwrite and progressCallback are set', async () => { // noOverwrite does not matter here in remote setup allowed anyways // if dest is local though should throw an error as we cannot copy a dir to a file const srcFiles = addFiles(fakeFiles, { local: localSrc, prefix: fakeSrcDir }) addFiles([fakeDestFile], { local: localDest }) const options = { ...localOptions, progressCallback: jest.fn(), noOverwrite: true } if (localDest) { await global.expectToThrowBadFileType(files.copy.bind(files, fakeSrcDir, fakeDestFile, options), fakeDestFile, { srcPath: fakeSrcDir, destPath: fakeDestFile, options }) return } await testCopyOk(fakeSrcDir, fakeDestFile, options, arraysToObject(srcFiles, ujoinFiles(fakeDestFile, fakeFiles))) }) } // /* **** ACTUAL TESTS **** */ describe('remote <-> remote', () => { allGenericCopyTests() }) describe('local -> remote', () => { allGenericCopyTests(true, undefined) test('local src is symlink', async () => { addFiles([fakeSrcFile], { local: true, isSymlink: true }) await global.expectToThrowBadFileType(files.copy.bind(files, fakeSrcFile, fakeDestFile, { localSrc: true }), fakeSrcFile, { srcPath: fakeSrcFile, destPath: fakeDestFile, options: { localSrc: true } }) }) test('local src is a directory containing a symlink', async () => { // is fine if symlink is within folder should just get ignored addFiles(['symlinkFile'], { local: true, isSymlink: true, prefix: fakeSrcDir }) const srcFiles = addFiles(['b/a'], { local: true, prefix: fakeSrcDir }) await testCopyOk(fakeSrcDir, fakeDestDir, { localSrc: true }, arraysToObject(srcFiles, ujoinFiles([fakeDestDir, upath.basename(fakeSrcDir)], ['b/a']))) }) test('fs.stat throws other error than ENOENT', async () => { const fakeError = new Error('myerror') fakeError.code = 'FAKE' fsStatMock.mockRejectedValue(fakeError) addFiles([fakeSrcFile], { local: false }) addFiles([fakeDestFile], { local: true }) try { await files.copy(fakeSrcFile, fakeDestFile, { localSrc: true }) } catch (e) { // eslint-disable-next-line jest/no-conditional-expect expect(e).toEqual(fakeError) } }) }) describe('remote -> local', () => { allGenericCopyTests(undefined, true) test('local dest is symlink', async () => { addFiles([fakeSrcFile], { local: false }) addFiles([fakeDestFile], { local: true, isSymlink: true }) await global.expectToThrowBadFileType(files.copy.bind(files, fakeSrcFile, fakeDestFile, { localDest: true }), fakeDestFile, { srcPath: fakeSrcFile, destPath: fakeDestFile, options: { localDest: true } }) }) test('fs.stat throws other error than ENOENT', async () => { const fakeError = new Error('myerror') fakeError.code = 'FAKE' fsStatMock.mockRejectedValue(fakeError) addFiles([fakeSrcFile], { local: false }) addFiles([fakeDestFile], { local: true }) try { await files.copy(fakeSrcFile, fakeDestFile, { localDest: true }) } catch (e) { // eslint-disable-next-line jest/no-conditional-expect expect(e).toEqual(fakeError) } }) }) }) }) describe('generatePresignURL', () => { test('missing _getPresignUrl implementation', async () => { const files = new Files() await global.expectToThrowNotImplemented(files._getPresignUrl.bind(files, fakeFile), '_getPresignUrl') }) describe('_getPresignUrl mock implementation', () => { const getPresignUrlMock = jest.spyOn(Files.prototype, '_getPresignUrl') const options = { expiryInSeconds: 30 } let files const fakeUrl = 'http://fake.com' beforeEach(() => { files = new Files() getPresignUrlMock.mockReset() getPresignUrlMock.mockReturnValue(fakeUrl) }) afterAll(() => { getPresignUrlMock.mockRestore() }) test('when path is not a valid string', async () => { await global.expectToThrowBadArg(files.write.bind(files, 123), ['filePath', 'string'], { filePath: 123 }) }) test('when path undefined', async () => { await global.expectToThrowBadArg(files.write.bind(files, undefined), ['filePath', 'required'], { filePath: undefined }) }) test('when filePath is non normalized', async () => { await files.generatePresignURL('hello/../file', options) expect(initWithNewCredsMock).toHaveBeenCalled() expect(getPresignUrlMock).toHaveBeenCalledWith('file', { expiryInSeconds: 30 }) }) test('when filePath is a private file', async () => { const res = await files.generatePresignURL('a/private/file', options) expect(initWithNewCredsMock).toHaveBeenCalled() expect(res).toEqual('http://fake.com') }) test('when filePath is a public file', async () => { const res = await files.generatePresignURL('public/file', options) expect(initWithNewCredsMock).toHaveBeenCalled() expect(res).toEqual('http://fake.com') }) test('when filePath is a private path starting with `public` (publicisnotpublicfile.txt)', async () => { const res = await files.generatePresignURL('publicisnotpublicfile.txt', options) expect(initWithNewCredsMock).toHaveBeenCalled() expect(res).toEqual('http://fake.com') }) test('when filePath is a public dir', async () => { const res = await files.generatePresignURL('public/dir/', options) expect(initWithNewCredsMock).toHaveBeenCalled() expect(res).toEqual('http://fake.com') }) }) }) describe('revokeAllPresignURLs', () => { test('missing _revokeAllPresignURLs implementation', async () => { const files = new Files() await global.expectToThrowNotImplemented(files._revokeAllPresignURLs.bind(files, fakeFile), '_revokeAllPresignURLs') }) describe('_revokeAllPresignURLs mock implementation', () => { const revokePresignUrlMock = jest.spyOn(Files.prototype, '_revokeAllPresignURLs') let files const fakeRes = {} beforeEach(() => { files = new Files() revokePresignUrlMock.mockReset() revokePresignUrlMock.mockReturnValue(fakeRes) }) afterAll(() => { revokePresignUrlMock.mockRestore() }) test('call revoke', async () => { const res = await files.revokeAllPresignURLs() expect(initWithNewCredsMock).toHaveBeenCalled() expect(revokePresignUrlMock).toHaveBeenCalled() expect(res).toEqual(fakeRes) }) }) }) describe('exists', () => { test('missing _fileExists implementation', async () => { const files = new Files() await global.expectToThrowNotImplemented(files._fileExists.bind('path'), '_fileExists') }) })