From 4916a6e23d8b4f8549134320995129f809bd1a86 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 7 May 2021 14:51:09 +0200 Subject: [PATCH] Decode req.path before loading object from GCS Signed-off-by: Eric Peterson --- .../__mocks__/@google-cloud/storage.ts | 14 ++++--- packages/techdocs-common/package.json | 4 +- .../src/stages/publish/googleStorage.test.ts | 41 +++++++++++++++++++ .../src/stages/publish/googleStorage.ts | 4 +- 4 files changed, 54 insertions(+), 9 deletions(-) diff --git a/packages/techdocs-common/__mocks__/@google-cloud/storage.ts b/packages/techdocs-common/__mocks__/@google-cloud/storage.ts index f8d0368f27..11fbc32de2 100644 --- a/packages/techdocs-common/__mocks__/@google-cloud/storage.ts +++ b/packages/techdocs-common/__mocks__/@google-cloud/storage.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EventEmitter } from 'events'; +import { Readable, Writable } from 'stream'; import fs from 'fs-extra'; import os from 'os'; import path from 'path'; @@ -58,19 +58,21 @@ class GCSFile { } createReadStream() { - const emitter = new EventEmitter(); + const readable = new Readable(); + readable._read = () => {}; + process.nextTick(() => { if (fs.existsSync(this.localFilePath)) { - emitter.emit('data', Buffer.from(fs.readFileSync(this.localFilePath))); - emitter.emit('end'); + readable.emit('data', fs.readFileSync(this.localFilePath)); + readable.emit('end'); } else { - emitter.emit( + readable.emit( 'error', new Error(`The file ${this.localFilePath} does not exist !`), ); } }); - return emitter; + return readable; } } diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index f2d19ba8c5..7c12f6f675 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -67,7 +67,9 @@ "@types/mime-types": "^2.1.0", "@types/mock-fs": "^4.13.0", "@types/pkgcloud": "^1.7.4", - "@types/recursive-readdir": "^2.2.0" + "@types/recursive-readdir": "^2.2.0", + "@types/supertest": "^2.0.8", + "supertest": "^6.1.3" }, "jest": { "roots": [ diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts b/packages/techdocs-common/src/stages/publish/googleStorage.test.ts index 19e4938763..9a966e6a60 100644 --- a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts +++ b/packages/techdocs-common/src/stages/publish/googleStorage.test.ts @@ -21,6 +21,8 @@ import { EntityName, } from '@backstage/catalog-model'; 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'; @@ -271,4 +273,43 @@ describe('GoogleGCSPublish', () => { }); }); }); + + describe('docsRouter', () => { + let app: express.Express; + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + beforeEach(() => { + app = express().use(publisher.docsRouter()); + + mockFs.restore(); + mockFs({ + [entityRootDir]: { + img: { + 'with spaces.png': 'found it', + }, + 'some folder': { + 'also with spaces.js': 'found it too', + }, + }, + }); + }); + + 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(pngResponse.text).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'); + }); + }); }); diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.ts b/packages/techdocs-common/src/stages/publish/googleStorage.ts index 8adafc361a..e5e3d6c25a 100644 --- a/packages/techdocs-common/src/stages/publish/googleStorage.ts +++ b/packages/techdocs-common/src/stages/publish/googleStorage.ts @@ -187,9 +187,9 @@ export class GoogleGCSPublish implements PublisherBase { */ docsRouter(): express.Handler { return (req, res) => { - // Trim the leading forward slash + // Decode and trim the leading forward slash // filePath example - /default/Component/documented-component/index.html - const filePath = req.path.replace(/^\//, ''); + const filePath = decodeURI(req.path.replace(/^\//, '')); // Files with different extensions (CSS, HTML) need to be served with different headers const fileExtension = path.extname(filePath);