/plugins/techdocs-node/src/stages/publish/openStackSwift.test.ts
TypeScript | 482 lines | 401 code | 61 blank | 20 comment | 15 complexity | aec5a1cb8ac436e8f2a5d5abbfb318fb MD5 | raw file
Possible License(s): Apache-2.0, MIT, BSD-3-Clause, MPL-2.0-no-copyleft-exception
- /*
- * 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 { getVoidLogger } from '@backstage/backend-common';
- import {
- Entity,
- CompoundEntityRef,
- DEFAULT_NAMESPACE,
- } from '@backstage/catalog-model';
- import { ConfigReader } from '@backstage/config';
- import express from 'express';
- import request from 'supertest';
- import mockFs from 'mock-fs';
- import fs from 'fs-extra';
- import path from 'path';
- import { OpenStackSwiftPublish } from './openStackSwift';
- import { PublisherBase, TechDocsMetadata } from './types';
- import { storageRootDir } from '../../testUtils/StorageFilesMock';
- import { Stream, Readable } from 'stream';
- jest.mock('@trendyol-js/openstack-swift-sdk', () => {
- const {
- ContainerMetaResponse,
- DownloadResponse,
- NotFound,
- ObjectMetaResponse,
- UploadResponse,
- }: typeof import('@trendyol-js/openstack-swift-sdk') = jest.requireActual(
- '@trendyol-js/openstack-swift-sdk',
- );
- const checkFileExists = async (Key: string): Promise<boolean> => {
- // Key will always have / as file separator irrespective of OS since cloud providers expects /.
- // Normalize Key to OS specific path before checking if file exists.
- const filePath = path.join(storageRootDir, Key);
- try {
- await fs.access(filePath, fs.constants.F_OK);
- return true;
- } catch (err) {
- return false;
- }
- };
- const streamToBuffer = (stream: Stream | Readable): Promise<Buffer> => {
- return new Promise((resolve, reject) => {
- try {
- const chunks: any[] = [];
- stream.on('data', chunk => chunks.push(chunk));
- stream.on('error', reject);
- stream.on('end', () => resolve(Buffer.concat(chunks)));
- } catch (e) {
- throw new Error(`Unable to parse the response data ${e.message}`);
- }
- });
- };
- return {
- __esModule: true,
- SwiftClient: class {
- async getMetadata(_containerName: string, file: string) {
- const fileExists = await checkFileExists(file);
- if (fileExists) {
- return new ObjectMetaResponse({
- fullPath: file,
- });
- }
- return new NotFound();
- }
- async getContainerMetadata(containerName: string) {
- if (containerName === 'mock') {
- return new ContainerMetaResponse({
- size: 10,
- });
- }
- return new NotFound();
- }
- async upload(
- _containerName: string,
- destination: string,
- stream: Readable,
- ) {
- try {
- const filePath = path.join(storageRootDir, destination);
- const fileBuffer = await streamToBuffer(stream);
- await fs.writeFile(filePath, fileBuffer);
- const fileExists = await checkFileExists(destination);
- if (fileExists) {
- return new UploadResponse(filePath);
- }
- const errorMessage = `Unable to upload file(s) to OpenStack Swift.`;
- throw new Error(errorMessage);
- } catch (error) {
- const errorMessage = `Unable to upload file(s) to OpenStack Swift. ${error}`;
- throw new Error(errorMessage);
- }
- }
- async download(_containerName: string, file: string) {
- const filePath = path.join(storageRootDir, file);
- const fileExists = await checkFileExists(file);
- if (!fileExists) {
- return new NotFound();
- }
- return new DownloadResponse([], fs.createReadStream(filePath));
- }
- },
- };
- });
- const createMockEntity = (annotations = {}): Entity => {
- return {
- apiVersion: 'version',
- kind: 'TestKind',
- metadata: {
- name: 'test-component-name',
- namespace: 'test-namespace',
- annotations: {
- ...annotations,
- },
- },
- };
- };
- const createMockEntityName = (): CompoundEntityRef => ({
- kind: 'TestKind',
- name: 'test-component-name',
- namespace: 'test-namespace',
- });
- const getEntityRootDir = (entity: Entity) => {
- const {
- kind,
- metadata: { namespace, name },
- } = entity;
- return path.join(storageRootDir, namespace || DEFAULT_NAMESPACE, kind, name);
- };
- const getPosixEntityRootDir = (entity: Entity) => {
- const {
- kind,
- metadata: { namespace, name },
- } = entity;
- return path.posix.join(
- '/rootDir',
- namespace || DEFAULT_NAMESPACE,
- kind,
- name,
- );
- };
- const logger = getVoidLogger();
- let publisher: PublisherBase;
- beforeEach(() => {
- mockFs.restore();
- const mockConfig = new ConfigReader({
- techdocs: {
- publisher: {
- type: 'openStackSwift',
- openStackSwift: {
- credentials: {
- id: 'mockid',
- secret: 'verystrongsecret',
- },
- authUrl: 'mockauthurl',
- swiftUrl: 'mockSwiftUrl',
- containerName: 'mock',
- },
- },
- },
- });
- publisher = OpenStackSwiftPublish.fromConfig(mockConfig, logger);
- });
- describe('OpenStackSwiftPublish', () => {
- describe('getReadiness', () => {
- it('should validate correct config', async () => {
- expect(await publisher.getReadiness()).toEqual({
- isAvailable: true,
- });
- });
- it('should reject incorrect config', async () => {
- const mockConfig = new ConfigReader({
- techdocs: {
- publisher: {
- type: 'openStackSwift',
- openStackSwift: {
- credentials: {
- id: 'mockId',
- secret: 'mockSecret',
- },
- authUrl: 'mockauthurl',
- swiftUrl: 'mockSwiftUrl',
- containerName: 'errorBucket',
- },
- },
- },
- });
- const errorPublisher = OpenStackSwiftPublish.fromConfig(
- mockConfig,
- logger,
- );
- expect(await errorPublisher.getReadiness()).toEqual({
- isAvailable: false,
- });
- });
- });
- describe('publish', () => {
- beforeEach(() => {
- const entity = createMockEntity();
- const entityRootDir = getEntityRootDir(entity);
- mockFs({
- [entityRootDir]: {
- 'index.html': '',
- '404.html': '',
- assets: {
- 'main.css': '',
- },
- },
- });
- });
- afterEach(() => {
- mockFs.restore();
- });
- it('should publish a directory', async () => {
- const entity = createMockEntity();
- const entityRootDir = getEntityRootDir(entity);
- expect(
- await publisher.publish({
- entity,
- directory: entityRootDir,
- }),
- ).toMatchObject({
- objects: expect.arrayContaining([
- 'test-namespace/TestKind/test-component-name/404.html',
- `test-namespace/TestKind/test-component-name/index.html`,
- `test-namespace/TestKind/test-component-name/assets/main.css`,
- ]),
- });
- });
- it('should fail to publish a directory', async () => {
- const wrongPathToGeneratedDirectory = path.join(
- storageRootDir,
- 'wrong',
- 'path',
- 'to',
- 'generatedDirectory',
- );
- const entity = createMockEntity();
- await expect(
- publisher.publish({
- entity,
- directory: wrongPathToGeneratedDirectory,
- }),
- ).rejects.toThrowError();
- const fails = publisher.publish({
- entity,
- directory: wrongPathToGeneratedDirectory,
- });
- // Can not do exact error message match due to mockFs adding unexpected characters in the path when throwing the error
- // Issue reported https://github.com/tschaub/mock-fs/issues/118
- await expect(fails).rejects.toMatchObject({
- message: expect.stringContaining(
- `Unable to upload file(s) to OpenStack Swift. Error: Failed to read template directory: ENOENT, no such file or directory`,
- ),
- });
- await expect(fails).rejects.toMatchObject({
- message: expect.stringContaining(wrongPathToGeneratedDirectory),
- });
- mockFs.restore();
- });
- });
- describe('hasDocsBeenGenerated', () => {
- it('should return true if docs has been generated', async () => {
- const entity = createMockEntity();
- const entityRootDir = getEntityRootDir(entity);
- mockFs({
- [entityRootDir]: {
- 'index.html': 'file-content',
- },
- });
- expect(await publisher.hasDocsBeenGenerated(entity)).toBe(true);
- mockFs.restore();
- });
- it('should return false if docs has not been generated', async () => {
- const entity = createMockEntity();
- expect(await publisher.hasDocsBeenGenerated(entity)).toBe(false);
- });
- });
- describe('fetchTechDocsMetadata', () => {
- it('should return tech docs metadata', async () => {
- const entityNameMock = createMockEntityName();
- const entity = createMockEntity();
- const entityRootDir = getEntityRootDir(entity);
- mockFs({
- [entityRootDir]: {
- 'techdocs_metadata.json':
- '{"site_name": "backstage", "site_description": "site_content", "etag": "etag", "build_timestamp": 612741599}',
- },
- });
- const expectedMetadata: TechDocsMetadata = {
- site_name: 'backstage',
- site_description: 'site_content',
- etag: 'etag',
- build_timestamp: 612741599,
- };
- expect(
- await publisher.fetchTechDocsMetadata(entityNameMock),
- ).toStrictEqual(expectedMetadata);
- mockFs.restore();
- });
- it('should return tech docs metadata when json encoded with single quotes', async () => {
- const entityNameMock = createMockEntityName();
- const entity = createMockEntity();
- const entityRootDir = getEntityRootDir(entity);
- mockFs({
- [entityRootDir]: {
- 'techdocs_metadata.json': `{'site_name': 'backstage', 'site_description': 'site_content', 'etag': 'etag', 'build_timestamp': 612741599}`,
- },
- });
- const expectedMetadata: TechDocsMetadata = {
- site_name: 'backstage',
- site_description: 'site_content',
- etag: 'etag',
- build_timestamp: 612741599,
- };
- expect(
- await publisher.fetchTechDocsMetadata(entityNameMock),
- ).toStrictEqual(expectedMetadata);
- mockFs.restore();
- });
- it('should return an error if the techdocs_metadata.json file is not present', async () => {
- const entityNameMock = createMockEntityName();
- const entity = createMockEntity();
- const entityRootDir = getPosixEntityRootDir(entity);
- const fails = publisher.fetchTechDocsMetadata(entityNameMock);
- await expect(fails).rejects.toMatchObject({
- message: `TechDocs metadata fetch failed, The file ${path.posix.join(
- entityRootDir,
- 'techdocs_metadata.json',
- )} does not exist !`,
- });
- });
- });
- describe('docsRouter', () => {
- let app: express.Express;
- const entity = createMockEntity();
- const entityRootDir = getEntityRootDir(entity);
- beforeEach(() => {
- app = express().use(publisher.docsRouter());
- mockFs.restore();
- mockFs({
- [entityRootDir]: {
- html: {
- 'unsafe.html': '<html></html>',
- },
- img: {
- 'unsafe.svg': '<svg></svg>',
- 'with spaces.png': 'found it',
- },
- 'some folder': {
- 'also with spaces.js': 'found it too',
- },
- },
- });
- });
- afterEach(() => {
- mockFs.restore();
- });
- it('should pass expected object path to bucket', async () => {
- const {
- kind,
- metadata: { namespace, name },
- } = entity;
- // Ensures leading slash is trimmed and encoded path is decoded.
- const pngResponse = await request(app).get(
- `/${namespace}/${kind}/${name}/img/with%20spaces.png`,
- );
- expect(Buffer.from(pngResponse.body).toString('utf8')).toEqual(
- 'found it',
- );
- const jsResponse = await request(app).get(
- `/${namespace}/${kind}/${name}/some%20folder/also%20with%20spaces.js`,
- );
- expect(jsResponse.text).toEqual('found it too');
- });
- it('should pass text/plain content-type for unsafe types', async () => {
- const {
- kind,
- metadata: { namespace, name },
- } = entity;
- const htmlResponse = await request(app).get(
- `/${namespace}/${kind}/${name}/html/unsafe.html`,
- );
- expect(htmlResponse.text).toEqual('<html></html>');
- expect(htmlResponse.header).toMatchObject({
- 'content-type': 'text/plain; charset=utf-8',
- });
- const svgResponse = await request(app).get(
- `/${namespace}/${kind}/${name}/img/unsafe.svg`,
- );
- expect(svgResponse.text).toEqual('<svg></svg>');
- expect(svgResponse.header).toMatchObject({
- 'content-type': 'text/plain; charset=utf-8',
- });
- });
- it('should return 404 if file is not found', async () => {
- const {
- kind,
- metadata: { namespace, name },
- } = entity;
- const response = await request(app).get(
- `/${namespace}/${kind}/${name}/not-found.html`,
- );
- expect(response.status).toBe(404);
- expect(Buffer.from(response.text).toString('utf8')).toEqual(
- 'File Not Found',
- );
- });
- });
- });