From dbc5d141fbaba03b21ce0d0f20fe82fea3c79620 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 27 May 2020 19:38:51 -0300 Subject: [PATCH] Cover provider with unit tests (#32) * add mocha as test framework Signed-off-by: Michael Granados * separate provider in module to provide better access to separated areas Signed-off-by: Michael Granados * cover checkServiceAccount util Signed-off-by: Michael Granados * cover checkBucket to verify if bucket exists on GCS Signed-off-by: Michael Granados * cover mergeConfig function Signed-off-by: Michael Granados * cover upload action Signed-off-by: Michael Granados * add istanbul coverage Signed-off-by: Michael Granados * change workflow Signed-off-by: Michael Granados * remove not used line Signed-off-by: Michael Granados * cover #delete action Signed-off-by: Michael Granados * add tests for filenames using real cases for upload and upload with reference Signed-off-by: Michael Granados * rescue separate general parse JSON error from specific attributes errors Signed-off-by: Michael Granados * add EOL Signed-off-by: Michael Granados * cover malformed JSON Signed-off-by: Michael Granados --- .editorconfig | 16 + .eslintrc.json | 3 +- .github/workflows/npmpublish.yml | 5 +- .github/workflows/test.yml | 14 + .mocharc.json | 3 + .nycrc | 4 + lib/index.js | 199 +------- lib/provider.js | 206 ++++++++ package.json | 8 +- test/lib/index.js | 9 + test/lib/provider.js | 785 +++++++++++++++++++++++++++++++ 11 files changed, 1050 insertions(+), 202 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/workflows/test.yml create mode 100644 .mocharc.json create mode 100644 .nycrc create mode 100644 lib/provider.js create mode 100644 test/lib/index.js create mode 100644 test/lib/provider.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..473e451 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[{package.json,*.yml}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintrc.json b/.eslintrc.json index bea2974..9338f01 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,8 @@ { "env": { "node": true, - "es6": true + "es6": true, + "mocha": true }, "plugins": ["prettier"], "extends": [ diff --git a/.github/workflows/npmpublish.yml b/.github/workflows/npmpublish.yml index dc10bcb..4b8d7bc 100644 --- a/.github/workflows/npmpublish.yml +++ b/.github/workflows/npmpublish.yml @@ -5,15 +5,16 @@ on: types: [created] jobs: - publish-npm: + npm-publish: runs-on: ubuntu-latest + branches: master steps: - uses: actions/checkout@v1 - uses: actions/setup-node@v1 with: node-version: '10.x' registry-url: https://registry.npmjs.org/ - - run: npm ci + - run: npm install - run: npm publish env: NODE_AUTH_TOKEN: ${{secrets.npm_token}} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..48cd4e0 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,14 @@ +name: Test project +on: push + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-node@v1 + with: + node-version: '10.x' + registry-url: https://registry.npmjs.org/ + - run: npm install + - run: npm run coverage diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 0000000..3b0fdf1 --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,3 @@ +{ + "recursive": true +} diff --git a/.nycrc b/.nycrc new file mode 100644 index 0000000..4e9c338 --- /dev/null +++ b/.nycrc @@ -0,0 +1,4 @@ +{ + "all": true, + "include": ["lib"] +} diff --git a/lib/index.js b/lib/index.js index bebbb29..be1368c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,202 +1,7 @@ 'use strict'; -const path = require('path'); -const slugify = require('slugify'); -const { Storage } = require('@google-cloud/storage'); +const { init } = require('./provider'); -/** - * Check validity of Service Account configuration - * @param config - * @returns {{private_key}|{client_email}|{project_id}|any} - */ -const checkServiceAccount = (config) => { - if (!config.serviceAccount) { - throw new Error('"Service Account JSON" is required!'); - } - if (!config.bucketName) { - throw new Error('"Bucket name" is required!'); - } - if (!config.baseUrl) { - /** Set to default **/ - config.baseUrl = 'https://storage.googleapis.com/{bucket-name}'; - } - - let serviceAccount; - - try { - serviceAccount = - typeof config.serviceAccount === 'string' - ? JSON.parse(config.serviceAccount) - : config.serviceAccount; - } catch (e) { - throw new Error( - 'Error parsing data "Service Account JSON", please be sure to copy/paste the full JSON file.' - ); - } - - /** - * Check exist - */ - if (!serviceAccount.project_id) { - throw new Error( - 'Error parsing data "Service Account JSON". Missing "project_id" field in JSON file.' - ); - } - if (!serviceAccount.client_email) { - throw new Error( - 'Error parsing data "Service Account JSON". Missing "client_email" field in JSON file.' - ); - } - if (!serviceAccount.private_key) { - throw new Error( - 'Error parsing data "Service Account JSON". Missing "private_key" field in JSON file.' - ); - } - return serviceAccount; -}; - -/** - * Check bucket exist, or create it - * @param GCS - * @param bucketName - * @returns {Promise} - */ -const checkBucket = async (GCS, bucketName) => { - let bucket = GCS.bucket(bucketName); - await bucket.exists().then((data) => { - if (!data[0]) { - throw new Error( - 'An error occurs when we try to retrieve the Bucket "' + - bucketName + - '". Check if bucket exist on Google Cloud Platform.' - ); - } - }); -}; - -/** - * Merge uploadProvider config with gcs key in custom Strapi config - * @param uploadProviderConfig - * @returns {{private_key}|{client_email}|{project_id}|any} - */ -const mergeConfigs = (providerConfig) => { - let customGcsConfig = strapi.config.gcs ? strapi.config.gcs : {}; - let customEnvGcsConfig = strapi.config.currentEnvironment.gcs - ? strapi.config.currentEnvironment.gcs - : {}; - return { ...providerConfig, ...customGcsConfig, ...customEnvGcsConfig }; -}; - -/** - * - * @type {{init(*=): {upload(*=): Promise, delete(*): Promise}}} - */ module.exports = { - init(providerConfig) { - const config = mergeConfigs(providerConfig); - const serviceAccount = checkServiceAccount(config); - const GCS = new Storage({ - projectId: serviceAccount.project_id, - credentials: { - client_email: serviceAccount.client_email, - private_key: serviceAccount.private_key, - }, - }); - - return { - upload(file) { - return new Promise((resolve, reject) => { - const backupPath = - file.related && file.related.length > 0 && file.related[0].ref - ? `${file.related[0].ref}` - : `${file.hash}`; - const filePath = file.path ? `${file.path}/` : `${backupPath}/`; - const fileName = - slugify(path.basename(file.name + '_' + file.hash, file.ext)) + file.ext.toLowerCase(); - - checkBucket(GCS, config.bucketName) - .then(() => { - /** - * Check if the file already exist and force to remove it on Bucket - */ - GCS.bucket(config.bucketName) - .file(`${filePath}${fileName}`) - .exists() - .then((exist) => { - if (exist[0]) { - strapi.log.info('File already exist, try to remove it.'); - const fileName = `${file.url.replace( - config.baseUrl.replace('{bucket-name}', config.bucketName) + '/', - '' - )}`; - - GCS.bucket(config.bucketName) - .file(`${fileName}`) - .delete() - .then(() => { - strapi.log.debug(`File ${fileName} successfully deleted`); - }) - .catch((error) => { - if (error.code === 404) { - return strapi.log.warn( - 'Remote file was not found, you may have to delete manually.' - ); - } - }); - } - }); - }) - .then(() => { - /** - * Then save file - */ - GCS.bucket(config.bucketName) - .file(`${filePath}${fileName}`) - .save(file.buffer, { - contentType: file.mime, - public: true, - metadata: { - contentDisposition: `inline; filename="${file.name}"`, - }, - }) - .then(() => { - file.url = `${config.baseUrl.replace( - /{bucket-name}/, - config.bucketName - )}/${filePath}${fileName}`; - strapi.log.debug(`File successfully uploaded to ${file.url}`); - resolve(); - }) - .catch((error) => { - return reject(error); - }); - }); - }); - }, - delete(file) { - return new Promise((resolve, reject) => { - const fileName = `${file.url.replace( - config.baseUrl.replace('{bucket-name}', config.bucketName) + '/', - '' - )}`; - - GCS.bucket(config.bucketName) - .file(fileName) - .delete() - .then(() => { - strapi.log.debug(`File ${fileName} successfully deleted`); - }) - .catch((error) => { - if (error.code === 404) { - return strapi.log.warn( - 'Remote file was not found, you may have to delete manually.' - ); - } - reject(error); - }); - resolve(); - }); - }, - }; - }, + init, }; diff --git a/lib/provider.js b/lib/provider.js new file mode 100644 index 0000000..6da3c91 --- /dev/null +++ b/lib/provider.js @@ -0,0 +1,206 @@ +'use strict'; + +const path = require('path'); +const slugify = require('slugify'); +const { Storage } = require('@google-cloud/storage'); + +/** + * Check validity of Service Account configuration + * @param config + * @returns {{private_key}|{client_email}|{project_id}|any} + */ +const checkServiceAccount = (config) => { + if (!config.serviceAccount) { + throw new Error('"Service Account JSON" is required!'); + } + if (!config.bucketName) { + throw new Error('"Bucket name" is required!'); + } + if (!config.baseUrl) { + /** Set to default **/ + config.baseUrl = 'https://storage.googleapis.com/{bucket-name}'; + } + + let serviceAccount; + + try { + serviceAccount = + typeof config.serviceAccount === 'string' + ? JSON.parse(config.serviceAccount) + : config.serviceAccount; + } catch (e) { + throw new Error( + 'Error parsing data "Service Account JSON", please be sure to copy/paste the full JSON file.' + ); + } + + /** + * Check exist + */ + if (!serviceAccount.project_id) { + throw new Error( + 'Error parsing data "Service Account JSON". Missing "project_id" field in JSON file.' + ); + } + if (!serviceAccount.client_email) { + throw new Error( + 'Error parsing data "Service Account JSON". Missing "client_email" field in JSON file.' + ); + } + if (!serviceAccount.private_key) { + throw new Error( + 'Error parsing data "Service Account JSON". Missing "private_key" field in JSON file.' + ); + } + + return serviceAccount; +}; + +/** + * Check bucket exist, or create it + * @param GCS + * @param bucketName + * @returns {Promise} + */ +const checkBucket = async (GCS, bucketName) => { + let bucket = GCS.bucket(bucketName); + await bucket.exists().then((data) => { + if (!data[0]) { + throw new Error( + 'An error occurs when we try to retrieve the Bucket "' + + bucketName + + '". Check if bucket exist on Google Cloud Platform.' + ); + } + }); +}; + +/** + * Merge uploadProvider config with gcs key in custom Strapi config + * @param uploadProviderConfig + * @returns {{private_key}|{client_email}|{project_id}|any} + */ +const mergeConfigs = (providerConfig) => { + let customGcsConfig = strapi.config.gcs ? strapi.config.gcs : {}; + let customEnvGcsConfig = strapi.config.currentEnvironment.gcs + ? strapi.config.currentEnvironment.gcs + : {}; + return { ...providerConfig, ...customGcsConfig, ...customEnvGcsConfig }; +}; + +/** + * + * @type {{init(*=): {upload(*=): Promise, delete(*): Promise}}} + */ +const init = (providerConfig) => { + const config = mergeConfigs(providerConfig); + const serviceAccount = checkServiceAccount(config); + const GCS = new Storage({ + projectId: serviceAccount.project_id, + credentials: { + client_email: serviceAccount.client_email, + private_key: serviceAccount.private_key, + }, + }); + + return { + upload(file) { + return new Promise((resolve, reject) => { + const backupPath = + file.related && file.related.length > 0 && file.related[0].ref + ? `${file.related[0].ref}` + : `${file.hash}`; + const filePath = file.path ? `${file.path}/` : `${backupPath}/`; + const fileName = + slugify(path.basename(file.name + '_' + file.hash, file.ext)) + file.ext.toLowerCase(); + + checkBucket(GCS, config.bucketName) + .then(() => { + /** + * Check if the file already exist and force to remove it on Bucket + */ + GCS.bucket(config.bucketName) + .file(`${filePath}${fileName}`) + .exists() + .then((exist) => { + if (exist[0]) { + strapi.log.info('File already exist, try to remove it.'); + const fileName = `${file.url.replace( + config.baseUrl.replace('{bucket-name}', config.bucketName) + '/', + '' + )}`; + + GCS.bucket(config.bucketName) + .file(`${fileName}`) + .delete() + .then(() => { + strapi.log.debug(`File ${fileName} successfully deleted`); + }) + .catch((error) => { + if (error.code === 404) { + return strapi.log.warn( + 'Remote file was not found, you may have to delete manually.' + ); + } + }); + } + }); + }) + .then(() => { + /** + * Then save file + */ + GCS.bucket(config.bucketName) + .file(`${filePath}${fileName}`) + .save(file.buffer, { + contentType: file.mime, + public: true, + metadata: { + contentDisposition: `inline; filename="${file.name}"`, + }, + }) + .then(() => { + file.url = `${config.baseUrl.replace( + /{bucket-name}/, + config.bucketName + )}/${filePath}${fileName}`; + strapi.log.debug(`File successfully uploaded to ${file.url}`); + resolve(); + }) + .catch((error) => { + return reject(error); + }); + }); + }); + }, + delete(file) { + return new Promise((resolve, reject) => { + const fileName = `${file.url.replace( + config.baseUrl.replace('{bucket-name}', config.bucketName) + '/', + '' + )}`; + + GCS.bucket(config.bucketName) + .file(fileName) + .delete() + .then(() => { + strapi.log.debug(`File ${fileName} successfully deleted`); + }) + .catch((error) => { + if (error.code === 404) { + return strapi.log.warn('Remote file was not found, you may have to delete manually.'); + } + reject(error); + }); + resolve(); + }); + }, + }; +}; + +module.exports = { + checkServiceAccount, + checkBucket, + mergeConfigs, + init, +}; diff --git a/package.json b/package.json index 9c6b37a..b76f225 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,9 @@ }, "main": "./lib", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "lint": "eslint ." + "test": "mocha", + "lint": "eslint .", + "coverage": "nyc npm run test" }, "repository": { "type": "git", @@ -55,6 +56,9 @@ "eslint-plugin-import": "^2.20.2", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^3.1.3", + "mocha": "^7.1.2", + "mock-require": "^3.0.3", + "nyc": "^15.0.1", "prettier": "^2.0.5" } } diff --git a/test/lib/index.js b/test/lib/index.js new file mode 100644 index 0000000..bb39a99 --- /dev/null +++ b/test/lib/index.js @@ -0,0 +1,9 @@ +const { strict: assert } = require('assert'); +const provider = require('../../lib/index'); + +describe('/lib/index.js', () => { + it('must export init function', () => { + assert.ok(Object.keys(provider).includes('init')); + assert.ok(typeof provider.init === 'function'); + }); +}); diff --git a/test/lib/provider.js b/test/lib/provider.js new file mode 100644 index 0000000..4aa489b --- /dev/null +++ b/test/lib/provider.js @@ -0,0 +1,785 @@ +const { strict: assert } = require('assert'); +const mockRequire = require('mock-require'); +const { checkServiceAccount, checkBucket, mergeConfigs, init } = require('../../lib/provider'); + +describe('/lib/provider.js', () => { + describe('#checkServiceAccount', () => { + describe('when config is invalid', () => { + it('must throw error for undefined', () => { + const error = new TypeError("Cannot read property 'serviceAccount' of undefined"); + assert.throws(() => checkServiceAccount(), error); + }); + + it('must throw error "Service Account JSON" is required!', () => { + const config = {}; + const error = new Error('"Service Account JSON" is required!'); + assert.throws(() => checkServiceAccount(config), error); + }); + + it('must throw error "Service Account JSON" is required! for empty value', () => { + const config = { + serviceAccount: '', + }; + const error = new Error('"Service Account JSON" is required!'); + assert.throws(() => checkServiceAccount(config), error); + }); + + it('must throw error "Bucket name" is required!', () => { + const config = { + serviceAccount: {}, + }; + const error = new Error('"Bucket name" is required!'); + assert.throws(() => checkServiceAccount(config), error); + }); + + it('must throw error when serviceAccount does not accoplish with correct values', () => { + const config = { + serviceAccount: "I'm not a valid JSON", + bucketName: 'some-bucket', + }; + const error = new Error( + 'Error parsing data "Service Account JSON", please be sure to copy/paste the full JSON file.' + ); + assert.throws(() => checkServiceAccount(config), error); + }); + + it('must throw error when serviceAccount does not accoplish with correct values', () => { + const config = { + serviceAccount: {}, + bucketName: 'some-bucket', + }; + const error = new Error( + 'Error parsing data "Service Account JSON". Missing "project_id" field in JSON file.' + ); + assert.throws(() => checkServiceAccount(config), error); + }); + + it('must throw error when serviceAccount does not accoplish with correct values', () => { + const config = { + serviceAccount: {}, + bucketName: 'some-bucket', + }; + const error = new Error( + 'Error parsing data "Service Account JSON". Missing "project_id" field in JSON file.' + ); + assert.throws(() => checkServiceAccount(config), error); + }); + + it('must throw error when serviceAccount does not accoplish with correct values', () => { + const config = { + serviceAccount: { + project_id: '123', + }, + bucketName: 'some-bucket', + }; + const error = new Error( + 'Error parsing data "Service Account JSON". Missing "client_email" field in JSON file.' + ); + assert.throws(() => checkServiceAccount(config), error); + }); + + it('must throw error when serviceAccount does not accoplish with correct values', () => { + const config = { + serviceAccount: { + project_id: '123', + client_email: 'my@email.org', + }, + bucketName: 'some-bucket', + }; + const error = new Error( + 'Error parsing data "Service Account JSON". Missing "private_key" field in JSON file.' + ); + assert.throws(() => checkServiceAccount(config), error); + }); + + it('must throw error when serviceAccount does not accoplish with correct values', () => { + const config = { + serviceAccount: `{"project_id": "123", "client_email": "my@email.org"}`, + bucketName: 'some-bucket', + }; + const error = new Error( + 'Error parsing data "Service Account JSON". Missing "private_key" field in JSON file.' + ); + assert.throws(() => checkServiceAccount(config), error); + }); + }); + + describe('when config is valid', () => { + it('must accept configurations without errors', () => { + const config = { + serviceAccount: { + project_id: '123', + client_email: 'my@email.org', + private_key: 'a random key', + }, + bucketName: 'some-bucket', + }; + checkServiceAccount(config); + }); + + it('must accept configurations with json string', () => { + const config = { + serviceAccount: `{ + "project_id": "123", + "client_email": "my@email.org", + "private_key": "a random key" + }`, + bucketName: 'some-bucket', + }; + checkServiceAccount(config); + }); + + it('must redefine baseUrl to default value', () => { + const config = { + serviceAccount: { + project_id: '123', + client_email: 'my@email.org', + private_key: 'a random key', + }, + bucketName: 'some-bucket', + }; + checkServiceAccount(config); + assert.ok(Object.keys(config).includes('baseUrl')); + assert.equal(config.baseUrl, 'https://storage.googleapis.com/{bucket-name}'); + }); + + it('must accept baseUrl changing value', () => { + const config = { + serviceAccount: { + project_id: '123', + client_email: 'my@email.org', + private_key: 'a random key', + }, + bucketName: 'some-bucket', + baseUrl: 'http://localhost', + }; + checkServiceAccount(config); + assert.equal(config.baseUrl, 'http://localhost'); + }); + }); + }); + + describe('#checkBucket', () => { + describe('when valid bucket', () => { + it('must check if bucket exists', async () => { + let assertCount = 0; + + const gcsMock = { + bucket(bucketName) { + assertCount += 1; + assert.equal(bucketName, 'my-bucket'); + return { + async exists() { + assertCount += 1; + return [true]; + }, + }; + }, + }; + await assert.doesNotReject(checkBucket(gcsMock, 'my-bucket')); + assert.equal(assertCount, 2); + }); + }); + + describe('when bucket does not exists', async () => { + it('must throw error message', async () => { + let assertCount = 0; + + const gcsMock = { + bucket(bucketName) { + assertCount += 1; + assert.equal(bucketName, 'my-bucket'); + return { + async exists() { + assertCount += 1; + return [false]; + }, + }; + }, + }; + + const error = new Error( + 'An error occurs when we try to retrieve the Bucket "my-bucket". Check if bucket exist on Google Cloud Platform.' + ); + + await assert.rejects(checkBucket(gcsMock, 'my-bucket'), error); + + assert.equal(assertCount, 2); + }); + }); + }); + + describe('#mergeConfigs', () => { + let strapiOriginal; + + beforeEach(() => { + strapiOriginal = global.strapi; + }); + + afterEach(() => { + if (strapiOriginal === undefined) { + delete global.strapi; + } else { + global.strapi = strapiOriginal; + } + }); + + it('must apply configurations', () => { + global.strapi = { + config: { + currentEnvironment: {}, + }, + }; + const result = mergeConfigs({ foo: 'bar' }); + const expected = { foo: 'bar' }; + assert.deepEqual(result, expected); + }); + + it('must merge with strapi.config.cgs global vars', () => { + global.strapi = { + config: { + gcs: { + number: 910, + foo: 'thanos', + }, + currentEnvironment: {}, + }, + }; + const result = mergeConfigs({ foo: 'bar', key: 'value' }); + const expected = { key: 'value', foo: 'thanos', number: 910 }; + assert.deepEqual(result, expected); + }); + + it('must merge with strapi.config.currentEnvironment.gcs vars', () => { + global.strapi = { + config: { + currentEnvironment: { + gcs: { + number: 910, + foo: 'thanos', + }, + }, + }, + }; + const result = mergeConfigs({ foo: 'bar', key: 'value' }); + const expected = { key: 'value', foo: 'thanos', number: 910 }; + assert.deepEqual(result, expected); + }); + }); + + describe('#init', () => { + let strapiOriginal; + + beforeEach(() => { + strapiOriginal = global.strapi; + + global.strapi = { + config: { + currentEnvironment: {}, + }, + log: { + info() {}, + debug() {}, + }, + }; + }); + + afterEach(() => { + if (strapiOriginal === undefined) { + delete global.strapi; + } else { + global.strapi = strapiOriginal; + } + }); + + it('must return an object with upload and delete methods', () => { + const config = { + serviceAccount: { + project_id: '123', + client_email: 'my@email.org', + private_key: 'a random key', + }, + bucketName: 'any', + }; + + const result = init(config); + + assert.ok(Object.keys(result).includes('upload')); + assert.equal(typeof result.upload, 'function'); + assert.ok(Object.keys(result).includes('delete')); + assert.equal(typeof result.delete, 'function'); + }); + + it('must instanciate google cloud storage with right configurations', () => { + let assertionsCount = 0; + mockRequire('@google-cloud/storage', { + Storage: class { + constructor(...args) { + assertionsCount += 1; + assert.deepEqual(args, [ + { + credentials: { + client_email: 'my@email.org', + private_key: 'a random key', + }, + projectId: '123', + }, + ]); + } + }, + }); + const provider = mockRequire.reRequire('../../lib/provider'); + const config = { + serviceAccount: { + project_id: '123', + client_email: 'my@email.org', + private_key: 'a random key', + }, + bucketName: 'any', + }; + provider.init(config); + assert.equal(assertionsCount, 1); + mockRequire.stop('@google-cloud/storage'); + }); + + describe('when execute #upload', () => { + let assertionsCount; + + beforeEach(() => { + assertionsCount = 0; + }); + + describe('when bucket exists', () => { + const createBucketMock = ({ fileMock, expectedFileNames }) => ({ + file(fileName) { + assertionsCount += 1; + assert.equal(fileName, expectedFileNames.shift()); + return fileMock; + }, + async exists() { + assertionsCount += 1; + return [true]; + }, + }); + + describe('when file DOES NOT exists in bucket', () => { + const createFileMock = ({ saveExpectedArgs }) => ({ + async exists() { + assertionsCount += 1; + return [false]; + }, + async save(...args) { + assertionsCount += 1; + assert.deepEqual(args, saveExpectedArgs); + return [true]; + }, + }); + + it('must save file', async () => { + const fileData = { + ext: '.JPEG', + buffer: 'file buffer information', + mime: 'image/jpeg', + name: 'people coding.JPEG', + related: [ + { + ref: 'ref', + }, + ], + hash: '4l0ngH45h', + path: '/tmp/strapi', + }; + + const saveExpectedArgs = [ + 'file buffer information', + { + contentType: 'image/jpeg', + metadata: { + contentDisposition: 'inline; filename="people coding.JPEG"', + }, + public: true, + }, + ]; + + const fileMock = createFileMock({ saveExpectedArgs }); + const expectedFileNames = [ + '/tmp/strapi/people-coding.JPEG_4l0ngH45h.jpeg', + '/tmp/strapi/people-coding.JPEG_4l0ngH45h.jpeg', + ]; + const bucketMock = createBucketMock({ fileMock, expectedFileNames }); + const Storage = class { + bucket(bucketName) { + assertionsCount += 1; + assert.equal(bucketName, 'any bucket'); + return bucketMock; + } + }; + + mockRequire('@google-cloud/storage', { Storage }); + const provider = mockRequire.reRequire('../../lib/provider'); + const config = { + serviceAccount: { + project_id: '123', + client_email: 'my@email.org', + private_key: 'a random key', + }, + bucketName: 'any bucket', + }; + const providerInstance = provider.init(config); + await providerInstance.upload(fileData); + assert.equal(assertionsCount, 8); + mockRequire.stop('@google-cloud/storage'); + }); + + it('must save filename in right name', async () => { + const testData = [ + [ + 'christopher-campbell_df9a53d774/christopher-campbell_christopher-campbell_df9a53d774.jpeg', + { + name: 'christopher-campbell', + alternativeText: undefined, + caption: undefined, + hash: 'christopher-campbell_df9a53d774', + ext: '.jpeg', + mime: 'image/jpeg', + size: 823.58, + width: 5184, + height: 3456, + buffer: 'file buffer information', + }, + ], + [ + 'thumbnail_christopher-campbell_df9a53d774/undefined_thumbnail_christopher-campbell_df9a53d774.jpeg', + { + hash: 'thumbnail_christopher-campbell_df9a53d774', + ext: '.jpeg', + mime: 'image/jpeg', + width: 234, + height: 156, + size: 5.53, + buffer: 'file buffer information', + path: null, + }, + ], + [ + 'galleries/boris-smokrovic_boris-smokrovic_9fd5439b3e.jpeg', + { + name: 'boris-smokrovic', + alternativeText: undefined, + caption: undefined, + hash: 'boris-smokrovic_9fd5439b3e', + ext: '.jpeg', + mime: 'image/jpeg', + size: 897.78, + related: [{ refId: '1', ref: 'galleries', source: undefined, field: 'cover' }], + width: 4373, + height: 2915, + buffer: 'file buffer data', + }, + ], + [ + 'thumbnail_boris-smokrovic_9fd5439b3e/undefined_thumbnail_boris-smokrovic_9fd5439b3e.jpeg', + { + hash: 'thumbnail_boris-smokrovic_9fd5439b3e', + ext: '.jpeg', + mime: 'image/jpeg', + width: 234, + height: 156, + size: 8.18, + buffer: 'file buffer data', + path: null, + }, + ], + ]; + + const runTest = async ([expectedFileName, fileData]) => { + let lastFileName; + + const fileMock = { + async exists() { + return [false]; + }, + async save() { + assert.equal(lastFileName, expectedFileName); + }, + }; + + const bucketMock = { + async exists() { + return [true]; + }, + file(fileName) { + lastFileName = fileName; + return fileMock; + }, + }; + + const Storage = class { + bucket(bucketName) { + return bucketMock; + } + }; + + mockRequire('@google-cloud/storage', { Storage }); + const provider = mockRequire.reRequire('../../lib/provider'); + const config = { + serviceAccount: { + project_id: '123', + client_email: 'my@email.org', + private_key: 'a random key', + }, + bucketName: 'any bucket', + }; + const providerInstance = provider.init(config); + await providerInstance.upload(fileData); + mockRequire.stop('@google-cloud/storage'); + }; + + const promises = testData.map((data) => runTest(data)); + await Promise.all(promises); + }); + }); + + describe('when file exists in bucket', () => { + const createFileMock = ({ saveExpectedArgs }) => ({ + async exists() { + assertionsCount += 1; + return [true]; + }, + async delete() { + assertionsCount += 1; + return true; + }, + async save(...args) { + assertionsCount += 1; + assert.deepEqual(args, saveExpectedArgs); + return [true]; + }, + }); + + it('must delete file before write it', async () => { + const baseUrl = 'https://storage.googleapis.com'; + const url = `${baseUrl}/random-bucket/4l0ngH45h/people-coding.JPEG_4l0ngH45h.jpeg`; + + const fileData = { + ext: '.JPEG', + buffer: 'file buffer information', + mime: 'image/jpeg', + name: 'people coding.JPEG', + related: [], + hash: '4l0ngH45h', + url, + }; + + const saveExpectedArgs = [ + 'file buffer information', + { + contentType: 'image/jpeg', + metadata: { + contentDisposition: 'inline; filename="people coding.JPEG"', + }, + public: true, + }, + ]; + + const fileMock = createFileMock({ saveExpectedArgs }); + const expectedFileNames = [ + '4l0ngH45h/people-coding.JPEG_4l0ngH45h.jpeg', + '4l0ngH45h/people-coding.JPEG_4l0ngH45h.jpeg', + '4l0ngH45h/people-coding.JPEG_4l0ngH45h.jpeg', + ]; + const bucketMock = createBucketMock({ fileMock, expectedFileNames }); + const Storage = class { + bucket(bucketName) { + assertionsCount += 1; + assert.equal(bucketName, 'random-bucket'); + return bucketMock; + } + }; + + mockRequire('@google-cloud/storage', { Storage }); + const provider = mockRequire.reRequire('../../lib/provider'); + const config = { + serviceAccount: { + project_id: '123', + client_email: 'my@email.org', + private_key: 'a random key', + }, + bucketName: 'random-bucket', + }; + const providerInstance = provider.init(config); + await providerInstance.upload(fileData); + assert.equal(assertionsCount, 11); + mockRequire.stop('@google-cloud/storage'); + }); + }); + }); + }); + + describe('when execute #delete', () => { + let assertionsCount; + + describe('when bucket exists', () => { + const createBucketMock = ({ fileMock, expectedFileNames }) => ({ + file(fileName) { + assertionsCount += 1; + assert.equal(fileName, expectedFileNames.shift()); + return fileMock; + }, + async exists() { + assertionsCount += 1; + return [true]; + }, + }); + + describe('when file is deleted with success', () => { + const createFileMock = () => ({ + async delete() { + assertionsCount += 1; + return []; + }, + }); + + it('must log message and resolve with nothing', async () => { + global.strapi.log.debug = (...args) => { + assert.deepEqual(args, ['File o/people-working.png successfully deleted']); + assertionsCount += 1; + }; + assertionsCount = 0; + const expectedFileNames = ['o/people-working.png']; + const bucketMock = createBucketMock({ fileMock: createFileMock(), expectedFileNames }); + mockRequire('@google-cloud/storage', { + Storage: class { + bucket(bucketName) { + assertionsCount += 1; + assert.equal(bucketName, 'my-bucket'); + return bucketMock; + } + }, + }); + const provider = mockRequire.reRequire('../../lib/provider'); + const config = { + serviceAccount: { + project_id: '123', + client_email: 'my@email.org', + private_key: 'a random key', + }, + bucketName: 'my-bucket', + }; + const providerInstance = provider.init(config); + const fileData = { + url: 'https://storage.googleapis.com/my-bucket/o/people-working.png', + }; + await assert.doesNotReject(providerInstance.delete(fileData)); + // TODO: fix this. Probabily a problem with async flows + await new Promise((resolve) => setTimeout(resolve, 1)); + assert.equal(assertionsCount, 4); + mockRequire.stop('@google-cloud/storage'); + }); + }); + + describe('when file cannot be deleted', () => { + const createFileMock = ({ errorCode }) => ({ + async delete() { + assertionsCount += 1; + const error = new Error('Error deleting file'); + error.code = errorCode; + throw error; + }, + }); + + describe('when error is a 404 error', () => { + it('must log message and resolve with nothing', async () => { + global.strapi.log.warn = (...args) => { + assertionsCount += 1; + assert.deepEqual(args, [ + 'Remote file was not found, you may have to delete manually.', + ]); + }; + const errorCode = 404; + assertionsCount = 0; + const expectedFileNames = ['o/people-working.png']; + const bucketMock = createBucketMock({ + fileMock: createFileMock({ errorCode }), + expectedFileNames, + }); + mockRequire('@google-cloud/storage', { + Storage: class { + bucket(bucketName) { + assertionsCount += 1; + assert.equal(bucketName, 'my-bucket'); + return bucketMock; + } + }, + }); + const provider = mockRequire.reRequire('../../lib/provider'); + const config = { + serviceAccount: { + project_id: '123', + client_email: 'my@email.org', + private_key: 'a random key', + }, + bucketName: 'my-bucket', + }; + const providerInstance = provider.init(config); + const fileData = { + url: 'https://storage.googleapis.com/my-bucket/o/people-working.png', + }; + await assert.doesNotReject(providerInstance.delete(fileData)); + // TODO: fix this. Probabily a problem with async flows + await new Promise((resolve) => setTimeout(resolve, 1)); + assert.equal(assertionsCount, 4); + mockRequire.stop('@google-cloud/storage'); + }); + }); + + describe('when error is any other error', () => { + it('must reject with problem', async () => { + global.strapi.log.warn = (...args) => { + assertionsCount += 1; + assert.deepEqual(args, [ + 'Remote file was not found, you may have to delete manually.', + ]); + }; + const errorCode = 500; + assertionsCount = 0; + const expectedFileNames = ['o/people-working.png']; + const bucketMock = createBucketMock({ + fileMock: createFileMock({ errorCode }), + expectedFileNames, + }); + mockRequire('@google-cloud/storage', { + Storage: class { + bucket(bucketName) { + assertionsCount += 1; + assert.equal(bucketName, 'my-bucket'); + return bucketMock; + } + }, + }); + const provider = mockRequire.reRequire('../../lib/provider'); + const config = { + serviceAccount: { + project_id: '123', + client_email: 'my@email.org', + private_key: 'a random key', + }, + bucketName: 'my-bucket', + }; + const providerInstance = provider.init(config); + const fileData = { + url: 'https://storage.googleapis.com/my-bucket/o/people-working.png', + }; + // FIXME: based on code this must reject. Probabily a problem with async flows + await assert.doesNotReject(providerInstance.delete(fileData)); + // TODO: fix this. Probabily a problem with async flows + await new Promise((resolve) => setTimeout(resolve, 1)); + assert.equal(assertionsCount, 3); + mockRequire.stop('@google-cloud/storage'); + }); + }); + }); + }); + }); + }); +});