From 15203089585f989f25b760638f818527918ce2fa Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Thu, 18 Feb 2021 23:19:33 +0100 Subject: [PATCH] TechDocs/GCS: Improve test coverage by testing other two methods --- .../__mocks__/@google-cloud/storage.ts | 42 ++++ packages/techdocs-common/__mocks__/aws-sdk.ts | 2 +- .../src/stages/publish/googleStorage.test.ts | 222 +++++++++++++----- .../src/stages/publish/googleStorage.ts | 6 +- 4 files changed, 205 insertions(+), 67 deletions(-) diff --git a/packages/techdocs-common/__mocks__/@google-cloud/storage.ts b/packages/techdocs-common/__mocks__/@google-cloud/storage.ts index 648aceadb4..264852d31c 100644 --- a/packages/techdocs-common/__mocks__/@google-cloud/storage.ts +++ b/packages/techdocs-common/__mocks__/@google-cloud/storage.ts @@ -13,13 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { EventEmitter } from 'events'; import fs from 'fs-extra'; +import os from 'os'; import path from 'path'; type storageOptions = { keyFilename?: string; }; +const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; /** * @param sourceFile Absolute path. Contains either / or \ as file separator depending upon the OS. */ @@ -36,6 +39,41 @@ const checkFileExists = async (sourceFile: string): Promise => { } }; +class GCSFile { + private readonly localFilePath: string; + + constructor(private readonly destinationFilePath: string) { + this.destinationFilePath = destinationFilePath; + this.localFilePath = path.join(rootDir, this.destinationFilePath); + } + + exists() { + return new Promise(async (resolve, reject) => { + if (await checkFileExists(this.localFilePath)) { + resolve([true]); + } else { + reject(); + } + }); + } + + createReadStream() { + const emitter = new EventEmitter(); + process.nextTick(() => { + if (fs.existsSync(this.localFilePath)) { + emitter.emit('data', Buffer.from(fs.readFileSync(this.localFilePath))); + emitter.emit('end'); + } else { + emitter.emit( + 'error', + new Error(`The file ${this.localFilePath} does not exist !`), + ); + } + }); + return emitter; + } +} + class Bucket { private readonly bucketName; @@ -58,6 +96,10 @@ class Bucket { } }); } + + file(destinationFilePath: string) { + return new GCSFile(destinationFilePath); + } } export class Storage { diff --git a/packages/techdocs-common/__mocks__/aws-sdk.ts b/packages/techdocs-common/__mocks__/aws-sdk.ts index 0a50b0e792..4717feda9b 100644 --- a/packages/techdocs-common/__mocks__/aws-sdk.ts +++ b/packages/techdocs-common/__mocks__/aws-sdk.ts @@ -65,13 +65,13 @@ export class S3 { process.nextTick(() => { if (fs.existsSync(filePath)) { emitter.emit('data', Buffer.from(fs.readFileSync(filePath))); + emitter.emit('end'); } else { emitter.emit( 'error', new Error(`The file ${filePath} does not exist !`), ); } - emitter.emit('end'); }); return emitter; }, diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts b/packages/techdocs-common/src/stages/publish/googleStorage.test.ts index 1069b7b9a4..a8df9ccbc1 100644 --- a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts +++ b/packages/techdocs-common/src/stages/publish/googleStorage.test.ts @@ -14,13 +14,13 @@ * limitations under the License. */ import { getVoidLogger } from '@backstage/backend-common'; -import { Entity } from '@backstage/catalog-model'; +import { Entity, EntityName } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import mockFs from 'mock-fs'; import os from 'os'; import path from 'path'; import { GoogleGCSPublish } from './googleStorage'; -import { PublisherBase } from './types'; +import { PublisherBase, TechDocsMetadata } from './types'; // NOTE: /packages/techdocs-common/__mocks__ is being used to mock Google Cloud Storage client library @@ -38,6 +38,12 @@ const createMockEntity = (annotations = {}): Entity => { }; }; +const createMockEntityName = (): EntityName => ({ + kind: 'TestKind', + name: 'test-component-name', + namespace: 'test-namespace', +}); + const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; const getEntityRootDir = (entity: Entity) => { @@ -73,74 +79,164 @@ beforeEach(async () => { }); describe('GoogleGCSPublish', () => { - beforeEach(() => { - const entity = createMockEntity(); - const entityRootDir = getEntityRootDir(entity); + describe('publish', () => { + beforeEach(() => { + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); - mockFs({ - [entityRootDir]: { - 'index.html': '', - '404.html': '', - assets: { - 'main.css': '', + mockFs({ + [entityRootDir]: { + 'index.html': '', + '404.html': '', + assets: { + 'main.css': '', + }, }, - }, + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('should publish a directory', async () => { + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + expect( + await publisher.publish({ + entity, + directory: entityRootDir, + }), + ).toBeUndefined(); + mockFs.restore(); + }); + + it('should fail to publish a directory', async () => { + const wrongPathToGeneratedDirectory = path.join( + rootDir, + 'wrong', + 'path', + 'to', + 'generatedDirectory', + ); + + const entity = createMockEntity(); + + await expect( + publisher.publish({ + entity, + directory: wrongPathToGeneratedDirectory, + }), + ).rejects.toThrowError(); + + await publisher + .publish({ + entity, + directory: wrongPathToGeneratedDirectory, + }) + .catch(error => { + expect(error.message).toEqual( + // Can not do exact error message match due to mockFs adding unexpected characters in the path when throwing the error + // Issue reported https://github.com/tschaub/mock-fs/issues/118 + expect.stringContaining( + `Unable to upload file(s) to Google Cloud Storage. Error: Failed to read template directory: ENOENT, no such file or directory`, + ), + ); + expect(error.message).toEqual( + expect.stringContaining(wrongPathToGeneratedDirectory), + ); + }); + + mockFs.restore(); }); }); - afterEach(() => { - mockFs.restore(); - }); + describe('hasDocsBeenGenerated', () => { + it('should return true if docs has been generated', async () => { + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); - it('should publish a directory', async () => { - const entity = createMockEntity(); - const entityRootDir = getEntityRootDir(entity); - - expect( - await publisher.publish({ - entity, - directory: entityRootDir, - }), - ).toBeUndefined(); - mockFs.restore(); - }); - - it('should fail to publish a directory', async () => { - const wrongPathToGeneratedDirectory = path.join( - rootDir, - 'wrong', - 'path', - 'to', - 'generatedDirectory', - ); - - const entity = createMockEntity(); - - await expect( - publisher.publish({ - entity, - directory: wrongPathToGeneratedDirectory, - }), - ).rejects.toThrowError(); - - await publisher - .publish({ - entity, - directory: wrongPathToGeneratedDirectory, - }) - .catch(error => { - expect(error.message).toEqual( - // Can not do exact error message match due to mockFs adding unexpected characters in the path when throwing the error - // Issue reported https://github.com/tschaub/mock-fs/issues/118 - expect.stringContaining( - `Unable to upload file(s) to Google Cloud Storage. Error: Failed to read template directory: ENOENT, no such file or directory`, - ), - ); - expect(error.message).toEqual( - expect.stringContaining(wrongPathToGeneratedDirectory), - ); + mockFs({ + [entityRootDir]: { + 'index.html': 'file-content', + }, }); - mockFs.restore(); + expect(await publisher.hasDocsBeenGenerated(entity)).toBe(true); + mockFs.restore(); + }); + + it('should return false if docs has not been generated', async () => { + const entity = createMockEntity(); + + expect(await publisher.hasDocsBeenGenerated(entity)).toBe(false); + }); + }); + + describe('fetchTechDocsMetadata', () => { + beforeEach(() => { + mockFs.restore(); + }); + + it('should return tech docs metadata', async () => { + const entityNameMock = createMockEntityName(); + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + mockFs({ + [entityRootDir]: { + 'techdocs_metadata.json': + '{"site_name": "backstage", "site_description": "site_content"}', + }, + }); + + const expectedMetadata: TechDocsMetadata = { + site_name: 'backstage', + site_description: 'site_content', + }; + expect( + await publisher.fetchTechDocsMetadata(entityNameMock), + ).toStrictEqual(expectedMetadata); + }); + + it('should return tech docs metadata when json encoded with single quotes', async () => { + const entityNameMock = createMockEntityName(); + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + mockFs({ + [entityRootDir]: { + 'techdocs_metadata.json': + "{'site_name': 'backstage', 'site_description': 'site_content'}", + }, + }); + + const expectedMetadata: TechDocsMetadata = { + site_name: 'backstage', + site_description: 'site_content', + }; + expect( + await publisher.fetchTechDocsMetadata(entityNameMock), + ).toStrictEqual(expectedMetadata); + mockFs.restore(); + }); + + it('should return an error if the techdocs_metadata.json file is not present', async () => { + const entityNameMock = createMockEntityName(); + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + await publisher + .fetchTechDocsMetadata(entityNameMock) + .catch(errorMessage => + expect(errorMessage).toEqual( + `The file ${path.join( + entityRootDir, + 'techdocs_metadata.json', + )} does not exist !`, + ), + ); + }); }); }); diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.ts b/packages/techdocs-common/src/stages/publish/googleStorage.ts index e07defb237..12ecf45b2d 100644 --- a/packages/techdocs-common/src/stages/publish/googleStorage.ts +++ b/packages/techdocs-common/src/stages/publish/googleStorage.ts @@ -160,9 +160,9 @@ export class GoogleGCSPublish implements PublisherBase { fileStreamChunks.push(chunk); }) .on('end', () => { - const techdocsMetadataJson = Buffer.concat( - fileStreamChunks, - ).toString(); + const techdocsMetadataJson = Buffer.concat(fileStreamChunks).toString( + 'utf-8', + ); resolve(JSON5.parse(techdocsMetadataJson)); }); });