diff --git a/packages/techdocs-common/__mocks__/@azure/storage-blob.ts b/packages/techdocs-common/__mocks__/@azure/storage-blob.ts index 73c5873832..80c60acce7 100644 --- a/packages/techdocs-common/__mocks__/@azure/storage-blob.ts +++ b/packages/techdocs-common/__mocks__/@azure/storage-blob.ts @@ -18,29 +18,8 @@ import type { 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. - */ -const checkFileExists = async (sourceFile: string): Promise => { - // sourceFile will always have / as file separator irrespective of OS since Azure expects /. - // Normalize sourceFile to OS specific path before checking if file exists. - const relativeFilePath = sourceFile.split(path.posix.sep).join(path.sep); - const filePath = path.join(rootDir, sourceFile); - - try { - await fs.access(filePath, fs.constants.F_OK); - return true; - } catch (err) { - return false; - } -}; +const storage = new (global as any).StorageFilesMock(); export class BlockBlobClient { private readonly blobName; @@ -50,9 +29,7 @@ export class BlockBlobClient { } uploadFile(source: string): Promise { - if (!fs.existsSync(source)) { - return Promise.reject(new Error(`The file ${source} does not exist`)); - } + storage.writeFile(this.blobName, source); return Promise.resolve({ _response: { request: { @@ -65,20 +42,19 @@ export class BlockBlobClient { } exists() { - return checkFileExists(this.blobName); + return storage.fileExists(this.blobName); } download() { - const filePath = path.join(rootDir, this.blobName); const emitter = new EventEmitter(); setTimeout(() => { - if (fs.existsSync(filePath)) { - emitter.emit('data', fs.readFileSync(filePath)); + if (storage.fileExists(this.blobName)) { + emitter.emit('data', storage.readFile(this.blobName)); emitter.emit('end'); } else { emitter.emit( 'error', - new Error(`The file ${filePath} does not exist !`), + new Error(`The file ${this.blobName} does not exist!`), ); } }, 0); @@ -89,7 +65,7 @@ export class BlockBlobClient { } class BlockBlobClientFailUpload extends BlockBlobClient { - uploadFile(source: string): Promise { + uploadFile(): Promise { return Promise.resolve({ _response: { request: { @@ -103,12 +79,6 @@ class BlockBlobClientFailUpload extends BlockBlobClient { } export class ContainerClient { - private readonly containerName; - - constructor(containerName: string) { - this.containerName = containerName; - } - getProperties(): Promise { return Promise.resolve({ _response: { @@ -153,18 +123,19 @@ export class BlobServiceClient { private readonly credential; constructor(url: string, credential?: StorageSharedKeyCredential) { + storage.emptyFiles(); this.url = url; this.credential = credential; } getContainerClient(containerName: string) { if (containerName === 'bad_container') { - return new ContainerClientFailGetProperties(containerName); + return new ContainerClientFailGetProperties(); } - if (this.credential.accountName === 'failupload') { - return new ContainerClientFailUpload(containerName); + if (this.credential.accountName === 'bad_account_credentials') { + return new ContainerClientFailUpload(); } - return new ContainerClient(containerName); + return new ContainerClient(); } } diff --git a/packages/techdocs-common/__mocks__/@google-cloud/storage.ts b/packages/techdocs-common/__mocks__/@google-cloud/storage.ts index 18fe62af14..efc2454d08 100644 --- a/packages/techdocs-common/__mocks__/@google-cloud/storage.ts +++ b/packages/techdocs-common/__mocks__/@google-cloud/storage.ts @@ -14,42 +14,19 @@ * limitations under the License. */ import { Readable } from 'stream'; -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. - */ -const checkFileExists = async (sourceFile: string): Promise => { - // sourceFile will always have / as file separator irrespective of OS since GCS expects /. - // Normalize sourceFile to OS specific path before checking if file exists. - const filePath = sourceFile.split(path.posix.sep).join(path.sep); - - try { - await fs.access(filePath, fs.constants.F_OK); - return true; - } catch (err) { - return false; - } -}; +const storage = new (global as any).StorageFilesMock(); class GCSFile { - private readonly localFilePath: string; + private readonly path: string; - constructor(private readonly destinationFilePath: string) { - this.destinationFilePath = destinationFilePath; - this.localFilePath = path.join(rootDir, this.destinationFilePath); + constructor(path: string) { + this.path = path; } exists() { return new Promise(async (resolve, reject) => { - if (await checkFileExists(this.localFilePath)) { + if (storage.fileExists(this.path)) { resolve([true]); } else { reject(); @@ -62,19 +39,20 @@ class GCSFile { readable._read = () => {}; process.nextTick(() => { - if (fs.existsSync(this.localFilePath)) { + if (storage.fileExists(this.path)) { if (readable.eventNames().includes('pipe')) { readable.emit('pipe'); } - readable.emit('data', fs.readFileSync(this.localFilePath)); + readable.emit('data', storage.readFile(this.path)); readable.emit('end'); } else { readable.emit( 'error', - new Error(`The file ${this.localFilePath} does not exist !`), + new Error(`The file ${this.path} does not exist!`), ); } }); + return readable; } } @@ -87,20 +65,16 @@ class Bucket { } async getMetadata() { - if (this.bucketName === 'errorBucket') { + if (this.bucketName === 'bad_bucket_name') { throw Error('Bucket does not exist'); } - return ''; } upload(source: string, { destination }) { - return new Promise(async (resolve, reject) => { - if (await checkFileExists(source)) { - resolve({ source, destination }); - } else { - reject(`Source file ${source} does not exist.`); - } + return new Promise(async resolve => { + storage.writeFile(destination, source); + resolve(null); }); } @@ -110,10 +84,8 @@ class Bucket { } export class Storage { - private readonly keyFilename; - - constructor(options: storageOptions) { - this.keyFilename = options.keyFilename; + constructor() { + storage.emptyFiles(); } bucket(bucketName) { diff --git a/packages/techdocs-common/__mocks__/aws-sdk.ts b/packages/techdocs-common/__mocks__/aws-sdk.ts index 763fad0bda..36c864bfe7 100644 --- a/packages/techdocs-common/__mocks__/aws-sdk.ts +++ b/packages/techdocs-common/__mocks__/aws-sdk.ts @@ -13,39 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type { S3 as S3Types } from 'aws-sdk'; import { EventEmitter } from 'events'; -import fs from 'fs-extra'; -import os from 'os'; -import path from 'path'; +import { ReadStream } from 'fs'; export { Credentials } from 'aws-sdk'; -const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; - -/** - * @param Key Relative path to entity root dir. Contains either / or \ as file separator - * depending upon the OS. - */ -const checkFileExists = async (Key: string): Promise => { - // Key will always have / as file separator irrespective of OS since S3 expects /. - // Normalize Key to OS specific path before checking if file exists. - const relativeFilePath = Key.split(path.posix.sep).join(path.sep); - const filePath = path.join(rootDir, Key); - - try { - await fs.access(filePath, fs.constants.F_OK); - return true; - } catch (err) { - return false; - } -}; +const storage = new (global as any).StorageFilesMock(); export class S3 { + constructor() { + storage.emptyFiles(); + } + headObject({ Key }: { Key: string }) { return { promise: async () => { - if (!(await checkFileExists(Key))) { + if (!storage.fileExists(Key)) { throw new Error('File does not exist'); } }, @@ -53,20 +36,16 @@ export class S3 { } getObject({ Key }: { Key: string }) { - const filePath = path.join(rootDir, Key); return { - promise: async () => await checkFileExists(filePath), + promise: async () => storage.fileExists(Key), createReadStream: () => { const emitter = new EventEmitter(); process.nextTick(() => { - if (fs.existsSync(filePath)) { - emitter.emit('data', Buffer.from(fs.readFileSync(filePath))); + if (storage.fileExists(Key)) { + emitter.emit('data', Buffer.from(storage.readFile(Key))); emitter.emit('end'); } else { - emitter.emit( - 'error', - new Error(`The file ${filePath} does not exist !`), - ); + emitter.emit('error', new Error(`The file ${Key} does not exist!`)); } }); return emitter; @@ -85,15 +64,18 @@ export class S3 { }; } - upload({ Key }: { Key: string }) { + upload({ Key, Body }: { Key: string; Body: ReadStream }) { return { promise: () => - new Promise(async (resolve, reject) => { - if (!(await checkFileExists(Key))) { - reject(`The file ${Key} does not exist`); - } else { - resolve(''); - } + new Promise(async resolve => { + const chunks = []; + Body.on('data', chunk => { + chunks.push(chunk); + }); + Body.once('end', () => { + storage.writeFile(Key, Buffer.concat(chunks)); + resolve(null); + }); }), }; } diff --git a/packages/techdocs-common/src/setupTests.ts b/packages/techdocs-common/src/setupTests.ts new file mode 100644 index 0000000000..996a8d35dd --- /dev/null +++ b/packages/techdocs-common/src/setupTests.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed 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 CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import os from 'os'; +import path from 'path'; +import fs from 'fs-extra'; + +const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; + +const encoding = 'utf8'; + +export class StorageFilesMock { + private files: Record; + + constructor() { + this.files = {}; + } + + public emptyFiles() { + this.files = {}; + } + + public fileExists(targetPath: string): boolean { + const filePath = path.join(rootDir, targetPath); + const posixPath = filePath.split(path.posix.sep).join(path.sep); + return this.files[posixPath] !== undefined; + } + + public readFile(targetPath: string): Buffer { + const filePath = path.join(rootDir, targetPath); + return Buffer.from(this.files[filePath] ?? '', encoding); + } + + public writeFile(targetPath: string, sourcePath: string): void; + public writeFile(targetPath: string, sourceBuffer: Buffer): void; + public writeFile(targetPath: string, source: string | Buffer): void { + const filePath = path.join(rootDir, targetPath); + if (typeof source === 'string') { + this.files[filePath] = fs.readFileSync(source).toString(encoding); + } else { + this.files[filePath] = source.toString(encoding); + } + } +} + +const _global = global as any; +_global.rootDir = rootDir; +_global.StorageFilesMock = StorageFilesMock; diff --git a/packages/techdocs-common/src/stages/publish/awsS3.test.ts b/packages/techdocs-common/src/stages/publish/awsS3.test.ts index 715786e3f9..d77d643291 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.test.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.test.ts @@ -20,13 +20,13 @@ import { ConfigReader } from '@backstage/config'; import express from 'express'; import request from 'supertest'; import mockFs from 'mock-fs'; -import os from 'os'; import path from 'path'; +import fs from 'fs-extra'; import { AwsS3Publish } from './awsS3'; // NOTE: /packages/techdocs-common/__mocks__ is being used to mock aws-sdk client library -const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; +const rootDir = (global as any).rootDir; const getEntityRootDir = (entity: Entity) => { const { @@ -46,7 +46,7 @@ const createPublisherFromConfig = ({ bucketName?: string; legacyUseCaseSensitiveTripletPaths?: boolean; } = {}) => { - const mockConfig = new ConfigReader({ + const config = new ConfigReader({ techdocs: { requestUrl: 'http://localhost:7000', publisher: { @@ -63,7 +63,7 @@ const createPublisherFromConfig = ({ }, }); - return AwsS3Publish.fromConfig(mockConfig, logger); + return AwsS3Publish.fromConfig(config, logger); }; describe('AwsS3Publish', () => { @@ -72,7 +72,6 @@ describe('AwsS3Publish', () => { kind: 'Component', metadata: { name: 'backstage', - namespace: 'default', annotations: {}, }, @@ -90,18 +89,7 @@ describe('AwsS3Publish', () => { etag: 'etag', }; - const localRootDir = getEntityRootDir(entity); - const storageRootDir = getEntityRootDir({ - ...entity, - kind: entity.kind.toLowerCase(), - }); - const storageSingleQuoteDir = getEntityRootDir({ - metadata: { - namespace: 'storage', - name: 'quote', - }, - kind: 'single', - } as Entity); + const directory = getEntityRootDir(entity); const files = { 'index.html': '', @@ -110,9 +98,6 @@ describe('AwsS3Publish', () => { assets: { 'main.css': '', }, - attachments: { - 'image.png': Buffer.from([2, 3, 5, 3, 5, 3, 5, 9, 1]), - }, html: { 'unsafe.html': '', }, @@ -127,14 +112,7 @@ describe('AwsS3Publish', () => { beforeAll(() => { mockFs({ - [localRootDir]: files, - [storageRootDir]: files, - [storageSingleQuoteDir]: { - 'techdocs_metadata.json': files['techdocs_metadata.json'].replace( - /"/g, - "'", - ), - }, + [directory]: files, }); }); @@ -163,24 +141,14 @@ describe('AwsS3Publish', () => { describe('publish', () => { it('should publish a directory', async () => { const publisher = createPublisherFromConfig(); - expect( - await publisher.publish({ - entity, - directory: localRootDir, - }), - ).toBeUndefined(); + expect(await publisher.publish({ entity, directory })).toBeUndefined(); }); it('should publish a directory as well when legacy casing is used', async () => { const publisher = createPublisherFromConfig({ legacyUseCaseSensitiveTripletPaths: true, }); - expect( - await publisher.publish({ - entity, - directory: localRootDir, - }), - ).toBeUndefined(); + expect(await publisher.publish({ entity, directory })).toBeUndefined(); }); it('should fail to publish a directory', async () => { @@ -214,6 +182,7 @@ describe('AwsS3Publish', () => { describe('hasDocsBeenGenerated', () => { it('should return true if docs has been generated', async () => { const publisher = createPublisherFromConfig(); + await publisher.publish({ entity, directory }); expect(await publisher.hasDocsBeenGenerated(entity)).toBe(true); }); @@ -221,6 +190,7 @@ describe('AwsS3Publish', () => { const publisher = createPublisherFromConfig({ legacyUseCaseSensitiveTripletPaths: true, }); + await publisher.publish({ entity, directory }); expect(await publisher.hasDocsBeenGenerated(entity)).toBe(true); }); @@ -241,6 +211,7 @@ describe('AwsS3Publish', () => { describe('fetchTechDocsMetadata', () => { it('should return tech docs metadata', async () => { const publisher = createPublisherFromConfig(); + await publisher.publish({ entity, directory }); expect(await publisher.fetchTechDocsMetadata(entityName)).toStrictEqual( techdocsMetadata, ); @@ -250,20 +221,32 @@ describe('AwsS3Publish', () => { const publisher = createPublisherFromConfig({ legacyUseCaseSensitiveTripletPaths: true, }); + await publisher.publish({ entity, directory }); expect(await publisher.fetchTechDocsMetadata(entityName)).toStrictEqual( techdocsMetadata, ); }); it('should return tech docs metadata when json encoded with single quotes', async () => { + const techdocsMetadataPath = path.join( + directory, + 'techdocs_metadata.json', + ); + const techdocsMetadataContent = files['techdocs_metadata.json']; + + fs.writeFileSync( + techdocsMetadataPath, + techdocsMetadataContent.replace(/"/g, "'"), + ); + const publisher = createPublisherFromConfig(); - expect( - await publisher.fetchTechDocsMetadata({ - namespace: 'storage', - kind: 'single', - name: 'quote', - }), - ).toStrictEqual(techdocsMetadata); + await publisher.publish({ entity, directory }); + + expect(await publisher.fetchTechDocsMetadata(entityName)).toStrictEqual( + techdocsMetadata, + ); + + fs.writeFileSync(techdocsMetadataPath, techdocsMetadataContent); }); it('should return an error if the techdocs_metadata.json file is not present', async () => { @@ -276,7 +259,6 @@ describe('AwsS3Publish', () => { }; const techDocsMetadaFilePath = path.join( - rootDir, ...Object.values(invalidEntityName), 'techdocs_metadata.json', ); @@ -284,7 +266,7 @@ describe('AwsS3Publish', () => { const fails = publisher.fetchTechDocsMetadata(invalidEntityName); await expect(fails).rejects.toMatchObject({ - message: `TechDocs metadata fetch failed, The file ${techDocsMetadaFilePath} does not exist !`, + message: `TechDocs metadata fetch failed, The file ${techDocsMetadaFilePath} does not exist!`, }); }); }); @@ -294,8 +276,9 @@ describe('AwsS3Publish', () => { let app: express.Express; - beforeEach(() => { + beforeEach(async () => { const publisher = createPublisherFromConfig(); + await publisher.publish({ entity, directory }); app = express().use(publisher.docsRouter()); }); @@ -317,6 +300,7 @@ describe('AwsS3Publish', () => { const publisher = createPublisherFromConfig({ legacyUseCaseSensitiveTripletPaths: true, }); + await publisher.publish({ entity, directory }); app = express().use(publisher.docsRouter()); // Ensures leading slash is trimmed and encoded path is decoded. const pngResponse = await request(app).get( diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts index 78cc7c0c79..dd658440d6 100644 --- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts +++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts @@ -20,13 +20,13 @@ import { ConfigReader } from '@backstage/config'; import express from 'express'; import request from 'supertest'; import mockFs from 'mock-fs'; -import os from 'os'; import path from 'path'; +import fs from 'fs-extra'; import { AzureBlobStoragePublish } from './azureBlobStorage'; // NOTE: /packages/techdocs-common/__mocks__ is being used to mock Azure client library -const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; +const rootDir = (global as any).rootDir; const getEntityRootDir = (entity: Entity) => { const { @@ -38,7 +38,6 @@ const getEntityRootDir = (entity: Entity) => { }; const logger = getVoidLogger(); -jest.spyOn(logger, 'info').mockReturnValue(logger); jest.spyOn(logger, 'error').mockReturnValue(logger); const createPublisherFromConfig = ({ @@ -50,7 +49,7 @@ const createPublisherFromConfig = ({ containerName?: string; legacyUseCaseSensitiveTripletPaths?: boolean; } = {}) => { - const mockConfig = new ConfigReader({ + const config = new ConfigReader({ techdocs: { requestUrl: 'http://localhost:7000', publisher: { @@ -67,10 +66,10 @@ const createPublisherFromConfig = ({ }, }); - return AzureBlobStoragePublish.fromConfig(mockConfig, logger); + return AzureBlobStoragePublish.fromConfig(config, logger); }; -describe('publishing with valid credentials', () => { +describe('AzureBlobStoragePublish', () => { const entity = { apiVersion: '1', kind: 'Component', @@ -93,21 +92,9 @@ describe('publishing with valid credentials', () => { etag: 'etag', }; - const localRootDir = getEntityRootDir(entity); - const storageRootDir = getEntityRootDir({ - ...entity, - kind: entity.kind.toLowerCase(), - }); - const storageSingleQuoteDir = getEntityRootDir({ - metadata: { - namespace: 'storage', - name: 'quote', - }, - kind: 'single', - } as Entity); + const directory = getEntityRootDir(entity); beforeEach(() => { - (logger.info as jest.Mock).mockClear(); (logger.error as jest.Mock).mockClear(); }); @@ -118,9 +105,6 @@ describe('publishing with valid credentials', () => { assets: { 'main.css': '', }, - attachments: { - 'image.png': Buffer.from([2, 3, 5, 3, 5, 3, 5, 9, 1]), - }, html: { 'unsafe.html': '', }, @@ -135,14 +119,7 @@ describe('publishing with valid credentials', () => { beforeAll(async () => { mockFs({ - [localRootDir]: files, - [storageRootDir]: files, - [storageSingleQuoteDir]: { - 'techdocs_metadata.json': files['techdocs_metadata.json'].replace( - /"/g, - "'", - ), - }, + [directory]: files, }); }); @@ -178,24 +155,14 @@ describe('publishing with valid credentials', () => { describe('publish', () => { it('should publish a directory', async () => { const publisher = createPublisherFromConfig(); - expect( - await publisher.publish({ - entity, - directory: localRootDir, - }), - ).toBeUndefined(); + expect(await publisher.publish({ entity, directory })).toBeUndefined(); }); it('should publish a directory as well when legacy casing is used', async () => { const publisher = createPublisherFromConfig({ legacyUseCaseSensitiveTripletPaths: true, }); - expect( - await publisher.publish({ - entity, - directory: localRootDir, - }), - ).toBeUndefined(); + expect(await publisher.publish({ entity, directory })).toBeUndefined(); }); it('should fail to publish a directory', async () => { @@ -229,15 +196,12 @@ describe('publishing with valid credentials', () => { it('reports an error when bad account credentials', async () => { const publisher = createPublisherFromConfig({ - accountName: 'failupload', + accountName: 'bad_account_credentials', }); let error; try { - await publisher.publish({ - entity, - directory: localRootDir, - }); + await publisher.publish({ entity, directory }); } catch (e) { error = e; } @@ -249,7 +213,7 @@ describe('publishing with valid credentials', () => { expect(logger.error).toHaveBeenCalledWith( expect.stringContaining( `Unable to upload file(s) to Azure Blob Storage. Error: Upload failed for ${path.join( - localRootDir, + directory, '404.html', )} with status code 500`, ), @@ -258,15 +222,17 @@ describe('publishing with valid credentials', () => { }); describe('hasDocsBeenGenerated', () => { - it('should return true if docs has been generated', async () => { + it('should check expected file', async () => { const publisher = createPublisherFromConfig(); + await publisher.publish({ entity, directory }); expect(await publisher.hasDocsBeenGenerated(entity)).toBe(true); }); - it('should return true if docs has been generated even if the legacy case is enabled', async () => { + it('should check expected file when legacy case flag is passed', async () => { const publisher = createPublisherFromConfig({ legacyUseCaseSensitiveTripletPaths: true, }); + await publisher.publish({ entity, directory }); expect(await publisher.hasDocsBeenGenerated(entity)).toBe(true); }); @@ -287,6 +253,7 @@ describe('publishing with valid credentials', () => { describe('fetchTechDocsMetadata', () => { it('should return tech docs metadata', async () => { const publisher = createPublisherFromConfig(); + await publisher.publish({ entity, directory }); expect(await publisher.fetchTechDocsMetadata(entityName)).toStrictEqual( techdocsMetadata, ); @@ -296,46 +263,53 @@ describe('publishing with valid credentials', () => { const publisher = createPublisherFromConfig({ legacyUseCaseSensitiveTripletPaths: true, }); + await publisher.publish({ entity, directory }); expect(await publisher.fetchTechDocsMetadata(entityName)).toStrictEqual( techdocsMetadata, ); }); it('should return tech docs metadata when json encoded with single quotes', async () => { + const techdocsMetadataPath = path.join( + directory, + 'techdocs_metadata.json', + ); + const techdocsMetadataContent = files['techdocs_metadata.json']; + + fs.writeFileSync( + techdocsMetadataPath, + techdocsMetadataContent.replace(/"/g, "'"), + ); + const publisher = createPublisherFromConfig(); - expect( - await publisher.fetchTechDocsMetadata({ - namespace: 'storage', - kind: 'single', - name: 'quote', - }), - ).toStrictEqual(techdocsMetadata); + await publisher.publish({ entity, directory }); + + expect(await publisher.fetchTechDocsMetadata(entityName)).toStrictEqual( + techdocsMetadata, + ); + + fs.writeFileSync(techdocsMetadataPath, techdocsMetadataContent); }); it('should return an error if the techdocs_metadata.json file is not present', async () => { const publisher = createPublisherFromConfig(); + const invalidEntityName = { namespace: 'invalid', kind: 'triplet', name: 'path', }; - let error; - try { - await publisher.fetchTechDocsMetadata(invalidEntityName); - } catch (e) { - error = e; - } - - expect(error).toEqual( - new Error( - `TechDocs metadata fetch failed, The file ${path.join( - rootDir, - ...Object.values(invalidEntityName), - 'techdocs_metadata.json', - )} does not exist !`, - ), + const techDocsMetadaFilePath = path.join( + ...Object.values(invalidEntityName), + 'techdocs_metadata.json', ); + + const fails = publisher.fetchTechDocsMetadata(invalidEntityName); + + await expect(fails).rejects.toMatchObject({ + message: `TechDocs metadata fetch failed, The file ${techDocsMetadaFilePath} does not exist!`, + }); }); }); @@ -344,8 +318,9 @@ describe('publishing with valid credentials', () => { let app: express.Express; - beforeEach(() => { + beforeEach(async () => { const publisher = createPublisherFromConfig(); + await publisher.publish({ entity, directory }); app = express().use(publisher.docsRouter()); }); @@ -367,6 +342,7 @@ describe('publishing with valid credentials', () => { const publisher = createPublisherFromConfig({ legacyUseCaseSensitiveTripletPaths: true, }); + await publisher.publish({ entity, directory }); app = express().use(publisher.docsRouter()); // Ensures leading slash is trimmed and encoded path is decoded. const pngResponse = await request(app).get( diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts b/packages/techdocs-common/src/stages/publish/googleStorage.test.ts index ce08e2f2ce..57fde767ba 100644 --- a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts +++ b/packages/techdocs-common/src/stages/publish/googleStorage.test.ts @@ -20,13 +20,13 @@ import { ConfigReader } from '@backstage/config'; import express from 'express'; import request from 'supertest'; import mockFs from 'mock-fs'; -import os from 'os'; import path from 'path'; +import fs from 'fs-extra'; import { GoogleGCSPublish } from './googleStorage'; // NOTE: /packages/techdocs-common/__mocks__ is being used to mock Google Cloud Storage client library -const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; +const rootDir = (global as any).rootDir; const getEntityRootDir = (entity: Entity) => { const { @@ -38,7 +38,6 @@ const getEntityRootDir = (entity: Entity) => { }; const logger = getVoidLogger(); -jest.spyOn(logger, 'info').mockReturnValue(logger); const createPublisherFromConfig = ({ bucketName = 'bucketName', @@ -47,7 +46,7 @@ const createPublisherFromConfig = ({ bucketName?: string; legacyUseCaseSensitiveTripletPaths?: boolean; } = {}) => { - const mockConfig = new ConfigReader({ + const config = new ConfigReader({ techdocs: { requestUrl: 'http://localhost:7000', publisher: { @@ -61,7 +60,7 @@ const createPublisherFromConfig = ({ }, }); - return GoogleGCSPublish.fromConfig(mockConfig, logger); + return GoogleGCSPublish.fromConfig(config, logger); }; describe('GoogleGCSPublish', () => { @@ -87,18 +86,7 @@ describe('GoogleGCSPublish', () => { etag: 'etag', }; - const localRootDir = getEntityRootDir(entity); - const storageRootDir = getEntityRootDir({ - ...entity, - kind: entity.kind.toLowerCase(), - }); - const storageSingleQuoteDir = getEntityRootDir({ - metadata: { - namespace: 'storage', - name: 'quote', - }, - kind: 'single', - } as Entity); + const directory = getEntityRootDir(entity); const files = { 'index.html': '', @@ -121,14 +109,7 @@ describe('GoogleGCSPublish', () => { beforeAll(() => { mockFs({ - [localRootDir]: files, - [storageRootDir]: files, - [storageSingleQuoteDir]: { - 'techdocs_metadata.json': files['techdocs_metadata.json'].replace( - /"/g, - "'", - ), - }, + [directory]: files, }); }); @@ -146,7 +127,7 @@ describe('GoogleGCSPublish', () => { it('should reject incorrect config', async () => { const publisher = createPublisherFromConfig({ - bucketName: 'errorBucket', + bucketName: 'bad_bucket_name', }); expect(await publisher.getReadiness()).toEqual({ isAvailable: false, @@ -157,24 +138,14 @@ describe('GoogleGCSPublish', () => { describe('publish', () => { it('should publish a directory', async () => { const publisher = createPublisherFromConfig(); - expect( - await publisher.publish({ - entity, - directory: localRootDir, - }), - ).toBeUndefined(); + expect(await publisher.publish({ entity, directory })).toBeUndefined(); }); it('should publish a directory as well when legacy casing is used', async () => { const publisher = createPublisherFromConfig({ legacyUseCaseSensitiveTripletPaths: true, }); - expect( - await publisher.publish({ - entity, - directory: localRootDir, - }), - ).toBeUndefined(); + expect(await publisher.publish({ entity, directory })).toBeUndefined(); }); it('should fail to publish a directory', async () => { @@ -210,6 +181,7 @@ describe('GoogleGCSPublish', () => { describe('hasDocsBeenGenerated', () => { it('should return true if docs has been generated', async () => { const publisher = createPublisherFromConfig(); + await publisher.publish({ entity, directory }); expect(await publisher.hasDocsBeenGenerated(entity)).toBe(true); }); @@ -217,6 +189,7 @@ describe('GoogleGCSPublish', () => { const publisher = createPublisherFromConfig({ legacyUseCaseSensitiveTripletPaths: true, }); + await publisher.publish({ entity, directory }); expect(await publisher.hasDocsBeenGenerated(entity)).toBe(true); }); @@ -237,6 +210,7 @@ describe('GoogleGCSPublish', () => { describe('fetchTechDocsMetadata', () => { it('should return tech docs metadata', async () => { const publisher = createPublisherFromConfig(); + await publisher.publish({ entity, directory }); expect(await publisher.fetchTechDocsMetadata(entityName)).toStrictEqual( techdocsMetadata, ); @@ -246,20 +220,32 @@ describe('GoogleGCSPublish', () => { const publisher = createPublisherFromConfig({ legacyUseCaseSensitiveTripletPaths: true, }); + await publisher.publish({ entity, directory }); expect(await publisher.fetchTechDocsMetadata(entityName)).toStrictEqual( techdocsMetadata, ); }); it('should return tech docs metadata when json encoded with single quotes', async () => { + const techdocsMetadataPath = path.join( + directory, + 'techdocs_metadata.json', + ); + const techdocsMetadataContent = files['techdocs_metadata.json']; + + fs.writeFileSync( + techdocsMetadataPath, + techdocsMetadataContent.replace(/"/g, "'"), + ); + const publisher = createPublisherFromConfig(); - expect( - await publisher.fetchTechDocsMetadata({ - namespace: 'storage', - kind: 'single', - name: 'quote', - }), - ).toStrictEqual(techdocsMetadata); + await publisher.publish({ entity, directory }); + + expect(await publisher.fetchTechDocsMetadata(entityName)).toStrictEqual( + techdocsMetadata, + ); + + fs.writeFileSync(techdocsMetadataPath, techdocsMetadataContent); }); it('should return an error if the techdocs_metadata.json file is not present', async () => { @@ -272,7 +258,6 @@ describe('GoogleGCSPublish', () => { }; const techDocsMetadaFilePath = path.join( - rootDir, ...Object.values(invalidEntityName), 'techdocs_metadata.json', ); @@ -280,7 +265,7 @@ describe('GoogleGCSPublish', () => { const fails = publisher.fetchTechDocsMetadata(invalidEntityName); await expect(fails).rejects.toMatchObject({ - message: `The file ${techDocsMetadaFilePath} does not exist !`, + message: `The file ${techDocsMetadaFilePath} does not exist!`, }); }); }); @@ -288,10 +273,11 @@ describe('GoogleGCSPublish', () => { describe('docsRouter', () => { const entityTripletPath = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`; - let app: express.Express; + let app: Express.Application; - beforeEach(() => { + beforeEach(async () => { const publisher = createPublisherFromConfig(); + await publisher.publish({ entity, directory }); app = express().use(publisher.docsRouter()); }); @@ -313,7 +299,9 @@ describe('GoogleGCSPublish', () => { const publisher = createPublisherFromConfig({ legacyUseCaseSensitiveTripletPaths: true, }); + await publisher.publish({ entity, directory }); app = express().use(publisher.docsRouter()); + // Ensures leading slash is trimmed and encoded path is decoded. const pngResponse = await request(app).get( `/${entityTripletPath}/img/with%20spaces.png`,