@adobe/aio-lib-files
Version:
An abstraction on top of blob cloud storage exposing a file like API
1,046 lines (893 loc) • 41.2 kB
JavaScript
/* eslint-disable jest/expect-expect */
/*
Copyright 2019 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
const fetch = require('node-fetch')
jest.mock('node-fetch', () => jest.fn())
const fakeResponse = jest.fn()
jest.mock('uuid', () => ({ v4: () => 'fake-uuid' }))
const libEnv = require('@adobe/aio-lib-env')
jest.mock('@adobe/aio-lib-env')
const { AzureBlobFiles } = require('../../lib/impl/AzureBlobFiles')
const stream = require('stream')
const cloneDeep = require('lodash.clonedeep')
const azure = require('@azure/storage-blob')
jest.mock('@azure/storage-blob')
const fakeSASCredentials = {
sasURLPrivate: 'https://fake.com/private?secret=abcd',
sasURLPublic: 'https://fake.com/public?secret=abcd'
}
const fakeUserCredentials = {
containerName: 'fake',
storageAccessKey: 'fakeKey',
storageAccount: 'fakeAccount'
}
const fakeAccessPolicy = '<?xml version="1.0" encoding="utf-8"?><SignedIdentifiers><SignedIdentifier><Id>12345678-868e-1234-1234-123456789abc</Id><permissions></permissions></SignedIdentifier></SignedIdentifiers>'
const fakeEmptyAccessPolicy = '<?xml version="1.0" encoding="utf-8"?><SignedIdentifiers></SignedIdentifiers>'
const fakeEmptyAccessPolicy1 = '<?xml version="1.0" encoding="utf-8"?>'
const fakeMultipleAccessPolicy = '<?xml version="1.0" encoding="utf-8"?><SignedIdentifiers><SignedIdentifier><Id>12345678-868e-1234-1234-123456789abc</Id><permissions></permissions></SignedIdentifier><SignedIdentifier><Id>FakeCustomPolicy</Id><permissions></permissions></SignedIdentifier></SignedIdentifiers>'
const fakeCustomAccessPolicy = '<?xml version="1.0" encoding="utf-8"?><SignedIdentifiers><SignedIdentifier><Id>FakeCustomPolicy</Id><permissions></permissions></SignedIdentifier></SignedIdentifiers>'
const DEFAULT_CDN_STORAGE_HOST = 'https://firefly.azureedge.net'
beforeEach(async () => {
jest.clearAllMocks()
})
const testWithProviderError = async (boundFunc, providerMock, errorDetails, file404) => {
errorDetails = cloneDeep(errorDetails)
if (file404) {
providerMock.mockRejectedValue({ response: { status: 404 } })
await global.expectToThrowFileNotExists(boundFunc, file404, errorDetails)
}
// 403
providerMock.mockRejectedValue({ response: { status: 403 } })
await global.expectToThrowBadCredentials(boundFunc, errorDetails, 'storage service')
// unknown status 444
let error = { response: { status: 444 }, somefield: true }
providerMock.mockRejectedValue(error)
await global.expectToThrowInternalWithStatus(boundFunc, 444, { ...errorDetails, _internal: error })
// no status Error
error = new Error('some fake error')
providerMock.mockRejectedValue(error)
await global.expectToThrowInternal(boundFunc, { ...errorDetails, _internal: error })
}
describe('init', () => {
const mockContainerCreateIfNotExists = jest.fn()
const mockContainerCreateIfNotExistsPublic = jest.fn()
const mockSetAccessPolicy = jest.fn()
const mockSetAccessPolicyPublic = jest.fn()
const mockContainerClientInstance = {
createIfNotExists: mockContainerCreateIfNotExists,
setAccessPolicy: mockSetAccessPolicy,
url: fakeSASCredentials.sasURLPrivate
}
const mockContainerClientInstancePublic = {
createIfNotExists: mockContainerCreateIfNotExistsPublic,
setAccessPolicy: mockSetAccessPolicyPublic,
url: fakeSASCredentials.sasURLPublic
}
beforeEach(async () => {
mockContainerCreateIfNotExists.mockReset()
mockContainerCreateIfNotExistsPublic.mockReset()
mockSetAccessPolicy.mockReset()
mockSetAccessPolicyPublic.mockReset()
// mock needed for byo master keys init
azure.BlobServiceClient.mockImplementation(() => {
return {
getContainerClient: jest.fn().mockImplementation((containerName) => containerName.includes('-public') ? mockContainerClientInstancePublic : mockContainerClientInstance)
}
})
// mock for sas creds init
azure.ContainerClient.mockImplementation(() => mockContainerClientInstance)
})
const checkInitDebugLogNoSecrets = (str) => expect(global.mockLogDebug).not.toHaveBeenCalledWith(expect.stringContaining(str))
describe('with bad args', () => {
// eslint-disable-next-line jest/expect-expect
test('when called with no arguments', async () => {
await global.expectToThrowBadArg(AzureBlobFiles.init, ['credentials', 'at least'], {})
})
// eslint-disable-next-line jest/expect-expect
test('when called with incomplete SAS credentials', async () => {
const badInput = { ...fakeSASCredentials }
delete badInput.sasURLPrivate
const details = { sasURLPublic: fakeSASCredentials.sasURLPublic.split('?')[0] + '?<hidden>' } // hide token part of SAS in error details
await global.expectToThrowBadArg(AzureBlobFiles.init.bind(null, badInput), ['credentials', 'required', 'sasURLPrivate'], details)
})
// eslint-disable-next-line jest/expect-expect
test('when called with incomplete user credentials', async () => {
const badInput = { ...fakeUserCredentials }
delete badInput.containerName
const details = { storageAccount: fakeUserCredentials.storageAccount, storageAccessKey: '<hidden>' }
await global.expectToThrowBadArg(AzureBlobFiles.init.bind(null, badInput), ['credentials', 'required', 'containerName'], details)
})
// eslint-disable-next-line jest/expect-expect
test('when called with both sas and user credentials', async () => {
const fakeErrorDetails = cloneDeep({ ...fakeUserCredentials, ...fakeSASCredentials })
fakeErrorDetails.sasURLPublic = fakeErrorDetails.sasURLPublic.split('?')[0] + '?<hidden>'
fakeErrorDetails.sasURLPrivate = fakeErrorDetails.sasURLPrivate.split('?')[0] + '?<hidden>'
fakeErrorDetails.storageAccessKey = '<hidden>'
await global.expectToThrowBadArg(AzureBlobFiles.init.bind(null, { ...fakeUserCredentials, ...fakeSASCredentials }), ['credentials', 'conflict'], fakeErrorDetails)
})
})
describe('with azure storage account credentials', () => {
beforeEach(async () => {
fetch.mockResolvedValue({
text: fakeResponse
})
fakeResponse.mockReset()
fakeResponse.mockResolvedValue(fakeAccessPolicy)
mockContainerCreateIfNotExists.mockResolvedValue('all good')
mockContainerCreateIfNotExistsPublic.mockResolvedValue('all good')
})
test('when createIfNotExists containers does not fail', async () => {
const files = await AzureBlobFiles.init(fakeUserCredentials)
expect(files).toBeInstanceOf(AzureBlobFiles)
expect(mockContainerCreateIfNotExists).toHaveBeenCalledTimes(1)
expect(mockContainerCreateIfNotExistsPublic).toHaveBeenCalledTimes(1)
expect(mockContainerCreateIfNotExists).toHaveBeenCalledWith()
expect(mockContainerCreateIfNotExistsPublic).toHaveBeenCalledWith({ access: 'blob' })
checkInitDebugLogNoSecrets(fakeUserCredentials.storageAccessKey)
})
test('when createIfNotExists container API call fails', async () =>
testWithProviderError(
AzureBlobFiles.init.bind(null, fakeUserCredentials),
mockContainerCreateIfNotExists,
{
containerName: fakeUserCredentials.containerName,
storageAccount: fakeUserCredentials.storageAccount
}
)
)
test('when there is no access policy defined yet', async () => {
fakeResponse.mockResolvedValueOnce(fakeEmptyAccessPolicy)
const files = await AzureBlobFiles.init(fakeUserCredentials)
expect(files).toBeInstanceOf(AzureBlobFiles)
expect(mockSetAccessPolicy).toHaveBeenCalledWith(undefined, [{ id: 'fake-uuid', accessPolicy: { permission: '' } }])
expect(mockSetAccessPolicyPublic).toHaveBeenCalledWith('blob', [{ id: 'fake-uuid', accessPolicy: { permission: '' } }]) // for public container
})
test('when there is an empty access policy already defined', async () => {
fakeResponse.mockResolvedValueOnce(fakeEmptyAccessPolicy)
const files = await AzureBlobFiles.init(fakeUserCredentials)
expect(files).toBeInstanceOf(AzureBlobFiles)
expect(mockSetAccessPolicy).toHaveBeenCalledWith(undefined, [{ id: 'fake-uuid', accessPolicy: { permission: '' } }])
})
test('when there is an empty access policy already defined (2)', async () => {
fakeResponse.mockResolvedValueOnce(fakeEmptyAccessPolicy1)
const files = await AzureBlobFiles.init(fakeUserCredentials)
expect(files).toBeInstanceOf(AzureBlobFiles)
expect(mockSetAccessPolicy).toHaveBeenCalledWith(undefined, [{ id: 'fake-uuid', accessPolicy: { permission: '' } }])
})
test('when there is a valid access policy already defined', async () => {
fakeResponse.mockResolvedValueOnce(fakeAccessPolicy)
const files = await AzureBlobFiles.init(fakeUserCredentials)
expect(files).toBeInstanceOf(AzureBlobFiles)
expect(mockSetAccessPolicy).toHaveBeenCalledTimes(0)
})
test('when multiple custom access policies are defined', async () => {
fakeResponse.mockResolvedValueOnce(fakeMultipleAccessPolicy)
const errorMsg = '[FilesLib:ERROR_INIT_FAILURE] Container has more than one access policies defined. Please remove all custom access policies or use another container.'
await expect(AzureBlobFiles.init(fakeUserCredentials)).rejects.toThrow(errorMsg)
})
test('when custom access policy is defined', async () => {
fakeResponse.mockResolvedValueOnce(fakeCustomAccessPolicy)
const errorMsg = '[FilesLib:ERROR_INIT_FAILURE] Custom access policies are defined. Please remove all access policies or use another container.'
await expect(AzureBlobFiles.init(fakeUserCredentials)).rejects.toThrow(errorMsg)
})
})
test('with azure SAS credentials', async () => {
// test
const files = await AzureBlobFiles.init(fakeSASCredentials)
expect(mockSetAccessPolicy).not.toHaveBeenCalled()
expect(mockContainerCreateIfNotExists).not.toHaveBeenCalled()
expect(files).toBeInstanceOf(AzureBlobFiles)
checkInitDebugLogNoSecrets(fakeSASCredentials.sasURLPublic)
checkInitDebugLogNoSecrets(fakeSASCredentials.sasURLPrivate)
})
})
describe('getFileInfo', () => {
let files
const fileName = 'path.html'
const fakeFileInfo = {
lastModified: Date.now(),
etag: 'asdasd',
contentLength: 100,
contentType: 'test/junk'
}
const mockBlobGetProperties = jest.fn()
beforeEach(async () => {
mockBlobGetProperties.mockReset()
mockBlobGetProperties.mockResolvedValue(fakeFileInfo)
azure.ContainerClient.mockImplementation(() => ({
getBlockBlobClient: () => ({
getProperties: mockBlobGetProperties
})
}))
files = await AzureBlobFiles.init(fakeSASCredentials)
files._getUrl = () => {
return 'duke of url'
}
})
test('when the file exists', async () => {
const fileInfo = await files.getFileInfo(fileName)
expect(fileInfo).toEqual({
isDirectory: false,
isPublic: false,
url: 'duke of url',
name: fileName,
internalUrl: 'duke of url',
...fakeFileInfo
})
})
test('when the file does not exist', async () => {
mockBlobGetProperties.mockRejectedValue({ response: { status: 404 } })
await expect(files.getFileInfo(fileName)).rejects.toThrow('[FilesLib:ERROR_FILE_NOT_EXISTS] file `path.html` does not exist')
})
test('when the azure.blob.getProperties fails', async () =>
testWithProviderError(
() => files.getFileInfo(fileName),
mockBlobGetProperties,
{
filePath: fileName
}
)
)
})
describe('_listFolder', () => {
const privateDir = 'some/private/dir/'
const publicDir = 'public/some/dir/'
const fakeAzureListResponse = (files) => {
const asyncIterable = files.map(name => ({
name,
properties: {
lastModified: Date.now(),
createdOn: Date.now(),
etag: 'asdasd',
contentLength: 100,
contentType: 'test/junk',
url: 'some/url?asdklk'
}
}))
asyncIterable[Symbol.asyncIterator] = async function * () {
for (let i = 0; i < asyncIterable.length; i++) {
yield { ...asyncIterable[i] }
}
}
return asyncIterable
}
const mockContainerPublicList = jest.fn()
const mockContainerPrivateList = jest.fn()
const fakeFiles = ['file1', 'subdir/file2', 'another/subdir/file3']
const fakeFiles2 = ['file4', 'subdir2/file5', 'another2/subdir3/file6']
let files
beforeEach(async () => {
mockContainerPublicList.mockReset()
mockContainerPrivateList.mockReset()
azure.ContainerClient.mockImplementation(url => {
const containerMockList = url.includes('public') ? mockContainerPublicList : mockContainerPrivateList
return {
listBlobsFlat: () => {
return {
byPage: () => {
return {
next: () => {
return {
value: containerMockList()
}
}
}
}
}
},
getBlockBlobClient: () => ({
url: 'some/url?asdklk'
})
}
})
files = await AzureBlobFiles.init(fakeSASCredentials)
})
// eslint-disable-next-line jsdoc/require-jsdoc
async function testListFolder (filePath, listsPublic, listsPrivate, isRoot) {
const publicFiles = fakeFiles.map(f => publicDir + f)
const privateFiles = fakeFiles2.map(f => privateDir + f)
mockContainerPublicList.mockReturnValue({
segment: {
blobItems: fakeAzureListResponse(publicFiles)
}
})
mockContainerPrivateList.mockReturnValue({
segment: {
blobItems: fakeAzureListResponse(privateFiles)
}
})
const fileList = await files._listFolder(filePath)
expect(fileList).toStrictEqual(expect.arrayContaining([expect.objectContaining({ name: expect.any(String) })]))
// expect length of returned list to equal sum
expect(fileList.length).toBe((listsPublic ? publicFiles.length : 0) + (listsPrivate ? privateFiles.length : 0))
expect(mockContainerPublicList).toHaveBeenCalledTimes(listsPublic ? 1 : 0)
expect(mockContainerPrivateList).toHaveBeenCalledTimes(listsPrivate ? 1 : 0)
}
test('when it is the root (empty string)', async () => {
await testListFolder('', true, true, true)
})
test('when it is private', async () => {
await testListFolder(privateDir, false, true)
})
test('when it is public', async () => {
await testListFolder(publicDir, true, false)
})
// can't do error handling on returned async iteerable for now
// ('when azure.ContainerClient.list rejects with an error', async () =>
// testWithProviderError(files._listFolder.bind(files, publicDir), mockContainerPublicList, { filePath: publicDir })
// )
})
describe('_deleteFile', () => {
const mockAzureDelete = jest.fn()
let files
beforeEach(async () => {
mockAzureDelete.mockReset()
azure.ContainerClient.mockImplementation(() => ({
getBlockBlobClient: () => ({
delete: mockAzureDelete
})
}))
files = await AzureBlobFiles.init(fakeSASCredentials)
})
test('a file that exists', async () => {
mockAzureDelete.mockResolvedValue(true)
await files._deleteFile('afile')
expect(mockAzureDelete).toHaveBeenCalledTimes(1)
})
// also checks 404 (note the double 'afile')
// eslint-disable-next-line jest/expect-expect
test('when azure.BlockBlobClient.delete rejects with an error, including file not exists (404)', async () =>
testWithProviderError(files._deleteFile.bind(files, 'afile'), mockAzureDelete, { filePath: 'afile' }, 'afile'))
})
describe('_createReadStream', () => {
const fakeFile = 'a/dir/file1'
const mockAzureDownload = jest.fn()
let files
beforeEach(async () => {
mockAzureDownload.mockReset()
azure.ContainerClient.mockImplementation(() => ({
getBlockBlobClient: () => ({
download: mockAzureDownload
})
}))
files = await AzureBlobFiles.init(fakeSASCredentials)
})
let fakeRdStream
const fakeOptions = { position: 1, length: 10 }
beforeEach(() => {
fakeRdStream = new stream.Readable()
fakeRdStream.push(null)
mockAzureDownload.mockResolvedValue({ readableStreamBody: fakeRdStream })
})
test('w default options ({ position: 0, length: undefined })', async () => {
const res = await files._createReadStream(fakeFile, { position: 0 })
expect(res).toBe(fakeRdStream)
expect(mockAzureDownload).toHaveBeenCalledTimes(1)
expect(mockAzureDownload).toHaveBeenCalledWith(0, undefined)
})
test('with options', async () => {
const res = await files._createReadStream(fakeFile, fakeOptions)
expect(res).toBe(fakeRdStream)
expect(mockAzureDownload).toHaveBeenCalledTimes(1)
expect(mockAzureDownload).toHaveBeenCalledWith(fakeOptions.position, fakeOptions.length)
})
test('when azure.BlockBlobClient.download rejects with an error, including file not exists (404)', async () =>
testWithProviderError(files._createReadStream.bind(files, 'afile', {}), mockAzureDownload, { filePath: 'afile', options: {} }, 'afile')
)
test('when azure.BlockBlobClient.download rejects with a 416 error because of out of range position', async () => {
mockAzureDownload.mockRejectedValue({ response: { status: 416 } })
await global.expectToThrowBadPosition(files._createReadStream.bind(files, 'afile', { position: 1234 }), 1234, 'afile', { filePath: 'afile', options: { position: 1234 } })
})
})
describe('writeBuffer', () => {
const fakeFile = 'a/dir/file1'
const mockAzureUpload = jest.fn()
const fakeBuffer = Buffer.from('some fake content @#$%^&*()@!12-=][;"\n\trewq')
let files
beforeEach(async () => {
mockAzureUpload.mockReset()
mockAzureUpload.mockResolvedValue(true)
azure.ContainerClient.mockImplementation(() => ({
getBlockBlobClient: () => ({
upload: mockAzureUpload
})
}))
files = await AzureBlobFiles.init(fakeSASCredentials)
})
const testWriteBuffer = async (fileExt, expectMimeType) => {
const res = await files._writeBuffer(fakeFile + fileExt, fakeBuffer)
expect(res).toBe(fakeBuffer.length)
expect(mockAzureUpload).toHaveBeenCalledTimes(1)
expect(mockAzureUpload).toHaveBeenCalledWith(fakeBuffer, fakeBuffer.length, { blobHTTPHeaders: { blobContentType: expectMimeType } })
}
test('when file has valid mime type file extension', async () => {
await testWriteBuffer('.json', 'application/json')
})
test('when file has invalid mime type file extension', async () => {
await testWriteBuffer('.iiiiiiiii', 'application/octet-stream')
})
test('when file has no file extension', async () => {
await testWriteBuffer('', 'application/octet-stream')
})
// eslint-disable-next-line jest/expect-expect
test('when azure.BlockBlobClient throws an error', async () => {
await testWithProviderError(
files._writeBuffer.bind(files, 'afile', fakeBuffer),
mockAzureUpload, { filePath: 'afile', contentType: 'Buffer' }
)
})
})
describe('_writeStream', () => {
const fakeFile = 'a/dir/file1'
const mockAzureStreamUpload = jest.fn()
const fakeContent = 'some fake content @#$%^&*()@!12-=][;"\n\trewq'
let fakeRdStream
let files
beforeEach(async () => {
mockAzureStreamUpload.mockReset()
mockAzureStreamUpload.mockImplementation((stream) => new Promise((resolve) => stream.on('end', resolve))) // tight coupling..
azure.ContainerClient.mockImplementation(() => ({
getBlockBlobClient: () => ({
uploadStream: mockAzureStreamUpload
})
}))
files = await AzureBlobFiles.init(fakeSASCredentials)
fakeRdStream = new stream.Readable()
fakeRdStream.push(fakeContent)
fakeRdStream.push(null)
})
const testWriteStream = async (fileExt, expectMimeType) => {
const res = await files._writeStream(fakeFile + fileExt, fakeRdStream)
expect(res).toBe(fakeContent.length)
expect(mockAzureStreamUpload).toHaveBeenCalledTimes(1)
expect(mockAzureStreamUpload.mock.calls[0]).toEqual(expect.arrayContaining([fakeRdStream, { blobHTTPHeaders: { blobContentType: expectMimeType } }]))
}
test('when file has valid mime type file extension', async () => {
await testWriteStream('.json', 'application/json')
})
test('when file has invalid mime type file extension', async () => {
await testWriteStream('.iiiiiiiii', 'application/octet-stream')
})
test('when file has no file extension', async () => {
await testWriteStream('', 'application/octet-stream')
})
test('when azure.uploadStreamToBlockBlob throws an error', async () => {
await testWithProviderError(
files._writeStream.bind(files, 'afile', fakeRdStream),
mockAzureStreamUpload, { filePath: 'afile', contentType: 'Readable' })
})
})
describe('_createWriteStream', () => {
const mockAzureStreamUpload = jest.fn()
const fakeFile = 'a/dir/file1'
const fakeChunks = ['some ', 'fake con', 'tent @#$%^&*()@!', '12-=][;"\n\trewq']
const fakeChunksSize = fakeChunks.reduce((prev, curr) => prev + curr.length, 0)
/** @type {AzureBlobFiles} */
let files
beforeEach(async () => {
mockAzureStreamUpload.mockReset()
mockAzureStreamUpload.mockImplementation((stream) => new Promise((resolve) => stream.on('end', resolve))) // tight coupling..
azure.ContainerClient.mockImplementation(() => ({
getBlockBlobClient: () => ({
uploadStream: mockAzureStreamUpload
})
}))
files = await AzureBlobFiles.init(fakeSASCredentials)
})
test('with file with html extension, write multiple chunks and end the stream', async () => {
expect.assertions(5)
const wrStream = await files._createWriteStream(fakeFile + '.html')
expect(wrStream).toBeInstanceOf(stream.Writable)
fakeChunks.forEach(chunk => wrStream.write(chunk))
wrStream.end()
return new Promise(resolve => {
wrStream.on('finish', async bytesWritten => {
expect(bytesWritten).toEqual(fakeChunksSize)
// also check field
expect(wrStream.bytesWritten).toEqual(fakeChunksSize)
expect(mockAzureStreamUpload).toHaveBeenCalledTimes(1)
expect(mockAzureStreamUpload.mock.calls[0]).toEqual(expect.arrayContaining([{ blobHTTPHeaders: { blobContentType: 'text/html' } }]))
resolve()
})
})
})
// eslint-disable-next-line jest/expect-expect
test('when stream is written and azure.uploadStreamToBlockBlob rejects an error', async () => {
mockAzureStreamUpload.mockRejectedValue({ response: { status: 444 } })
const wrStream = await files._createWriteStream(fakeFile)
wrStream.write('hi')
return new Promise(resolve => {
wrStream.on('error', async e => {
await global.expectToThrowInternalWithStatus(() => { throw e }, 444, { filePath: fakeFile, _internal: { response: { status: 444 } } })
resolve()
})
})
})
// eslint-disable-next-line jest/expect-expect
test('when stream is written and azure.uploadStreamToBlockBlob rejects with 403', async () => {
mockAzureStreamUpload.mockRejectedValue({ response: { status: 403 } })
const wrStream = await files._createWriteStream(fakeFile)
wrStream.write('hi')
return new Promise(resolve => {
wrStream.on('error', async e => {
await global.expectToThrowBadCredentials(() => { throw e }, { filePath: fakeFile }, 'storage service')
resolve()
})
})
})
})
describe('_copyRemoteToRemoteFile', () => {
const mockStartCopyFromURL = jest.fn()
const src = 'a/dir/file1'
const dest = 'public/another/dir/file2'
const fakeSrcURL = 'https://fake.blob.core.windows.net/a/dir/file1?secret=xxx'// important to keep secret
/** @type {AzureBlobFiles} */
let files
beforeEach(async () => {
mockStartCopyFromURL.mockReset()
mockStartCopyFromURL.mockResolvedValue(true)
azure.ContainerClient.mockImplementation(() => ({
getBlockBlobClient: () => ({
startCopyFromURL: mockStartCopyFromURL,
url: fakeSrcURL
})
}))
files = await AzureBlobFiles.init(fakeSASCredentials)
})
test('when source file exists', async () => {
await files._copyRemoteToRemoteFile(src, dest)
expect(mockStartCopyFromURL).toHaveBeenCalledTimes(1)
expect(mockStartCopyFromURL.mock.calls[0]).toEqual(expect.arrayContaining([fakeSrcURL]))
})
test('when azure.uploadStreamToBlockBlob throws an error', async () => {
await testWithProviderError(
files._copyRemoteToRemoteFile.bind(files, src, dest),
mockStartCopyFromURL,
{ srcPath: src, destPath: dest },
src)
})
})
describe('_fileExists', () => {
const mockBlobGetProperties = jest.fn()
const fileInPrivateDir = 'dir/inadir/file.html'
const fileInRoot = 'afile.html'
const fileInPublicDir = 'public/afile.html'
const fileInPublicSubDir = 'public/sub/afile.html'
const fileWithoutExtension = 'afile'
const fakeAzureFileProps = { fake: 'props' }
let files
beforeEach(async () => {
mockBlobGetProperties.mockReset()
azure.ContainerClient.mockImplementation(() => ({
getBlockBlobClient: () => ({
getProperties: mockBlobGetProperties
})
}))
files = await AzureBlobFiles.init(fakeSASCredentials)
})
const expectExists = async (exists) => {
exists ? mockBlobGetProperties.mockResolvedValue(fakeAzureFileProps) : mockBlobGetProperties.mockRejectedValue({ response: { status: 404 } })
expect(await files._fileExists(fileInPrivateDir)).toEqual(exists)
expect(await files._fileExists(fileInRoot)).toEqual(exists)
expect(await files._fileExists(fileInPublicDir)).toEqual(exists)
expect(await files._fileExists(fileInPublicSubDir)).toEqual(exists)
expect(await files._fileExists(fileWithoutExtension)).toEqual(exists)
expect(mockBlobGetProperties).toHaveBeenCalled()
}
test('when it exists', async () => expectExists(true))
test('when it does not exists', async () => expectExists(false))
test('when there is a provider error on azure.BlockBlobURL.getProperties',
async () => testWithProviderError(files._fileExists.bind(files, fileInPrivateDir), mockBlobGetProperties, { filePath: fileInPrivateDir })
)
})
describe('_getUrl', () => {
const setMockBlobUrl = url => {
const mockContainerClientInstance = {
url: fakeSASCredentials.sasURLPrivate, // some fake container url
createIfNotExists: () => Promise.resolve(),
getBlockBlobClient: () => ({
url
})
}
// mock for byo init
azure.BlobServiceClient.mockImplementation(() => {
return {
getContainerClient: jest.fn(() => mockContainerClientInstance)
}
})
// mock for tvm init
azure.ContainerClient.mockImplementation(() => mockContainerClientInstance)
}
const tvm = jest.fn()
test('url with no query args', async () => {
const cleanUrl = 'https://fake.blob.core.windows.net/fake/fakesub/afile'
setMockBlobUrl(cleanUrl) // must be set before init
const files = await AzureBlobFiles.init(fakeSASCredentials, tvm)
const expectedUrl = DEFAULT_CDN_STORAGE_HOST + '/fake/fakesub/afile'
const url = files._getUrl('fakepath')
expect(url).toEqual(expectedUrl)
})
test('url with query args', async () => {
const cleanUrl = 'https://fake.blob.core.windows.net/fake/fakesub/afile'
setMockBlobUrl(cleanUrl + '?password=xxxx&user=username')
const files = await AzureBlobFiles.init(fakeSASCredentials, tvm)
const expectedUrl = DEFAULT_CDN_STORAGE_HOST + '/fake/fakesub/afile'
const url = files._getUrl('fakepath')
expect(url).toEqual(expectedUrl)
})
test('url for custom host and byo', async () => {
const cleanUrl = 'https://fake.blob.core.windows.net/fake/fakesub/afile'
setMockBlobUrl(cleanUrl)
const files = await AzureBlobFiles.init({ ...fakeUserCredentials, hostName: 'fakeHost' }, null)
const expectedUrl = 'https://fakeHost/fake/fakesub/afile'
const url = files._getUrl('fakepath')
expect(url).toEqual(expectedUrl)
})
test('url for stage', async () => {
libEnv.getCliEnv.mockReturnValue('stage')
const files = await AzureBlobFiles.init({ ...fakeUserCredentials }, tvm)
const expectedUrl = 'https://firefly-stage.azureedge.net/fake/fakesub/afile'
const url = files._getUrl('fakepath')
expect(url).toEqual(expectedUrl)
})
test('url for invalid env should default to prod', async () => {
libEnv.getCliEnv.mockReturnValue('stage1')
const files = await AzureBlobFiles.init({ ...fakeUserCredentials }, tvm)
const expectedUrl = 'https://firefly.azureedge.net/fake/fakesub/afile'
const url = files._getUrl('fakepath')
expect(url).toEqual(expectedUrl)
})
})
describe('_getPresignUrl', () => {
const setMockBlobUrl = url => {
const mockContainerClientInstance = {
url: fakeSASCredentials.sasURLPrivate, // some fake container url
createIfNotExists: () => Promise.resolve(),
getBlockBlobClient: () => ({
url
})
}
// mock for byo init
azure.BlobServiceClient.mockImplementation(() => {
return {
getContainerClient: jest.fn(() => mockContainerClientInstance)
}
})
// mock for tvm init
azure.ContainerClient.mockImplementation(() => mockContainerClientInstance)
}
const tvm = jest.fn()
azure.generateBlobSASQueryParameters = jest.fn()
azure.BlobSASPermissions.parse = jest.fn()
beforeEach(async () => {
tvm.mockReset()
azure.generateBlobSASQueryParameters.mockReset()
azure.BlobSASPermissions.parse.mockReset()
// defaults that work
azure.generateBlobSASQueryParameters.mockReturnValue({ toString: () => 'fakeSAS' })
azure.BlobSASPermissions.parse.mockReturnValue({ toString: () => 'fakePermissionStr' })
tvm.getAzureBlobPresignCredentials = jest.fn()
tvm.getAzureBlobPresignCredentials.mockResolvedValue({
signature: 'defaultSign'
})
fetch.mockResolvedValue({
text: fakeResponse
})
fakeResponse.mockResolvedValue(fakeAccessPolicy)
setMockBlobUrl('default')
})
test('_getPresignUrl with no options', async () => {
const files = await AzureBlobFiles.init(fakeSASCredentials, tvm)
await expect(files._getPresignUrl('fakesub/afile')).rejects.toThrow('[FilesLib:ERROR_MISSING_OPTION] expiryInSeconds')
})
test('_getPresignUrl with missing options', async () => {
const files = await AzureBlobFiles.init(fakeSASCredentials, tvm)
await expect(files._getPresignUrl('fakesub/afile', { test: 'fake' })).rejects.toThrow('[FilesLib:ERROR_MISSING_OPTION] expiryInSeconds')
})
test('_getPresignUrl with correct options default permission', async () => {
const cleanUrl = 'https://fake.blob.core.windows.net/fake/fakesub/afile'
setMockBlobUrl(cleanUrl) // to set before files init
const files = await AzureBlobFiles.init(fakeSASCredentials, tvm)
const expectedUrl = DEFAULT_CDN_STORAGE_HOST + '/fake/fakesub/afile?defaultSign'
let url = await files._getPresignUrl('fakesub/afile', { expiryInSeconds: 60 })
expect(url).toEqual(expectedUrl)
expect(tvm.getAzureBlobPresignCredentials).toHaveBeenCalledWith({ blobName: 'fakesub/afile', expiryInSeconds: 60, permissions: 'r' })
// urlType = 'external'
url = await files._getPresignUrl('fakesub/afile', { expiryInSeconds: 60, urlType: 'external' })
expect(url).toEqual(expectedUrl)
expect(tvm.getAzureBlobPresignCredentials).toHaveBeenCalledWith({ blobName: 'fakesub/afile', expiryInSeconds: 60, permissions: 'r' })
})
test('_getPresignUrl with correct options and urlType = internal', async () => {
const cleanUrl = 'https://fake.blob.core.windows.net/fake/fakesub/afile'
setMockBlobUrl(cleanUrl) // to set before files init
const files = await AzureBlobFiles.init(fakeSASCredentials, tvm)
const expectedUrl = cleanUrl + '?defaultSign'
const url = await files._getPresignUrl('fakesub/afile', { expiryInSeconds: 60, urlType: 'internal' })
expect(url).toEqual(expectedUrl)
expect(tvm.getAzureBlobPresignCredentials).toHaveBeenCalledWith({ blobName: 'fakesub/afile', expiryInSeconds: 60, permissions: 'r' })
})
test('_getPresignUrl with correct options explicit permissions', async () => {
const cleanUrl = 'https://fake.blob.core.windows.net/fake/fakesub/afile'
setMockBlobUrl(cleanUrl)
const files = await AzureBlobFiles.init(fakeSASCredentials, tvm)
const expectedUrl = DEFAULT_CDN_STORAGE_HOST + '/fake/fakesub/afile?defaultSign'
const url = await files._getPresignUrl('fakesub/afile', { expiryInSeconds: 60, permissions: 'fake' })
expect(url).toEqual(expectedUrl)
expect(tvm.getAzureBlobPresignCredentials).toHaveBeenCalledWith({ blobName: 'fakesub/afile', expiryInSeconds: 60, permissions: 'fake' })
})
test('_getPresignUrl with correct options default permission own credentials', async () => {
const cleanUrl = 'https://fake.blob.core.windows.net/fake/fakesub/afile'
setMockBlobUrl(cleanUrl)
const files = await AzureBlobFiles.init(fakeUserCredentials)
const expectedUrl = 'https://fake.blob.core.windows.net/fake/fakesub/afile?fakeSAS'
const url = await files._getPresignUrl('fakesub/afile', { expiryInSeconds: 60 })
expect(url).toEqual(expectedUrl)
expect(azure.generateBlobSASQueryParameters).toHaveBeenCalledWith(expect.objectContaining({
containerName: fakeUserCredentials.containerName,
blobName: 'fakesub/afile',
permissions: 'fakePermissionStr'
}), expect.any(Object))
})
test('_getPresignUrl for public file with correct options default permission own credentials', async () => {
const cleanUrl = 'https://fake.blob.core.windows.net/fake/fakesub/afile'
setMockBlobUrl(cleanUrl)
const files = await AzureBlobFiles.init(fakeUserCredentials)
const expectedUrl = 'https://fake.blob.core.windows.net/fake/fakesub/afile?fakeSAS'
const url = await files._getPresignUrl('public/afile', { expiryInSeconds: 60 })
expect(url).toEqual(expectedUrl)
expect(azure.generateBlobSASQueryParameters).toHaveBeenCalledWith(expect.objectContaining({
containerName: fakeUserCredentials.containerName + '-public',
blobName: 'public/afile',
permissions: 'fakePermissionStr'
}), expect.any(Object))
})
test('_getPresignUrl with correct options explicit permission own credentials', async () => {
const cleanUrl = 'https://fake.blob.core.windows.net/fake/fakesub/afile'
setMockBlobUrl(cleanUrl)
const files = await AzureBlobFiles.init(fakeUserCredentials)
const expectedUrl = 'https://fake.blob.core.windows.net/fake/fakesub/afile?fakeSAS'
const url = await files._getPresignUrl('fakesub/afile', { expiryInSeconds: 60, permissions: 'fake' })
expect(url).toEqual(expectedUrl)
})
test('_getPresignUrl with correct options explicit permission own sas credentials', async () => {
const files = await AzureBlobFiles.init(fakeSASCredentials)
await expect(files._getPresignUrl(
'fakesub/afile',
{ expiryInSeconds: 60 }
)).rejects.toThrow('[FilesLib:ERROR_UNSUPPORTED_OPERATION] generatePresignURL is not supported with Azure Container SAS credentials, please initialize the SDK with Azure storage account credentials instead')
})
})
describe('_revokeAllPresignURLs', () => {
// for byo
const mockSetAccessPolicy = jest.fn()
const mockSetAccessPolicyPublic = jest.fn()
// for tvm
const tvm = jest.fn()
/** @type {AzureBlobFiles} */
let files
azure.generateBlobSASQueryParameters = jest.fn()
azure.BlobSASPermissions.parse = jest.fn()
beforeEach(async () => {
tvm.mockReset()
azure.generateBlobSASQueryParameters.mockReset()
azure.BlobSASPermissions.parse.mockReset()
mockSetAccessPolicy.mockReset()
const mockContainerClientInstance = {
url: fakeSASCredentials.sasURLPrivate, // some fake container url
createIfNotExists: () => Promise.resolve(),
setAccessPolicy: mockSetAccessPolicy,
getBlockBlobClient: () => ({
})
}
const mockContainerClientInstancePublic = {
url: fakeSASCredentials.sasURLPublic, // some fake container url
createIfNotExists: () => Promise.resolve(),
setAccessPolicy: mockSetAccessPolicyPublic,
getBlockBlobClient: () => ({
})
}
// mock for byo init
azure.BlobServiceClient.mockImplementation(() => {
return {
getContainerClient: jest.fn(name => name.endsWith('-public') ? mockContainerClientInstancePublic : mockContainerClientInstance)
}
})
// mock for tvm init
azure.ContainerClient.mockImplementation(() => mockContainerClientInstance)
// defaults that work
azure.generateBlobSASQueryParameters.mockReturnValue({ toString: () => 'fakeSAS' })
azure.BlobSASPermissions.parse.mockReturnValue({ toString: () => 'fakePermissionStr' })
tvm.revokeAzureBlobPresignCredentials = jest.fn()
tvm.revokeAzureBlobPresignCredentials.mockResolvedValue({})
tvm.revokePresignURLs = jest.fn()
tvm.revokePresignURLs.mockResolvedValue({})
fetch.mockResolvedValue({
text: fakeResponse
})
fakeResponse.mockResolvedValue(fakeAccessPolicy)
files = await AzureBlobFiles.init(fakeSASCredentials, tvm)
})
test('_revokeAllPresignURLs via tvm', async () => {
await files._revokeAllPresignURLs()
expect(tvm.revokePresignURLs).toHaveBeenCalled()
expect(mockSetAccessPolicy).not.toHaveBeenCalled()
})
test('_revokeAllPresignURLs with own credentials', async () => {
files = await AzureBlobFiles.init(fakeUserCredentials)
await files._revokeAllPresignURLs()
expect(tvm.revokePresignURLs).not.toHaveBeenCalled()
expect(mockSetAccessPolicy).toHaveBeenCalledWith(undefined, [{ accessPolicy: { permission: '' }, id: 'fake-uuid' }])
expect(mockSetAccessPolicyPublic).toHaveBeenCalledWith('blob', [{ accessPolicy: { permission: '' }, id: 'fake-uuid' }])
})
test('_revokeAllPresignURLs with own sas credentials', async () => {
files = await AzureBlobFiles.init(fakeSASCredentials)
await expect(files._revokeAllPresignURLs())
.rejects.toThrow('[FilesLib:ERROR_UNSUPPORTED_OPERATION] revokeAllPresignURLs is not supported with Azure Container SAS credentials, please initialize the SDK with Azure storage account credentials instead')
})
})
describe('_initWithNewCreds', () => {
const tvm = jest.fn()
let files
const mockContainerCreateIfNotExists = jest.fn()
const mockSetAccessPolicy = jest.fn()
const mockContainerClientInstance = {
createIfNotExists: mockContainerCreateIfNotExists,
setAccessPolicy: mockSetAccessPolicy,
url: fakeSASCredentials.sasURLPrivate
}
beforeEach(async () => {
tvm.mockReset()
tvm.getAzureBlobCredentials = jest.fn()
tvm.getAzureBlobCredentials.mockResolvedValue(fakeSASCredentials)
mockContainerCreateIfNotExists.mockReset()
mockSetAccessPolicy.mockReset()
// mock needed for byo master keys init
azure.BlobServiceClient.mockImplementation(() => {
return {
getContainerClient: jest.fn(() => mockContainerClientInstance)
}
})
mockContainerCreateIfNotExists.mockResolvedValue('all good')
files = await AzureBlobFiles.init(fakeSASCredentials, tvm)
})
test('_initWithNewCreds pass valid new creds', async () => {
await files._initWithNewCreds(fakeSASCredentials)
expect(tvm.getAzureBlobCredentials).not.toHaveBeenCalled()
})
test('_initWithNewCreds no creds hasOwnCredentials = false', async () => {
await files._initWithNewCreds()
expect(tvm.getAzureBlobCredentials).toHaveBeenCalled()
})
test('_initWithNewCreds no creds hasOwnCredentials = true', async () => {
files = await AzureBlobFiles.init(fakeUserCredentials)
await files._initWithNewCreds()
expect(tvm.getAzureBlobCredentials).not.toHaveBeenCalled()
})
})
describe('_statusFromProviderError', () => {
/** @type {AzureBlobFiles} */
let files
beforeEach(async () => {
files = await AzureBlobFiles.init(fakeSASCredentials)
})
test('error has no response field', async () => {
const status = await files._statusFromProviderError(new Error('yolo'))
expect(status).toEqual(undefined)
})
test('error has no response.status field', async () => {
const status = await files._statusFromProviderError({ response: 'yolo' })
expect(status).toEqual(undefined)
})
test('error has response.status field', async () => {
const status = await files._statusFromProviderError({ response: { status: 404 } })
expect(status).toEqual(404)
})
})