diff --git a/packages/techdocs-common/src/stages/publish/awsS3.test.ts b/packages/techdocs-common/src/stages/publish/awsS3.test.ts
index d25b54c529..715786e3f9 100644
--- a/packages/techdocs-common/src/stages/publish/awsS3.test.ts
+++ b/packages/techdocs-common/src/stages/publish/awsS3.test.ts
@@ -39,7 +39,13 @@ const getEntityRootDir = (entity: Entity) => {
const logger = getVoidLogger();
-const createPublisherFromConfig = (bucketName: string = 'bucketName') => {
+const createPublisherFromConfig = ({
+ bucketName = 'bucketName',
+ legacyUseCaseSensitiveTripletPaths = false,
+}: {
+ bucketName?: string;
+ legacyUseCaseSensitiveTripletPaths?: boolean;
+} = {}) => {
const mockConfig = new ConfigReader({
techdocs: {
requestUrl: 'http://localhost:7000',
@@ -53,6 +59,7 @@ const createPublisherFromConfig = (bucketName: string = 'bucketName') => {
bucketName,
},
},
+ legacyUseCaseSensitiveTripletPaths,
},
});
@@ -96,42 +103,37 @@ describe('AwsS3Publish', () => {
kind: 'single',
} as Entity);
+ const files = {
+ 'index.html': '',
+ '404.html': '',
+ 'techdocs_metadata.json': JSON.stringify(techdocsMetadata),
+ assets: {
+ 'main.css': '',
+ },
+ attachments: {
+ 'image.png': Buffer.from([2, 3, 5, 3, 5, 3, 5, 9, 1]),
+ },
+ html: {
+ 'unsafe.html': '',
+ },
+ img: {
+ 'with spaces.png': 'found it',
+ 'unsafe.svg': '',
+ },
+ 'some folder': {
+ 'also with spaces.js': 'found it too',
+ },
+ };
+
beforeAll(() => {
mockFs({
- [localRootDir]: {
- 'index.html': '',
- '404.html': '',
- assets: {
- 'main.css': '',
- },
- attachments: {
- 'image.png': Buffer.from([2, 3, 5, 3, 5, 3, 5, 9, 1]),
- },
- 'techdocs_metadata.json': JSON.stringify(techdocsMetadata),
- },
- [storageRootDir]: {
- 'index.html': '',
- '404.html': '',
- 'techdocs_metadata.json': JSON.stringify(techdocsMetadata),
- assets: {
- 'main.css': '',
- },
- attachments: {
- 'image.png': Buffer.from([2, 3, 5, 3, 5, 3, 5, 9, 1]),
- },
- html: {
- 'unsafe.html': '',
- },
- img: {
- 'with spaces.png': 'found it',
- 'unsafe.svg': '',
- },
- 'some folder': {
- 'also with spaces.js': 'found it too',
- },
- },
+ [localRootDir]: files,
+ [storageRootDir]: files,
[storageSingleQuoteDir]: {
- 'techdocs_metadata.json': `{'site_name': 'backstage', 'site_description': 'site_content', 'etag': 'etag'}`,
+ 'techdocs_metadata.json': files['techdocs_metadata.json'].replace(
+ /"/g,
+ "'",
+ ),
},
});
});
@@ -149,7 +151,9 @@ describe('AwsS3Publish', () => {
});
it('should reject incorrect config', async () => {
- const publisher = createPublisherFromConfig('errorBucket');
+ const publisher = createPublisherFromConfig({
+ bucketName: 'errorBucket',
+ });
expect(await publisher.getReadiness()).toEqual({
isAvailable: false,
});
@@ -167,6 +171,18 @@ describe('AwsS3Publish', () => {
).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();
+ });
+
it('should fail to publish a directory', async () => {
const wrongPathToGeneratedDirectory = path.join(
rootDir,
@@ -201,6 +217,13 @@ describe('AwsS3Publish', () => {
expect(await publisher.hasDocsBeenGenerated(entity)).toBe(true);
});
+ it('should return true if docs has been generated even if the legacy case is enabled', async () => {
+ const publisher = createPublisherFromConfig({
+ legacyUseCaseSensitiveTripletPaths: true,
+ });
+ expect(await publisher.hasDocsBeenGenerated(entity)).toBe(true);
+ });
+
it('should return false if docs has not been generated', async () => {
const publisher = createPublisherFromConfig();
expect(
@@ -223,6 +246,15 @@ describe('AwsS3Publish', () => {
);
});
+ it('should return tech docs metadata even if the legacy case is enabled', async () => {
+ const publisher = createPublisherFromConfig({
+ legacyUseCaseSensitiveTripletPaths: true,
+ });
+ expect(await publisher.fetchTechDocsMetadata(entityName)).toStrictEqual(
+ techdocsMetadata,
+ );
+ });
+
it('should return tech docs metadata when json encoded with single quotes', async () => {
const publisher = createPublisherFromConfig();
expect(
@@ -281,6 +313,24 @@ describe('AwsS3Publish', () => {
expect(jsResponse.text).toEqual('found it too');
});
+ it('should pass expected object path to bucket even if the legacy case is enabled', async () => {
+ const publisher = createPublisherFromConfig({
+ legacyUseCaseSensitiveTripletPaths: true,
+ });
+ 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`,
+ );
+ expect(Buffer.from(pngResponse.body).toString('utf8')).toEqual(
+ 'found it',
+ );
+ const jsResponse = await request(app).get(
+ `/${entityTripletPath}/some%20folder/also%20with%20spaces.js`,
+ );
+ expect(jsResponse.text).toEqual('found it too');
+ });
+
it('should pass text/plain content-type for html', async () => {
const htmlResponse = await request(app).get(
`/${entityTripletPath}/html/unsafe.html`,
diff --git a/packages/techdocs-common/src/stages/publish/awsS3.ts b/packages/techdocs-common/src/stages/publish/awsS3.ts
index 26a896cbac..f0e2d9cb6e 100644
--- a/packages/techdocs-common/src/stages/publish/awsS3.ts
+++ b/packages/techdocs-common/src/stages/publish/awsS3.ts
@@ -102,7 +102,17 @@ export class AwsS3Publish implements PublisherBase {
...(s3ForcePathStyle && { s3ForcePathStyle }),
});
- return new AwsS3Publish(storageClient, bucketName, logger);
+ const legacyPathCasing =
+ config.getOptionalBoolean(
+ 'techdocs.legacyUseCaseSensitiveTripletPaths',
+ ) || false;
+
+ return new AwsS3Publish(
+ storageClient,
+ bucketName,
+ legacyPathCasing,
+ logger,
+ );
}
private static buildCredentials(
@@ -139,10 +149,12 @@ export class AwsS3Publish implements PublisherBase {
constructor(
private readonly storageClient: aws.S3,
private readonly bucketName: string,
+ private readonly legacyPathCasing: boolean,
private readonly logger: Logger,
) {
this.storageClient = storageClient;
this.bucketName = bucketName;
+ this.legacyPathCasing = legacyPathCasing;
this.logger = logger;
}
@@ -204,9 +216,11 @@ export class AwsS3Publish implements PublisherBase {
const entityRootDir = `${
entity.metadata?.namespace ?? ENTITY_DEFAULT_NAMESPACE
}/${entity.kind}/${entity.metadata.name}`;
- const destination = lowerCaseEntityTripletInStoragePath(
- `${entityRootDir}/${relativeFilePathPosix}`,
- ); // S3 Bucket file relative path
+
+ const relativeFilePathTriplet = `${entityRootDir}/${relativeFilePathPosix}`;
+ const destination = this.legacyPathCasing
+ ? relativeFilePathTriplet
+ : lowerCaseEntityTripletInStoragePath(relativeFilePathTriplet); // S3 Bucket file relative path
// Rate limit the concurrent execution of file uploads to batches of 10 (per publish)
const uploadFile = limiter(() => {
@@ -239,9 +253,10 @@ export class AwsS3Publish implements PublisherBase {
): Promise {
try {
return await new Promise(async (resolve, reject) => {
- const entityRootDir = lowerCaseEntityTriplet(
- `${entityName.namespace}/${entityName.kind}/${entityName.name}`,
- );
+ const entityTriplet = `${entityName.namespace}/${entityName.kind}/${entityName.name}`;
+ const entityRootDir = this.legacyPathCasing
+ ? entityTriplet
+ : lowerCaseEntityTriplet(entityTriplet);
const stream = this.storageClient
.getObject({
@@ -282,7 +297,9 @@ export class AwsS3Publish implements PublisherBase {
const decodedUri = decodeURI(req.path.replace(/^\//, ''));
// filePath example - /default/component/documented-component/index.html
- const filePath = lowerCaseEntityTripletInStoragePath(decodedUri);
+ const filePath = this.legacyPathCasing
+ ? decodedUri
+ : lowerCaseEntityTripletInStoragePath(decodedUri);
// Files with different extensions (CSS, HTML) need to be served with different headers
const fileExtension = path.extname(filePath);
@@ -313,9 +330,11 @@ export class AwsS3Publish implements PublisherBase {
*/
async hasDocsBeenGenerated(entity: Entity): Promise {
try {
- const entityRootDir = lowerCaseEntityTriplet(
- `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`,
- );
+ const entityTriplet = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`;
+ const entityRootDir = this.legacyPathCasing
+ ? entityTriplet
+ : lowerCaseEntityTriplet(entityTriplet);
+
await this.storageClient
.headObject({
Bucket: this.bucketName,
diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts
index 60a70a2e0d..78cc7c0c79 100644
--- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts
+++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts
@@ -44,7 +44,12 @@ jest.spyOn(logger, 'error').mockReturnValue(logger);
const createPublisherFromConfig = ({
accountName = 'accountName',
containerName = 'containerName',
-}: undefined | { accountName?: string; containerName?: string } = {}) => {
+ legacyUseCaseSensitiveTripletPaths = false,
+}: {
+ accountName?: string;
+ containerName?: string;
+ legacyUseCaseSensitiveTripletPaths?: boolean;
+} = {}) => {
const mockConfig = new ConfigReader({
techdocs: {
requestUrl: 'http://localhost:7000',
@@ -58,6 +63,7 @@ const createPublisherFromConfig = ({
containerName,
},
},
+ legacyUseCaseSensitiveTripletPaths,
},
});
@@ -105,32 +111,37 @@ describe('publishing with valid credentials', () => {
(logger.error as jest.Mock).mockClear();
});
+ const files = {
+ 'index.html': '',
+ '404.html': '',
+ 'techdocs_metadata.json': JSON.stringify(techdocsMetadata),
+ assets: {
+ 'main.css': '',
+ },
+ attachments: {
+ 'image.png': Buffer.from([2, 3, 5, 3, 5, 3, 5, 9, 1]),
+ },
+ html: {
+ 'unsafe.html': '',
+ },
+ img: {
+ 'with spaces.png': 'found it',
+ 'unsafe.svg': '',
+ },
+ 'some folder': {
+ 'also with spaces.js': 'found it too',
+ },
+ };
+
beforeAll(async () => {
mockFs({
- [localRootDir]: {
- 'index.html': 'file-content',
- '404.html': '',
- assets: {
- 'main.css': '',
- },
- 'techdocs_metadata.json': JSON.stringify(techdocsMetadata),
- },
- [storageRootDir]: {
- 'index.html': '',
- 'techdocs_metadata.json': JSON.stringify(techdocsMetadata),
- html: {
- 'unsafe.html': '',
- },
- img: {
- 'with spaces.png': 'found it',
- 'unsafe.svg': '',
- },
- 'some folder': {
- 'also with spaces.js': 'found it too',
- },
- },
+ [localRootDir]: files,
+ [storageRootDir]: files,
[storageSingleQuoteDir]: {
- 'techdocs_metadata.json': `{'site_name': 'backstage', 'site_description': 'site_content', 'etag': 'etag'}`,
+ 'techdocs_metadata.json': files['techdocs_metadata.json'].replace(
+ /"/g,
+ "'",
+ ),
},
});
});
@@ -175,6 +186,18 @@ describe('publishing with valid credentials', () => {
).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();
+ });
+
it('should fail to publish a directory', async () => {
const wrongPathToGeneratedDirectory = path.join(
rootDir,
@@ -240,6 +263,13 @@ describe('publishing with valid credentials', () => {
expect(await publisher.hasDocsBeenGenerated(entity)).toBe(true);
});
+ it('should return true if docs has been generated even if the legacy case is enabled', async () => {
+ const publisher = createPublisherFromConfig({
+ legacyUseCaseSensitiveTripletPaths: true,
+ });
+ expect(await publisher.hasDocsBeenGenerated(entity)).toBe(true);
+ });
+
it('should return false if docs has not been generated', async () => {
const publisher = createPublisherFromConfig();
expect(
@@ -262,6 +292,15 @@ describe('publishing with valid credentials', () => {
);
});
+ it('should return tech docs metadata even if the legacy case is enabled', async () => {
+ const publisher = createPublisherFromConfig({
+ legacyUseCaseSensitiveTripletPaths: true,
+ });
+ expect(await publisher.fetchTechDocsMetadata(entityName)).toStrictEqual(
+ techdocsMetadata,
+ );
+ });
+
it('should return tech docs metadata when json encoded with single quotes', async () => {
const publisher = createPublisherFromConfig();
expect(
@@ -324,6 +363,24 @@ describe('publishing with valid credentials', () => {
expect(jsResponse.text).toEqual('found it too');
});
+ it('should pass expected object path to bucket even if the legacy case is enabled', async () => {
+ const publisher = createPublisherFromConfig({
+ legacyUseCaseSensitiveTripletPaths: true,
+ });
+ 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`,
+ );
+ expect(Buffer.from(pngResponse.body).toString('utf8')).toEqual(
+ 'found it',
+ );
+ const jsResponse = await request(app).get(
+ `/${entityTripletPath}/some%20folder/also%20with%20spaces.js`,
+ );
+ expect(jsResponse.text).toEqual('found it too');
+ });
+
it('should pass text/plain content-type for html', async () => {
const htmlResponse = await request(app).get(
`/${entityTripletPath}/html/unsafe.html`,
diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts
index 76f91cff66..cb8367ed9d 100644
--- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts
+++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts
@@ -89,16 +89,28 @@ export class AzureBlobStoragePublish implements PublisherBase {
credential,
);
- return new AzureBlobStoragePublish(storageClient, containerName, logger);
+ const legacyPathCasing =
+ config.getOptionalBoolean(
+ 'techdocs.legacyUseCaseSensitiveTripletPaths',
+ ) || false;
+
+ return new AzureBlobStoragePublish(
+ storageClient,
+ containerName,
+ legacyPathCasing,
+ logger,
+ );
}
constructor(
private readonly storageClient: BlobServiceClient,
private readonly containerName: string,
+ private readonly legacyPathCasing: boolean,
private readonly logger: Logger,
) {
this.storageClient = storageClient;
this.containerName = containerName;
+ this.legacyPathCasing = legacyPathCasing;
this.logger = logger;
}
@@ -167,9 +179,12 @@ export class AzureBlobStoragePublish implements PublisherBase {
const entityRootDir = `${
entity.metadata?.namespace ?? ENTITY_DEFAULT_NAMESPACE
}/${entity.kind}/${entity.metadata.name}`;
- const destination = lowerCaseEntityTripletInStoragePath(
- `${entityRootDir}/${relativeFilePathPosix}`,
- ); // Azure Blob Storage Container file relative path
+
+ const relativeFilePathTriplet = `${entityRootDir}/${relativeFilePathPosix}`;
+ const destination = this.legacyPathCasing
+ ? relativeFilePathTriplet
+ : lowerCaseEntityTripletInStoragePath(relativeFilePathTriplet); // Azure Blob Storage Container file relative path
+
return limiter(async () => {
const response = await this.storageClient
.getContainerClient(this.containerName)
@@ -242,9 +257,11 @@ export class AzureBlobStoragePublish implements PublisherBase {
async fetchTechDocsMetadata(
entityName: EntityName,
): Promise {
- const entityRootDir = lowerCaseEntityTriplet(
- `${entityName.namespace}/${entityName.kind}/${entityName.name}`,
- );
+ const entityTriplet = `${entityName.namespace}/${entityName.kind}/${entityName.name}`;
+ const entityRootDir = this.legacyPathCasing
+ ? entityTriplet
+ : lowerCaseEntityTriplet(entityTriplet);
+
try {
const techdocsMetadataJson = await this.download(
this.containerName,
@@ -273,7 +290,9 @@ export class AzureBlobStoragePublish implements PublisherBase {
const decodedUri = decodeURI(req.path.replace(/^\//, ''));
// filePath example - /default/Component/documented-component/index.html
- const filePath = lowerCaseEntityTripletInStoragePath(decodedUri);
+ const filePath = this.legacyPathCasing
+ ? decodedUri
+ : lowerCaseEntityTripletInStoragePath(decodedUri);
// Files with different extensions (CSS, HTML) need to be served with different headers
const fileExtension = platformPath.extname(filePath);
@@ -301,9 +320,11 @@ export class AzureBlobStoragePublish implements PublisherBase {
* can be used to verify if there are any pre-generated docs available to serve.
*/
hasDocsBeenGenerated(entity: Entity): Promise {
- const entityRootDir = lowerCaseEntityTriplet(
- `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`,
- );
+ const entityTriplet = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`;
+ const entityRootDir = this.legacyPathCasing
+ ? entityTriplet
+ : lowerCaseEntityTriplet(entityTriplet);
+
return this.storageClient
.getContainerClient(this.containerName)
.getBlockBlobClient(`${entityRootDir}/index.html`)
diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts b/packages/techdocs-common/src/stages/publish/googleStorage.test.ts
index a1c9d70fd3..ce08e2f2ce 100644
--- a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts
+++ b/packages/techdocs-common/src/stages/publish/googleStorage.test.ts
@@ -15,11 +15,7 @@
*/
import { getVoidLogger } from '@backstage/backend-common';
-import {
- Entity,
- ENTITY_DEFAULT_NAMESPACE,
- EntityName,
-} from '@backstage/catalog-model';
+import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import express from 'express';
import request from 'supertest';
@@ -27,30 +23,9 @@ import mockFs from 'mock-fs';
import os from 'os';
import path from 'path';
import { GoogleGCSPublish } from './googleStorage';
-import { PublisherBase, TechDocsMetadata } from './types';
// NOTE: /packages/techdocs-common/__mocks__ is being used to mock Google Cloud Storage client library
-const createMockEntity = (annotations = {}): Entity => {
- return {
- apiVersion: 'version',
- kind: 'TestKind',
- metadata: {
- name: 'test-component-name',
- namespace: 'test-namespace',
- annotations: {
- ...annotations,
- },
- },
- };
-};
-
-const createMockEntityName = (): EntityName => ({
- kind: 'TestKind',
- name: 'test-component-name',
- namespace: 'test-namespace',
-});
-
const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir';
const getEntityRootDir = (entity: Entity) => {
@@ -65,10 +40,13 @@ const getEntityRootDir = (entity: Entity) => {
const logger = getVoidLogger();
jest.spyOn(logger, 'info').mockReturnValue(logger);
-let publisher: PublisherBase;
-
-beforeEach(async () => {
- mockFs.restore();
+const createPublisherFromConfig = ({
+ bucketName = 'bucketName',
+ legacyUseCaseSensitiveTripletPaths = false,
+}: {
+ bucketName?: string;
+ legacyUseCaseSensitiveTripletPaths?: boolean;
+} = {}) => {
const mockConfig = new ConfigReader({
techdocs: {
requestUrl: 'http://localhost:7000',
@@ -76,76 +54,127 @@ beforeEach(async () => {
type: 'googleGcs',
googleGcs: {
credentials: '{}',
- bucketName: 'bucketName',
+ bucketName,
},
},
+ legacyUseCaseSensitiveTripletPaths,
},
});
- publisher = await GoogleGCSPublish.fromConfig(mockConfig, logger);
-});
+ return GoogleGCSPublish.fromConfig(mockConfig, logger);
+};
describe('GoogleGCSPublish', () => {
+ const entity = {
+ apiVersion: 'version',
+ kind: 'Component',
+ metadata: {
+ name: 'backstage',
+ namespace: 'default',
+ annotations: {},
+ },
+ };
+
+ const entityName = {
+ kind: 'Component',
+ name: 'backstage',
+ namespace: 'default',
+ };
+
+ const techdocsMetadata = {
+ site_name: 'backstage',
+ site_description: 'site_content',
+ 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 files = {
+ 'index.html': '',
+ '404.html': '',
+ assets: {
+ 'main.css': '',
+ },
+ 'techdocs_metadata.json': JSON.stringify(techdocsMetadata),
+ html: {
+ 'unsafe.html': '',
+ },
+ img: {
+ 'with spaces.png': 'found it',
+ 'unsafe.svg': '',
+ },
+ 'some folder': {
+ 'also with spaces.js': 'found it too',
+ },
+ };
+
+ beforeAll(() => {
+ mockFs({
+ [localRootDir]: files,
+ [storageRootDir]: files,
+ [storageSingleQuoteDir]: {
+ 'techdocs_metadata.json': files['techdocs_metadata.json'].replace(
+ /"/g,
+ "'",
+ ),
+ },
+ });
+ });
+
+ afterAll(() => {
+ mockFs.restore();
+ });
+
describe('getReadiness', () => {
it('should validate correct config', async () => {
+ const publisher = createPublisherFromConfig();
expect(await publisher.getReadiness()).toEqual({
isAvailable: true,
});
});
it('should reject incorrect config', async () => {
- const mockConfig = new ConfigReader({
- techdocs: {
- requestUrl: 'http://localhost:7000',
- publisher: {
- type: 'googleGcs',
- googleGcs: {
- credentials: '{}',
- bucketName: 'errorBucket',
- },
- },
- },
+ const publisher = createPublisherFromConfig({
+ bucketName: 'errorBucket',
});
-
- const errorPublisher = GoogleGCSPublish.fromConfig(mockConfig, logger);
-
- expect(await errorPublisher.getReadiness()).toEqual({
+ expect(await publisher.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);
-
+ const publisher = createPublisherFromConfig();
expect(
await publisher.publish({
entity,
- directory: entityRootDir,
+ directory: localRootDir,
+ }),
+ ).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();
- mockFs.restore();
});
it('should fail to publish a directory', async () => {
@@ -157,14 +186,7 @@ describe('GoogleGCSPublish', () => {
'generatedDirectory',
);
- const entity = createMockEntity();
-
- await expect(
- publisher.publish({
- entity,
- directory: wrongPathToGeneratedDirectory,
- }),
- ).rejects.toThrowError();
+ const publisher = createPublisherFromConfig();
const fails = publisher.publish({
entity,
@@ -178,173 +200,136 @@ describe('GoogleGCSPublish', () => {
`Unable to upload file(s) to Google Cloud Storage. 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,
- kind: entity.kind.toLowerCase(),
- });
-
- mockFs({
- [entityRootDir]: {
- 'index.html': 'file-content',
- },
- });
-
+ const publisher = createPublisherFromConfig();
+ expect(await publisher.hasDocsBeenGenerated(entity)).toBe(true);
+ });
+
+ it('should return true if docs has been generated even if the legacy case is enabled', async () => {
+ const publisher = createPublisherFromConfig({
+ legacyUseCaseSensitiveTripletPaths: true,
+ });
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);
+ const publisher = createPublisherFromConfig();
+ expect(
+ await publisher.hasDocsBeenGenerated({
+ kind: 'entity',
+ metadata: {
+ namespace: 'invalid',
+ name: 'triplet',
+ },
+ } as Entity),
+ ).toBe(false);
});
});
describe('fetchTechDocsMetadata', () => {
- beforeEach(() => {
- mockFs.restore();
+ it('should return tech docs metadata', async () => {
+ const publisher = createPublisherFromConfig();
+ expect(await publisher.fetchTechDocsMetadata(entityName)).toStrictEqual(
+ techdocsMetadata,
+ );
});
- it('should return tech docs metadata', async () => {
- const entityNameMock = createMockEntityName();
- const entity = createMockEntity();
- const entityRootDir = getEntityRootDir({
- ...entity,
- kind: entity.kind.toLowerCase(),
+ it('should return tech docs metadata even if the legacy case is enabled', async () => {
+ const publisher = createPublisherFromConfig({
+ legacyUseCaseSensitiveTripletPaths: true,
});
-
- mockFs({
- [entityRootDir]: {
- 'techdocs_metadata.json':
- '{"site_name": "backstage", "site_description": "site_content", "etag": "etag"}',
- },
- });
-
- const expectedMetadata: TechDocsMetadata = {
- site_name: 'backstage',
- site_description: 'site_content',
- etag: 'etag',
- };
- expect(
- await publisher.fetchTechDocsMetadata(entityNameMock),
- ).toStrictEqual(expectedMetadata);
+ expect(await publisher.fetchTechDocsMetadata(entityName)).toStrictEqual(
+ techdocsMetadata,
+ );
});
it('should return tech docs metadata when json encoded with single quotes', async () => {
- const entityNameMock = createMockEntityName();
- const entity = createMockEntity();
- const entityRootDir = getEntityRootDir({
- ...entity,
- kind: entity.kind.toLowerCase(),
- });
-
- mockFs({
- [entityRootDir]: {
- 'techdocs_metadata.json':
- "{'site_name': 'backstage', 'site_description': 'site_content', 'etag': 'etag'}",
- },
- });
-
- const expectedMetadata: TechDocsMetadata = {
- site_name: 'backstage',
- site_description: 'site_content',
- etag: 'etag',
- };
+ const publisher = createPublisherFromConfig();
expect(
- await publisher.fetchTechDocsMetadata(entityNameMock),
- ).toStrictEqual(expectedMetadata);
- mockFs.restore();
+ await publisher.fetchTechDocsMetadata({
+ namespace: 'storage',
+ kind: 'single',
+ name: 'quote',
+ }),
+ ).toStrictEqual(techdocsMetadata);
});
it('should return an error if the techdocs_metadata.json file is not present', async () => {
- const entityNameMock = createMockEntityName();
- const entity = createMockEntity();
- const entityRootDir = getEntityRootDir({
- ...entity,
- kind: entity.kind.toLowerCase(),
- });
+ const publisher = createPublisherFromConfig();
- const fails = publisher.fetchTechDocsMetadata(entityNameMock);
+ const invalidEntityName = {
+ namespace: 'invalid',
+ kind: 'triplet',
+ name: 'path',
+ };
+
+ const techDocsMetadaFilePath = path.join(
+ rootDir,
+ ...Object.values(invalidEntityName),
+ 'techdocs_metadata.json',
+ );
+
+ const fails = publisher.fetchTechDocsMetadata(invalidEntityName);
await expect(fails).rejects.toMatchObject({
- message: `The file ${path.join(
- entityRootDir,
- 'techdocs_metadata.json',
- )} does not exist !`,
+ message: `The file ${techDocsMetadaFilePath} does not exist !`,
});
});
});
describe('docsRouter', () => {
+ const entityTripletPath = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`;
+
let app: express.Express;
- const entity = createMockEntity();
- const entityRootDir = getEntityRootDir({
- ...entity,
- kind: entity.kind.toLowerCase(),
- });
beforeEach(() => {
+ const publisher = createPublisherFromConfig();
app = express().use(publisher.docsRouter());
-
- mockFs.restore();
- mockFs({
- [entityRootDir]: {
- html: {
- 'unsafe.html': '',
- },
- img: {
- 'with spaces.png': 'found it',
- 'unsafe.svg': '',
- },
- '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`,
+ `/${entityTripletPath}/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`,
+ `/${entityTripletPath}/some%20folder/also%20with%20spaces.js`,
+ );
+ expect(jsResponse.text).toEqual('found it too');
+ });
+
+ it('should pass expected object path to bucket even if the legacy case is enabled', async () => {
+ const publisher = createPublisherFromConfig({
+ legacyUseCaseSensitiveTripletPaths: true,
+ });
+ 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`,
+ );
+ expect(Buffer.from(pngResponse.body).toString('utf8')).toEqual(
+ 'found it',
+ );
+ const jsResponse = await request(app).get(
+ `/${entityTripletPath}/some%20folder/also%20with%20spaces.js`,
);
expect(jsResponse.text).toEqual('found it too');
});
it('should pass text/plain content-type for html', async () => {
- const {
- kind,
- metadata: { namespace, name },
- } = entity;
-
const htmlResponse = await request(app).get(
- `/${namespace}/${kind}/${name}/html/unsafe.html`,
+ `/${entityTripletPath}/html/unsafe.html`,
);
expect(htmlResponse.text).toEqual('');
expect(htmlResponse.header).toMatchObject({
@@ -352,7 +337,7 @@ describe('GoogleGCSPublish', () => {
});
const svgResponse = await request(app).get(
- `/${namespace}/${kind}/${name}/img/unsafe.svg`,
+ `/${entityTripletPath}/img/unsafe.svg`,
);
expect(svgResponse.text).toEqual('');
expect(svgResponse.header).toMatchObject({
diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.ts b/packages/techdocs-common/src/stages/publish/googleStorage.ts
index 3aa7b09b74..9db9b6fb49 100644
--- a/packages/techdocs-common/src/stages/publish/googleStorage.ts
+++ b/packages/techdocs-common/src/stages/publish/googleStorage.ts
@@ -99,8 +99,8 @@ export class GoogleGCSPublish implements PublisherBase {
) {
this.storageClient = storageClient;
this.bucketName = bucketName;
- this.logger = logger;
this.legacyPathCasing = legacyPathCasing;
+ this.logger = logger;
}
/**
@@ -160,11 +160,10 @@ export class GoogleGCSPublish implements PublisherBase {
entity.metadata?.namespace ?? ENTITY_DEFAULT_NAMESPACE
}/${entity.kind}/${entity.metadata.name}`;
+ const relativeFilePathTriplet = `${entityRootDir}/${relativeFilePathPosix}`;
const destination = this.legacyPathCasing
- ? lowerCaseEntityTripletInStoragePath(
- `${entityRootDir}/${relativeFilePathPosix}`,
- )
- : `${entityRootDir}/${relativeFilePathPosix}`; // GCS Bucket file relative path
+ ? relativeFilePathTriplet
+ : lowerCaseEntityTripletInStoragePath(relativeFilePathTriplet); // GCS Bucket file relative path
// Rate limit the concurrent execution of file uploads to batches of 10 (per publish)
const uploadFile = limiter(() =>
diff --git a/plugins/techdocs-backend/config.d.ts b/plugins/techdocs-backend/config.d.ts
index 7f2fdf8b26..537daa815f 100644
--- a/plugins/techdocs-backend/config.d.ts
+++ b/plugins/techdocs-backend/config.d.ts
@@ -246,5 +246,10 @@ export interface Config {
* @deprecated
*/
storageUrl?: string;
+
+ /**
+ * (Optional)
+ */
+ legacyUseCaseSensitiveTripletPaths?: boolean;
};
}