test(techdocs-common): refactor storage files mock

Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
Camila Belo
2021-08-07 21:53:09 +02:00
parent bf4ab56750
commit 759f15a14a
7 changed files with 229 additions and 295 deletions
@@ -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<boolean> => {
// 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<BlobUploadCommonResponse> {
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<BlobUploadCommonResponse> {
uploadFile(): Promise<BlobUploadCommonResponse> {
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<ContainerGetPropertiesResponse> {
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();
}
}
@@ -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<boolean> => {
// 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) {
+21 -39
View File
@@ -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<boolean> => {
// 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);
});
}),
};
}
@@ -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<string, string>;
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;
@@ -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': '<html></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(
@@ -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': '<html></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(
@@ -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`,