From 743dd4fea64c8ef2cbb278712e2dd85117d6e7a5 Mon Sep 17 00:00:00 2001 From: sandeep-paliwal Date: Tue, 10 Nov 2020 18:20:27 +0530 Subject: [PATCH] Revoke presign URLs (#38) * TVM changes to support presign revoke * moved common logic to AzureUtil js to get container URL --- actions/azure-revoke/index.js | 24 ++++ lib/impl/AzureBlobTvm.js | 11 +- lib/impl/AzurePresignTvm.js | 10 +- lib/impl/AzureRevokePresignTvm.js | 49 +++++++ lib/impl/AzureUtil.js | 124 ++++++++++++++++++ manifest.yml | 21 +++ package.json | 5 +- test/unit/actions/azure-revoke.test.js | 24 ++++ test/unit/lib/impl/AzureBlobTvm.test.js | 14 +- test/unit/lib/impl/AzurePresignTvm.test.js | 18 ++- .../lib/impl/AzureRevokePresignTvm.test.js | 52 ++++++++ test/unit/lib/impl/AzureUtil.test.js | 99 ++++++++++++++ 12 files changed, 440 insertions(+), 11 deletions(-) create mode 100644 actions/azure-revoke/index.js create mode 100644 lib/impl/AzureRevokePresignTvm.js create mode 100644 lib/impl/AzureUtil.js create mode 100644 test/unit/actions/azure-revoke.test.js create mode 100644 test/unit/lib/impl/AzureRevokePresignTvm.test.js create mode 100644 test/unit/lib/impl/AzureUtil.test.js diff --git a/actions/azure-revoke/index.js b/actions/azure-revoke/index.js new file mode 100644 index 0000000..81478e9 --- /dev/null +++ b/actions/azure-revoke/index.js @@ -0,0 +1,24 @@ +/* +Copyright 2020 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 { AzureRevokePresignTvm } = require('../../lib/impl/AzureRevokePresignTvm') +const azureRevokePresignTvm = new AzureRevokePresignTvm() + +/** + * @param {object} params the input params + * @returns {Promise} tvm response + */ +async function main (params) { + return azureRevokePresignTvm.processRequest(params) +} + +exports.main = main diff --git a/lib/impl/AzureBlobTvm.js b/lib/impl/AzureBlobTvm.js index adda530..354977d 100644 --- a/lib/impl/AzureBlobTvm.js +++ b/lib/impl/AzureBlobTvm.js @@ -11,6 +11,7 @@ governing permissions and limitations under the License. */ const { Tvm } = require('../Tvm') +const azureUtil = require('./AzureUtil') const azure = require('@azure/storage-blob') // eslint-disable-next-line jsdoc/require-jsdoc @@ -53,10 +54,12 @@ class AzureBlobTvm extends Tvm { const publicContainerName = containerName + '-public' // create containers - we need to do it here as the sas creds do not allow it - const pipeline = azure.StorageURL.newPipeline(sharedKeyCredential) - const serviceURL = new azure.ServiceURL(accountURL, pipeline) - await _createContainerIfNotExists(azure.ContainerURL.fromServiceURL(serviceURL, publicContainerName), azure.Aborter.none, { access: 'blob', metadata: { namespace: params.owNamespace } }) - await _createContainerIfNotExists(azure.ContainerURL.fromServiceURL(serviceURL, privateContainerName), azure.Aborter.none, { metadata: { namespace: params.owNamespace } }) + const privateContainerURL = azureUtil.getContainerURL(accountURL, sharedKeyCredential, privateContainerName) + await _createContainerIfNotExists(azureUtil.getContainerURL(accountURL, sharedKeyCredential, publicContainerName), azure.Aborter.none, { access: 'blob', metadata: { namespace: params.owNamespace } }) + await _createContainerIfNotExists(privateContainerURL, azure.Aborter.none, { metadata: { namespace: params.owNamespace } }) + + // set default access policy if it does not exists + await azureUtil.addAccessPolicyIfNotExists(privateContainerURL, params.azureStorageAccount, params.azureStorageAccessKey) // generate SAS token const expiryTime = new Date() diff --git a/lib/impl/AzurePresignTvm.js b/lib/impl/AzurePresignTvm.js index 5f5147e..7d7ea52 100644 --- a/lib/impl/AzurePresignTvm.js +++ b/lib/impl/AzurePresignTvm.js @@ -12,6 +12,7 @@ governing permissions and limitations under the License. const joi = require('@hapi/joi') const { Tvm } = require('../Tvm') +const azureUtil = require('./AzureUtil') const azure = require('@azure/storage-blob') /** @@ -43,9 +44,15 @@ class AzurePresignTvm extends Tvm { * @private */ async _generateCredentials (params) { + const accountURL = `https://${params.azureStorageAccount}.blob.core.windows.net` const sharedKeyCredential = new azure.SharedKeyCredential(params.azureStorageAccount, params.azureStorageAccessKey) const containerName = Tvm._hash(params.owNamespace) + const privateContainerURL = azureUtil.getContainerURL(accountURL, sharedKeyCredential, containerName) + const identifier = await azureUtil.getAccessPolicy(privateContainerURL, params.azureStorageAccount, params.azureStorageAccessKey) + + if (identifier === undefined) { throw new Error(`No Access Policy set for container ${containerName}`) } + // generate SAS token const expiryTime = new Date(Date.now() + (1000 * params.expiryInSeconds)) const perm = (params.permissions === undefined) ? 'r' : params.permissions @@ -54,7 +61,8 @@ class AzurePresignTvm extends Tvm { const commonSasParams = { permissions: permissions.toString(), expiryTime: expiryTime, - blobName: params.blobName + blobName: params.blobName, + identifier: identifier } const sasQueryParamsPrivate = azure.generateBlobSASQueryParameters({ ...commonSasParams, containerName: containerName }, sharedKeyCredential) diff --git a/lib/impl/AzureRevokePresignTvm.js b/lib/impl/AzureRevokePresignTvm.js new file mode 100644 index 0000000..5907347 --- /dev/null +++ b/lib/impl/AzureRevokePresignTvm.js @@ -0,0 +1,49 @@ +/* +Copyright 2020 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 { Tvm } = require('../Tvm') +const azureUtil = require('./AzureUtil') +const azure = require('@azure/storage-blob') + +/** + * @class AzureRevokePresignTvm + * @classdesc Tvm implementation for Azure Revoke Presign URLs + * @augments {Tvm} + */ +class AzureRevokePresignTvm extends Tvm { + /** + * @memberof AzureRevokePresignTvm + * @override + */ + constructor () { + super() + this._addToValidationSchema('azureStorageAccount') + this._addToValidationSchema('azureStorageAccessKey') + } + + /** + * @memberof AzureRevokePresignTvm + * @override + * @private + */ + async _generateCredentials (params) { + const containerName = Tvm._hash(params.owNamespace) + const accountURL = `https://${params.azureStorageAccount}.blob.core.windows.net` + + const sharedKeyCredential = new azure.SharedKeyCredential(params.azureStorageAccount, params.azureStorageAccessKey) + const containerURL = azureUtil.getContainerURL(accountURL, sharedKeyCredential, containerName) + + await azureUtil.setAccessPolicy(containerURL, params.azureStorageAccount) + } +} + +module.exports = { AzureRevokePresignTvm } diff --git a/lib/impl/AzureUtil.js b/lib/impl/AzureUtil.js new file mode 100644 index 0000000..5156a78 --- /dev/null +++ b/lib/impl/AzureUtil.js @@ -0,0 +1,124 @@ +/* +Copyright 2020 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') +const Crypto = require('crypto') +const { v4: uuidv4 } = require('uuid') +const xmlJS = require('xml-js') + +const azure = require('@azure/storage-blob') + +/** + * Sign Request + * + * @param {string} method http method + * @param {string} resource azure resource to be used + * @param {string }date Date string + * @param {string} storageAccessKey access Key + * @returns {string} signed request + */ +function _signRequest (method, resource, date, storageAccessKey) { + const canonicalHeaders = 'x-ms-date:' + date + '\n' + 'x-ms-version:2019-02-02' + var stringToSign = method + '\n\n\n\n\n\n\n\n\n\n\n\n' + canonicalHeaders + '\n' + resource + return Crypto.createHmac('sha256', Buffer.from(storageAccessKey, 'base64')).update(stringToSign, 'utf8').digest('base64') +} + +/** + * Get Access policy + * + * @param {object} containerURL azure container URL + * @param {string} storageAccount azure account + * @param {string} storageAccessKey azure access key + * @returns {string} Id for access policy + */ +async function getAccessPolicy (containerURL, storageAccount, storageAccessKey) { + // use API call as this._azure.containerURLPrivate.getAccessPolicy calls fails for policy with empty permissions + var index = containerURL.url.lastIndexOf('/') + var containerName = containerURL.url.substring(index + 1, containerURL.url.length) + + const resource = '/' + storageAccount + '/' + containerName + '\ncomp:acl\nrestype:container' + const date = new Date().toUTCString() + const sign = _signRequest('GET', resource, date, storageAccessKey) + + const reqHeaders = { + 'x-ms-date': date, + 'x-ms-version': '2019-02-02', + authorization: 'SharedKey ' + storageAccount + ':' + sign + } + const url = containerURL.url + '?restype=container&comp=acl' + const res = await fetch(url, { method: 'GET', headers: reqHeaders }) + + const acl = await res.text() + const aclObj = xmlJS.xml2js(acl) + let id + if (aclObj.elements) { + const signedIdentifiers = aclObj.elements[0] + if (signedIdentifiers.elements) { + const signedIdentifier = signedIdentifiers.elements[0].elements + signedIdentifier.forEach(function (val, index, arr) { + if (val.name === 'Id') { + id = val.elements[0].text + return id + } + }) + } + } + return id +} + +/** + * Set new acceess policy + * + * @param {object} containerURL azure container URL + * @returns {void} + */ +async function setAccessPolicy (containerURL) { + const id = uuidv4() + // set access policy with new id and without any permissions + await containerURL.setAccessPolicy(azure.Aborter.none, undefined, [{ id: id, accessPolicy: { permission: '' } }]) +} + +/** + * Add new access policy if it doest not exists + * + * @param {object} containerURL azure container URL + * @param {string} storageAccount azure account + * @param {string} storageAccessKey azure access key + * @returns {void} + */ +async function addAccessPolicyIfNotExists (containerURL, storageAccount, storageAccessKey) { + const identifier = await getAccessPolicy(containerURL, storageAccount, storageAccessKey) + if (!identifier) { + await setAccessPolicy(containerURL) + } +} + +/** + * Get Container URL + * + * @param {string} accountURL account URL + * @param {object} sharedKeyCredential azure sharedKeyCredential object + * @param {string} containerName azure container name + * @returns {object} container URL object + */ +function getContainerURL (accountURL, sharedKeyCredential, containerName) { + const pipeline = azure.StorageURL.newPipeline(sharedKeyCredential) + const serviceURL = new azure.ServiceURL(accountURL, pipeline) + return azure.ContainerURL.fromServiceURL(serviceURL, containerName) +} + +module.exports = { + getAccessPolicy: getAccessPolicy, + setAccessPolicy: setAccessPolicy, + addAccessPolicyIfNotExists: addAccessPolicyIfNotExists, + getContainerURL: getContainerURL +} diff --git a/manifest.yml b/manifest.yml index c721b52..fb4adbc 100644 --- a/manifest.yml +++ b/manifest.yml @@ -47,6 +47,21 @@ packages: annotations: # this is important security wise final: true + azure-revoke: + function: actions/azure-revoke/index.js + web: yes + runtime: 'nodejs:10' + inputs: + azureStorageAccount: $AZURE_STORAGE_ACCOUNT + azureStorageAccessKey: $AZURE_STORAGE_ACCESS_KEY + expirationDuration: $EXPIRATION_DURATION + owApihost: $AIO_RUNTIME_APIHOST + approvedList: $APPROVED_LIST + imsEnv: $IMS_ENV + disableAdobeIOApiGwTokenValidation: $DISABLE_ADOBE_IO_API_GW_TOKEN_VALIDATION + annotations: + # this is important security wise + final: true azure-cosmos: function: actions/azure-cosmos/index.js web: yes @@ -77,6 +92,12 @@ packages: azure-presign: method: GET response: http + tvm-azure-revoke: + tvm: + azure/revoke/{owNamespace}: + azure-revoke: + method: GET + response: http tvm-azure-cosmos: tvm: azure/cosmos/{owNamespace}: diff --git a/package.json b/package.json index a5237c6..97883f4 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,9 @@ "aws-sdk": "^2.539.0", "crypto": "^1.0.1", "lru-cache": "^6.0.0", - "openwhisk": "^3.20.0" + "openwhisk": "^3.20.0", + "node-fetch": "^2.6.0", + "uuid": "^8.3.1", + "xml-js": "^1.6.11" } } diff --git a/test/unit/actions/azure-revoke.test.js b/test/unit/actions/azure-revoke.test.js new file mode 100644 index 0000000..53c6f56 --- /dev/null +++ b/test/unit/actions/azure-revoke.test.js @@ -0,0 +1,24 @@ +/* +Copyright 2020 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 azureRevokePresignAction = require('../../../actions/azure-revoke') + +const { AzureRevokePresignTvm } = require('../../../lib/impl/AzureRevokePresignTvm') +jest.mock('../../../lib/impl/AzureRevokePresignTvm') + +beforeEach(() => { + AzureRevokePresignTvm.prototype.processRequest.mockReset() +}) +test('azure-revoke action has a main function and calls AzureRevokePresignTvm.processRequest', async () => { + const fakeParams = { a: { nested: 'param' }, another: 'param' } + await azureRevokePresignAction.main(fakeParams) + expect(AzureRevokePresignTvm.prototype.processRequest).toHaveBeenCalledWith(fakeParams) +}) diff --git a/test/unit/lib/impl/AzureBlobTvm.test.js b/test/unit/lib/impl/AzureBlobTvm.test.js index dce9852..f7ec9c3 100644 --- a/test/unit/lib/impl/AzureBlobTvm.test.js +++ b/test/unit/lib/impl/AzureBlobTvm.test.js @@ -12,6 +12,10 @@ governing permissions and limitations under the License. const { AzureBlobTvm } = require('../../../../lib/impl/AzureBlobTvm') +const azureUtil = require('../../../../lib/impl/AzureUtil') +jest.mock('../../../../lib/impl/AzureUtil') +azureUtil.addAccessPolicyIfNotExists = jest.fn() + const azure = require('@azure/storage-blob') jest.mock('@azure/storage-blob') @@ -21,13 +25,15 @@ const azureContainerCreateMock = jest.fn() azure.SharedKeyCredential = jest.fn() azure.StorageURL.newPipeline = jest.fn() azure.ServiceURL = jest.fn() -azure.ContainerURL.fromServiceURL = jest.fn().mockReturnValue({ - create: azureContainerCreateMock -}) + azure.ContainerURL.prototype.create = jest.fn() azure.generateBlobSASQueryParameters = jest.fn() azure.Aborter.none = {} +azureUtil.getContainerURL.mockReturnValue({ + create: azureContainerCreateMock +}) + class FakePermission { toString () { return (this.add && this.read && this.create && this.delete && this.write && this.list && 'ok') || 'not ok' @@ -56,6 +62,7 @@ describe('processRequest (Azure Cosmos)', () => { tvm = new AzureBlobTvm() azureContainerCreateMock.mockReset() azure.generateBlobSASQueryParameters.mockReset() + azureUtil.addAccessPolicyIfNotExists.mockClear() // defaults that work azure.generateBlobSASQueryParameters.mockReturnValue({ toString: () => fakeSas }) @@ -74,6 +81,7 @@ describe('processRequest (Azure Cosmos)', () => { const containerName = global.nsHash expect(response.statusCode).toEqual(200) + expect(azureUtil.addAccessPolicyIfNotExists).toHaveBeenCalledTimes(1) expect(response.body).toEqual({ sasURLPrivate: expect.stringContaining(containerName), sasURLPublic: expect.stringContaining('public'), diff --git a/test/unit/lib/impl/AzurePresignTvm.test.js b/test/unit/lib/impl/AzurePresignTvm.test.js index 2a5c524..32a38e2 100644 --- a/test/unit/lib/impl/AzurePresignTvm.test.js +++ b/test/unit/lib/impl/AzurePresignTvm.test.js @@ -11,6 +11,10 @@ governing permissions and limitations under the License. */ const { AzurePresignTvm } = require('../../../../lib/impl/AzurePresignTvm') +const azureUtil = require('../../../../lib/impl/AzureUtil') +jest.mock('../../../../lib/impl/AzureUtil') +azureUtil.getAccessPolicy.mockResolvedValue('fakeIdentifier') +azureUtil.getContainerURL.mockReturnValue({ fake: '' }) const azure = require('@azure/storage-blob') jest.mock('@azure/storage-blob') @@ -19,7 +23,7 @@ jest.mock('@azure/storage-blob') azure.SharedKeyCredential = jest.fn() azure.StorageURL.newPipeline = jest.fn() azure.ServiceURL = jest.fn() -azure.ContainerURL.prototype.create = jest.fn() + azure.generateBlobSASQueryParameters = jest.fn() azure.BlobSASPermissions.parse = jest.fn() azure.Aborter.none = {} @@ -58,6 +62,7 @@ describe('processRequest (Azure Presign)', () => { tvm = new AzurePresignTvm() azure.generateBlobSASQueryParameters.mockReset() azure.BlobSASPermissions.parse.mockReset() + azureUtil.getAccessPolicy.mockClear() // defaults that work azure.generateBlobSASQueryParameters.mockReturnValue({ toString: () => fakeSas }) @@ -83,9 +88,10 @@ describe('processRequest (Azure Presign)', () => { tempParams.permissions = permissions const response = await tvm.processRequest(tempParams) expect(response.statusCode).toEqual(200) + expect(azureUtil.getAccessPolicy).toHaveBeenCalledTimes(1) expect(response.body).toEqual({ signature: fakeSas }) expect(azure.generateBlobSASQueryParameters).toHaveBeenCalledTimes(1) - expect(azure.generateBlobSASQueryParameters).toHaveBeenCalledWith(expect.objectContaining({ permissions: fakePermissionStr }), expect.any(Object)) + expect(azure.generateBlobSASQueryParameters).toHaveBeenCalledWith(expect.objectContaining({ permissions: fakePermissionStr, identifier: 'fakeIdentifier' }), expect.any(Object)) expect(azure.generateBlobSASQueryParameters).toHaveBeenCalledWith(expect.objectContaining({ blobName: 'fakeBlob' }), expect.any(Object)) } test('generate signature with valid params', async () => testPresignSignature(tvm, presignReqFakeParams.permissions)) @@ -97,5 +103,13 @@ describe('processRequest (Azure Presign)', () => { test('generate signature with wd permissions', async () => testPresignSignature(tvm, 'wd')) test('generate signature with dwr permissions', async () => testPresignSignature(tvm, 'dwr')) + + test('when no access policy defined', async () => { + azureUtil.getAccessPolicy.mockResolvedValue(undefined) + const tempParams = JSON.parse(JSON.stringify(presignReqFakeParams)) + tempParams.permissions = 'r' + const response = await tvm.processRequest(tempParams) + global.expectServerError(response, 'No Access Policy set for container') + }) }) }) diff --git a/test/unit/lib/impl/AzureRevokePresignTvm.test.js b/test/unit/lib/impl/AzureRevokePresignTvm.test.js new file mode 100644 index 0000000..d43d6e9 --- /dev/null +++ b/test/unit/lib/impl/AzureRevokePresignTvm.test.js @@ -0,0 +1,52 @@ +/* +Copyright 2020 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 { AzureRevokePresignTvm } = require('../../../../lib/impl/AzureRevokePresignTvm') +const azureUtil = require('../../../../lib/impl/AzureUtil') +jest.mock('../../../../lib/impl/AzureUtil') +azureUtil.getContainerURL.mockReturnValue({ fake: '' }) + +const azure = require('@azure/storage-blob') +jest.mock('@azure/storage-blob') + +// mock azure blob +azure.SharedKeyCredential = jest.fn() +azure.StorageURL.newPipeline = jest.fn() +azure.ServiceURL = jest.fn() +azure.Aborter.none = {} + +// params +const presignReqFakeParams = JSON.parse(JSON.stringify(global.baseNoErrorParams)) +presignReqFakeParams.azureStorageAccount = 'fakeAccount' +presignReqFakeParams.azureStorageAccessKey = 'fakeKey' + +describe('processRequest (Azure Revoke Presign)', () => { + // setup + /** @type {AzureRevokePresignTvm} */ + let tvm + beforeEach(() => { + tvm = new AzureRevokePresignTvm() + }) + + describe('signature revoke tests', () => { + const testRevokeSignature = async (tvm) => { + const tempParams = JSON.parse(JSON.stringify(presignReqFakeParams)) + const response = await tvm.processRequest(tempParams) + expect(response.statusCode).toEqual(200) + expect(response.body).toEqual({}) + expect(azureUtil.setAccessPolicy).toHaveBeenCalledTimes(1) + expect(azureUtil.setAccessPolicy).toHaveBeenCalledWith({ fake: '' }, 'fakeAccount') + } + + test('revoke signature', async () => testRevokeSignature(tvm)) + }) +}) diff --git a/test/unit/lib/impl/AzureUtil.test.js b/test/unit/lib/impl/AzureUtil.test.js new file mode 100644 index 0000000..4074dd0 --- /dev/null +++ b/test/unit/lib/impl/AzureUtil.test.js @@ -0,0 +1,99 @@ +/* +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 azureUtil = require('../../../../lib/impl/AzureUtil') +const azure = require('@azure/storage-blob') +jest.mock('@azure/storage-blob') + +const fetch = require('node-fetch') +jest.mock('node-fetch', () => jest.fn()) + +const { v4: uuidv4 } = require('uuid') +jest.mock('uuid') +uuidv4.mockImplementation(() => 'fakeId') + +const fakeResponse = jest.fn() +const fakeAccount = 'fakeAccount' +const fakeKey = 'fakeKey' +const fakeAccessPolicy = 'fakepolicy' +const fakeEmptyAccessPolicy = '' +const fakeEmptyAccessPolicy1 = '' + +const mockSetAccessPolicy = jest.fn() +const mockFromServiceURL = jest.fn() +const mockAzureContainerURL = { + url: 'https://fakeAccount/fake', + setAccessPolicy: mockSetAccessPolicy +} + +azure.ContainerURL.fromServiceURL.mockReturnValue({}) + +describe('AzureUtil tests', () => { + fetch.mockResolvedValue({ + text: fakeResponse + }) + + beforeEach(async () => { + fakeResponse.mockResolvedValue(fakeAccessPolicy) + mockSetAccessPolicy.mockReset() + }) + describe('getAccessPolicy', () => { + test('getAccessPolicy valid policy', async () => { + const ret = await azureUtil.getAccessPolicy(mockAzureContainerURL, fakeAccount, fakeKey) + expect(ret).toBe('fakepolicy') + }) + + test('getAccessPolicy empty policy', async () => { + fakeResponse.mockResolvedValue(fakeEmptyAccessPolicy) + const ret = await azureUtil.getAccessPolicy(mockAzureContainerURL, fakeAccount, fakeKey) + expect(ret).toBe(undefined) + }) + + test('getAccessPolicy empty api response', async () => { + fakeResponse.mockResolvedValue(fakeEmptyAccessPolicy1) + const ret = await azureUtil.getAccessPolicy(mockAzureContainerURL, fakeAccount, fakeKey) + expect(ret).toBe(undefined) + }) + }) + + describe('setAccessPolicy', () => { + test('setAccessPolicy valid policy', async () => { + await azureUtil.setAccessPolicy(mockAzureContainerURL) + expect(mockSetAccessPolicy).toHaveBeenCalledTimes(1) + expect(mockSetAccessPolicy).toHaveBeenCalledWith(undefined, undefined, [{ accessPolicy: { permission: '' }, id: 'fakeId' }]) + }) + }) + + describe('addAccessPolicyIfNotExists', () => { + test('addAccessPolicyIfNotExists policy exists', async () => { + await azureUtil.addAccessPolicyIfNotExists(mockAzureContainerURL, fakeAccount, fakeKey) + expect(mockSetAccessPolicy).toHaveBeenCalledTimes(0) + }) + + test('addAccessPolicyIfNotExists policy does not exists', async () => { + fakeResponse.mockResolvedValue(fakeEmptyAccessPolicy) + await azureUtil.addAccessPolicyIfNotExists(mockAzureContainerURL, fakeAccount, fakeKey) + expect(mockSetAccessPolicy).toHaveBeenCalledTimes(1) + }) + }) + + describe('getContainerURL', () => { + test('getContainerURL valid params', async () => { + mockFromServiceURL.mockReturnValue({ fake: '' }) + const accountURL = `https://${fakeAccount}.blob.core.windows.net` + const sharedKeyCredential = new azure.SharedKeyCredential(fakeAccount, fakeKey) + const containerURL = await azureUtil.getContainerURL(accountURL, sharedKeyCredential, 'fakeContainer') + expect(containerURL).toStrictEqual({}) + expect(azure.ContainerURL.fromServiceURL).toHaveBeenCalledTimes(1) + }) + }) +})