diff --git a/packages/techdocs-common/src/stages/publish/local.test.ts b/packages/techdocs-common/src/stages/publish/local.test.ts index 9609e4f12d..4372dcdc1f 100644 --- a/packages/techdocs-common/src/stages/publish/local.test.ts +++ b/packages/techdocs-common/src/stages/publish/local.test.ts @@ -117,6 +117,13 @@ describe('local publisher', () => { [resolvedDir]: { 'unsafe.html': '', 'unsafe.svg': '', + default: { + testkind: { + testname: { + 'index.html': 'found it', + }, + }, + }, }, }); }); @@ -138,5 +145,37 @@ describe('local publisher', () => { 'content-type': 'text/plain; charset=utf-8', }); }); + + it('should redirect case-sensitive triplet path to lower-case', async () => { + await request(app) + .get('/default/TestKind/TestName/index.html') + .expect(301) + .expect('Location', '/default/testkind/testname/index.html'); + }); + + it('should resolve lower-case triplet path content eventually', async () => { + const response = await request(app) + .get('/default/TestKind/TestName/index.html') + .redirects(1); + expect(response.text).toEqual('found it'); + }); + + it('should not redirect when legacy case setting is used', async () => { + const legacyConfig = new ConfigReader({ + techdocs: { + legacyUseCaseSensitiveTripletPaths: true, + }, + }); + const legacyPublisher = new LocalPublish( + legacyConfig, + logger, + testDiscovery, + ); + app = express().use(legacyPublisher.docsRouter()); + + await request(app) + .get('/default/TestKind/TestName/index.html') + .expect(404); + }); }); }); diff --git a/packages/techdocs-common/src/stages/publish/local.ts b/packages/techdocs-common/src/stages/publish/local.ts index b59bae88bb..c547b65406 100644 --- a/packages/techdocs-common/src/stages/publish/local.ts +++ b/packages/techdocs-common/src/stages/publish/local.ts @@ -142,16 +142,55 @@ export class LocalPublish implements PublisherBase { } docsRouter(): express.Handler { - return express.static(staticDocsDir, { - // Handle content-type header the same as all other publishers. - setHeaders: (res, filePath) => { - const fileExtension = path.extname(filePath); - const headers = getHeadersForFileExtension(fileExtension); - for (const [header, value] of Object.entries(headers)) { - res.setHeader(header, value); - } - }, + const router = express.Router(); + + // Redirect middleware ensuring that requests to case-sensitive entity + // triplet paths are always sent to lower-case versions. + router.use((req, res, next) => { + // If legacy path casing is on, let the request immediately continue. + if (this.legacyPathCasing) { + return next(); + } + + // Generate a lower-case entity triplet path. + const [_, namespace, kind, name, ...rest] = req.path.split('/'); + + // Ignore non-triplet objects. + if (!namespace || !kind || !name) { + return next(); + } + + const newPath = [ + _, + namespace.toLowerCase(), + kind.toLowerCase(), + name.toLowerCase(), + ...rest, + ].join('/'); + + // If there was no change, then let express.static() handle the request. + if (newPath === req.path) { + return next(); + } + + // Otherwise, redirect to the new path. + return res.redirect(req.baseUrl + newPath, 301); }); + + router.use( + express.static(staticDocsDir, { + // Handle content-type header the same as all other publishers. + setHeaders: (res, filePath) => { + const fileExtension = path.extname(filePath); + const headers = getHeadersForFileExtension(fileExtension); + for (const [header, value] of Object.entries(headers)) { + res.setHeader(header, value); + } + }, + }), + ); + + return router; } async hasDocsBeenGenerated(entity: Entity): Promise {