test(techdocs-common): refactor storage files mock
Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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`,
|
||||
|
||||
Reference in New Issue
Block a user