diff --git a/packages/techdocs-common/__mocks__/@azure/storage-blob.ts b/packages/techdocs-common/__mocks__/@azure/storage-blob.ts index 4fc1e4c374..0d480a7fa9 100644 --- a/packages/techdocs-common/__mocks__/@azure/storage-blob.ts +++ b/packages/techdocs-common/__mocks__/@azure/storage-blob.ts @@ -17,11 +17,13 @@ import type { BlobUploadCommonResponse, ContainerGetPropertiesResponse, } from '@azure/storage-blob'; +import { EventEmitter } from 'events'; import fs from 'fs-extra'; import os from 'os'; import path from 'path'; const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; + /** * @param sourceFile Relative path to entity root dir. Contains either / or \ as file separator * depending upon the OS. @@ -48,6 +50,9 @@ export class BlockBlobClient { } uploadFile(source: string): Promise { + if (!fs.existsSync(source)) { + return Promise.reject(new Error(`The file ${source} does not exist`)); + } return Promise.resolve({ _response: { request: { @@ -62,6 +67,25 @@ export class BlockBlobClient { exists() { return checkFileExists(this.blobName); } + + download() { + const filePath = path.join(rootDir, this.blobName); + const emitter = new EventEmitter(); + 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 !`), + ); + } + }); + return Promise.resolve({ + readableStreamBody: emitter, + }); + } } class BlockBlobClientFailUpload extends BlockBlobClient { diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts index cf1ea60592..44262768a5 100644 --- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts +++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts @@ -14,13 +14,17 @@ * limitations under the License. */ import { getVoidLogger } from '@backstage/backend-common'; -import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { + Entity, + EntityName, + ENTITY_DEFAULT_NAMESPACE, +} from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import mockFs from 'mock-fs'; import os from 'os'; import path from 'path'; import { AzureBlobStoragePublish } from './azureBlobStorage'; -import { PublisherBase } from './types'; +import { PublisherBase, TechDocsMetadata } from './types'; // NOTE: /packages/techdocs-common/__mocks__ is being used to mock Azure client library @@ -38,7 +42,14 @@ const createMockEntity = (annotations = {}) => { }; }; +const createMockEntityName = (): EntityName => ({ + kind: 'TestKind', + name: 'test-component-name', + namespace: 'test-namespace', +}); + const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; + const getEntityRootDir = (entity: Entity) => { const { kind, @@ -163,6 +174,72 @@ describe('publishing with valid credentials', () => { expect(await publisher.hasDocsBeenGenerated(entity)).toBe(false); }); }); + + describe('fetchTechDocsMetadata', () => { + 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); + mockFs.restore(); + }); + + 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); + + let error; + try { + await publisher.fetchTechDocsMetadata(entityNameMock); + } catch (e) { + error = e; + } + + expect(error).toEqual( + new Error( + `TechDocs metadata fetch failed, The file ${path.join( + entityRootDir, + 'techdocs_metadata.json', + )} does not exist !`, + ), + ); + }); + }); }); describe('error reporting', () => { @@ -255,4 +332,63 @@ describe('error reporting', () => { mockFs.restore(); }); + + describe('fetchTechDocsMetadata', () => { + 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); + mockFs.restore(); + }); + + 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(); + + let error; + try { + await publisher.fetchTechDocsMetadata(entityNameMock); + } catch (e) { + error = e; + } + + expect(error.message).toEqual( + expect.stringContaining('TechDocs metadata fetch'), + ); + }); + }); }); diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts index 2136ee2bc2..155d14fbb5 100644 --- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts +++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts @@ -201,10 +201,7 @@ export class AzureBlobStoragePublish implements PublisherBase { return; } body - .on('error', e => { - this.logger.error(e.message); - reject(e.message); - }) + .on('error', reject) .on('data', chunk => { fileStreamChunks.push(chunk); }) @@ -220,16 +217,21 @@ export class AzureBlobStoragePublish implements PublisherBase { ): Promise { const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`; try { - return await new Promise(resolve => { - const download = this.download( - this.containerName, - `${entityRootDir}/techdocs_metadata.json`, + const techdocsMetadataJson = await this.download( + this.containerName, + `${entityRootDir}/techdocs_metadata.json`, + ); + if (!techdocsMetadataJson) { + throw new Error( + `Unable to parse the techdocs metadata file ${entityRootDir}/techdocs_metadata.json.`, ); - resolve(JSON5.parse(download.toString())); - }); + } + const techdocsMetadata = JSON5.parse( + techdocsMetadataJson.toString('utf-8'), + ); + return techdocsMetadata; } catch (e) { - this.logger.error(e.message); - throw e; + throw new Error(`TechDocs metadata fetch failed, ${e.message}`); } }