diff --git a/e2e/custom.test.js b/e2e/custom.test.js index e5b9c3b..da1c781 100644 --- a/e2e/custom.test.js +++ b/e2e/custom.test.js @@ -45,3 +45,37 @@ test.describe('Check customizations', () => { expect(logoId).toBe('example-logo') // it is included in the svg file }) }) + +test.describe('Check redirection and url handling of static assets', () => { + test('Check static/index.html redirects', async ({ page }) => { + const jsonResponsePromise = page.waitForResponse(/json/) + await page.goto(`${URL_DOCUMENTATION}/static/index.html`) + + // Check if the page is redirected to /documentation + const url = await page.url() + expect(url).toContain(`${URL_DOCUMENTATION}`) + expect(url).not.toContain('static/index.html') + + // Check if the page has requested the json spec, and if so has it succeeded + const jsonResponse = await jsonResponsePromise + expect(jsonResponse.ok()).toBe(true) + }) + + test('Check root UI without slash loads json spec', async ({ page }) => { + const jsonResponsePromise = page.waitForResponse(/json/) + await page.goto(`${URL_DOCUMENTATION}`) + + // Check if the page has requested the json spec, and if so has it succeeded + const jsonResponse = await jsonResponsePromise + expect(jsonResponse.ok()).toBe(true) + }) + + test('Check root UI with trailing slash loads json spec', async ({ page }) => { + const jsonResponsePromise = page.waitForResponse(/json/) + await page.goto(`${URL_DOCUMENTATION}/`) + + // Check if the page has requested the json spec, and if so has it succeeded + const jsonResponse = await jsonResponsePromise + expect(jsonResponse.ok()).toBe(true) + }) +}) diff --git a/lib/index-html.js b/lib/index-html.js index 518c4a6..2e83015 100644 --- a/lib/index-html.js +++ b/lib/index-html.js @@ -1,29 +1,29 @@ 'use strict' function indexHtml (opts) { - return ` + return (url) => ` ${opts.theme?.title || 'Swagger UI'} - - - ${opts.theme && opts.theme.css ? opts.theme.css.map(css => `\n`).join('') : ''} + + + ${opts.theme && opts.theme.css ? opts.theme.css.map(css => `\n`).join('') : ''} ${opts.theme && opts.theme.favicon -? opts.theme.favicon.map(favicon => `\n`).join('') +? opts.theme.favicon.map(favicon => `\n`).join('') : ` - - + + `}
- - - - ${opts.theme && opts.theme.js ? opts.theme.js.map(js => `\n`).join('') : ''} + + + + ${opts.theme && opts.theme.js ? opts.theme.js.map(js => `\n`).join('') : ''} ` diff --git a/lib/routes.js b/lib/routes.js index a179ff8..bb6e4aa 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -10,19 +10,6 @@ const indexHtml = require('./index-html') // URI prefix to separate static assets for swagger UI const staticPrefix = '/static' -function getRedirectPathForTheRootRoute (url) { - let redirectPath - - if (url.length !== 0 && url[url.length - 1] === '/') { - redirectPath = `.${staticPrefix}/index.html` - } else { - const urlPathParts = url.split('/') - redirectPath = `./${urlPathParts[urlPathParts.length - 1]}${staticPrefix}/index.html` - } - - return redirectPath -} - function fastifySwagger (fastify, opts, done) { let staticCSP = false if (opts.staticCSP === true) { @@ -66,16 +53,6 @@ function fastifySwagger (fastify, opts, done) { } } - fastify.route({ - url: '/', - method: 'GET', - schema: { hide: true }, - ...hooks, - handler: (req, reply) => { - reply.redirect(getRedirectPathForTheRootRoute(req.raw.url)) - } - }) - if (opts.theme) { const themePrefix = `${staticPrefix}/theme` if (opts.theme.css) { @@ -127,17 +104,27 @@ function fastifySwagger (fastify, opts, done) { } } - const indexHtmlContent = indexHtml(opts) + const indexHtmlContent = indexHtml({ ...opts, staticPrefix }) fastify.route({ - url: `${staticPrefix}/index.html`, + url: '/', method: 'GET', schema: { hide: true }, ...hooks, handler: (req, reply) => { reply .header('content-type', 'text/html; charset=utf-8') - .send(indexHtmlContent) + .send(indexHtmlContent(req.url.replace(/\/$/, ''))) // remove trailing slash, as staticPrefix has a leading slash + } + }) + + fastify.route({ + url: `${staticPrefix}/index.html`, + method: 'GET', + schema: { hide: true }, + ...hooks, + handler: (req, reply) => { + reply.redirect(req.url.replace(/\/static\/index\.html$/, '/')) } }) diff --git a/lib/swagger-initializer.js b/lib/swagger-initializer.js index e8e9372..40e8165 100644 --- a/lib/swagger-initializer.js +++ b/lib/swagger-initializer.js @@ -28,8 +28,10 @@ function swaggerInitializer (opts) { }); } function resolveUrl(url) { - const anchor = document.createElement('a') - anchor.href = url + var currentHref = window.location.href; + currentHref = currentHref.endsWith('/') ? currentHref : currentHref + '/'; + var anchor = document.createElement('a'); + anchor.href = currentHref + url; return anchor.href } @@ -47,8 +49,8 @@ function swaggerInitializer (opts) { layout: "StandaloneLayout", validatorUrl: ${serialize(opts.validatorUrl || null)}, }, config, { - url: resolveUrl('./json').replace('static/json', 'json'), - oauth2RedirectUrl: resolveUrl('./oauth2-redirect.html') + url: resolveUrl('./json'), + oauth2RedirectUrl: resolveUrl('./static/oauth2-redirect.html') }); const ui = SwaggerUIBundle(resConfig) diff --git a/test/csp.test.js b/test/csp.test.js index 6d59260..af3d650 100644 --- a/test/csp.test.js +++ b/test/csp.test.js @@ -31,7 +31,7 @@ test('staticCSP = undefined', async (t) => { const res = await fastify.inject({ method: 'GET', - url: '/documentation/static/index.html' + url: '/documentation' }) t.equal(res.statusCode, 200) t.equal(typeof res.headers['content-security-policy'], 'undefined') @@ -57,7 +57,7 @@ test('staticCSP = true', async (t) => { { const res = await fastify.inject({ method: 'GET', - url: '/documentation/static/index.html' + url: '/documentation' }) t.equal(res.statusCode, 200) t.equal(res.headers['content-security-policy'], `default-src 'self'; base-uri 'self'; font-src 'self' https: data:; frame-ancestors 'self'; img-src 'self' data: validator.swagger.io; object-src 'none'; script-src 'self' ${csp.script.join(' ')}; script-src-attr 'none'; style-src 'self' https: ${csp.style.join(' ')}; upgrade-insecure-requests;`) @@ -93,7 +93,7 @@ test('staticCSP = "default-src \'self\';"', async (t) => { { const res = await fastify.inject({ method: 'GET', - url: '/documentation/static/index.html' + url: '/documentation' }) t.equal(res.statusCode, 200) t.equal(res.headers['content-security-policy'], "default-src 'self';") @@ -132,7 +132,7 @@ test('staticCSP = object', async (t) => { { const res = await fastify.inject({ method: 'GET', - url: '/documentation/static/index.html' + url: '/documentation' }) t.equal(res.statusCode, 200) t.equal(res.headers['content-security-policy'], "default-src 'self'; script-src 'self';") @@ -172,7 +172,7 @@ test('transformStaticCSP = function', async (t) => { { const res = await fastify.inject({ method: 'GET', - url: '/documentation/static/index.html' + url: '/documentation' }) t.equal(res.statusCode, 200) t.equal(res.headers['content-security-policy'], "default-src 'self'; script-src 'self';") @@ -212,7 +212,7 @@ test('transformStaticCSP = function, with @fastify/helmet', async (t) => { { const res = await fastify.inject({ method: 'GET', - url: '/documentation/static/index.html' + url: '/documentation' }) t.equal(res.statusCode, 200) t.equal(res.headers['content-security-policy'], "default-src 'self'; script-src 'self';") diff --git a/test/route.test.js b/test/route.test.js index 56e41aa..03fd690 100644 --- a/test/route.test.js +++ b/test/route.test.js @@ -100,8 +100,8 @@ test('fastify.swagger should return a valid swagger yaml', async (t) => { t.pass('valid swagger yaml') }) -test('/documentation should redirect to ./documentation/static/index.html', async (t) => { - t.plan(3) +test('/documentation should display index html', async (t) => { + t.plan(4) const fastify = Fastify() await fastify.register(fastifySwagger, swaggerOption) await fastify.register(fastifySwaggerUi) @@ -117,13 +117,14 @@ test('/documentation should redirect to ./documentation/static/index.html', asyn method: 'GET', url: '/documentation' }) - t.equal(res.statusCode, 302) - t.equal(res.headers.location, './documentation/static/index.html') + t.equal(res.statusCode, 200) + t.equal(res.headers.location, undefined) t.equal(typeof res.payload, 'string') + t.equal('text/html; charset=utf-8', res.headers['content-type']) }) -test('/documentation/ should redirect to ./static/index.html', async (t) => { - t.plan(3) +test('/documentation/ should display index html ', async (t) => { + t.plan(4) const fastify = Fastify() await fastify.register(fastifySwagger, swaggerOption) await fastify.register(fastifySwaggerUi) @@ -139,13 +140,14 @@ test('/documentation/ should redirect to ./static/index.html', async (t) => { method: 'GET', url: '/documentation/' }) - t.equal(res.statusCode, 302) - t.equal(res.headers.location, './static/index.html') + t.equal(res.statusCode, 200) + t.equal(res.headers.location, undefined) t.equal(typeof res.payload, 'string') + t.equal('text/html; charset=utf-8', res.headers['content-type']) }) -test('/v1/documentation should redirect to ./documentation/static/index.html', async (t) => { - t.plan(3) +test('/v1/documentation should display index html', async (t) => { + t.plan(4) const fastify = Fastify() await fastify.register(fastifySwagger, swaggerOption) await fastify.register(fastifySwaggerUi, { routePrefix: '/v1/documentation' }) @@ -161,13 +163,14 @@ test('/v1/documentation should redirect to ./documentation/static/index.html', a method: 'GET', url: '/v1/documentation' }) - t.equal(res.statusCode, 302) - t.equal(res.headers.location, './documentation/static/index.html') + t.equal(res.statusCode, 200) + t.equal(res.headers.location, undefined) t.equal(typeof res.payload, 'string') + t.equal('text/html; charset=utf-8', res.headers['content-type']) }) -test('/v1/documentation/ should redirect to ./static/index.html', async (t) => { - t.plan(3) +test('/v1/documentation/ should display index html', async (t) => { + t.plan(4) const fastify = Fastify() await fastify.register(fastifySwagger, swaggerOption) await fastify.register(fastifySwaggerUi, { routePrefix: '/v1/documentation' }) @@ -183,18 +186,19 @@ test('/v1/documentation/ should redirect to ./static/index.html', async (t) => { method: 'GET', url: '/v1/documentation/' }) - t.equal(res.statusCode, 302) - t.equal(res.headers.location, './static/index.html') + t.equal(res.statusCode, 200) + t.equal(res.headers.location, undefined) t.equal(typeof res.payload, 'string') + t.equal('text/html; charset=utf-8', res.headers['content-type']) }) -test('/v1/foobar should redirect to ./foobar/static/index.html - in plugin', async (t) => { - t.plan(3) +test('/v1/foobar should display index html', async (t) => { + t.plan(4) const fastify = Fastify() fastify.register(async function (fastify, options) { await fastify.register(fastifySwagger, swaggerOption) - await fastify.register(fastifySwaggerUi, { routePrefix: '/foobar' }) + await fastify.register(fastifySwaggerUi, { routePrefix: '/foobar', noRedirect: true }) fastify.get('/', () => {}) fastify.post('/', () => {}) @@ -208,13 +212,14 @@ test('/v1/foobar should redirect to ./foobar/static/index.html - in plugin', asy method: 'GET', url: '/v1/foobar' }) - t.equal(res.statusCode, 302) - t.equal(res.headers.location, './foobar/static/index.html') + t.equal(res.statusCode, 200) + t.equal(res.headers.location, undefined) t.equal(typeof res.payload, 'string') + t.equal('text/html; charset=utf-8', res.headers['content-type']) }) -test('/v1/foobar/ should redirect to ./static/index.html - in plugin', async (t) => { - t.plan(3) +test('/v1/foobar/ should display index html', async (t) => { + t.plan(4) const fastify = Fastify() fastify.register(async function (fastify, options) { @@ -233,13 +238,14 @@ test('/v1/foobar/ should redirect to ./static/index.html - in plugin', async (t) method: 'GET', url: '/v1/foobar/' }) - t.equal(res.statusCode, 302) - t.equal(res.headers.location, './static/index.html') + t.equal(res.statusCode, 200) + t.equal(res.headers.location, undefined) t.equal(typeof res.payload, 'string') + t.equal('text/html; charset=utf-8', res.headers['content-type']) }) -test('with routePrefix: \'/\' should redirect to ./static/index.html', async (t) => { - t.plan(3) +test('with routePrefix: \'/\' should display index html', async (t) => { + t.plan(4) const fastify = Fastify() await fastify.register(fastifySwagger, swaggerOption) @@ -251,9 +257,10 @@ test('with routePrefix: \'/\' should redirect to ./static/index.html', async (t) method: 'GET', url: '/' }) - t.equal(res.statusCode, 302) - t.equal(res.headers.location, './static/index.html') + t.equal(res.statusCode, 200) + t.equal(res.headers.location, undefined) t.equal(typeof res.payload, 'string') + t.equal('text/html; charset=utf-8', res.headers['content-type']) }) test('/documentation/static/:file should send back the correct file', async (t) => { @@ -275,10 +282,10 @@ test('/documentation/static/:file should send back the correct file', async (t) { const res = await fastify.inject({ method: 'GET', - url: '/documentation/' + url: '/documentation/static/index.html' }) t.equal(res.statusCode, 302) - t.equal(res.headers.location, './static/index.html') + t.equal(res.headers.location, '/documentation/') } { @@ -481,20 +488,6 @@ test('/documentation/:myfile should run custom NotFoundHandler in dynamic mode', t.equal(res.statusCode, 410) }) -test('/documentation/ should redirect to ./static/index.html', async (t) => { - t.plan(2) - const fastify = Fastify() - await fastify.register(fastifySwagger, swaggerOption) - await fastify.register(fastifySwaggerUi) - - const res = await fastify.inject({ - method: 'GET', - url: '/documentation/' - }) - t.equal(res.statusCode, 302) - t.equal(res.headers.location, './static/index.html') -}) - test('/documentation/* should not return module files when baseDir not set', async (t) => { t.plan(1) const fastify = Fastify() @@ -522,8 +515,8 @@ test('should return silent log level of route /documentation', async (t) => { method: 'GET', url: '/documentation/' }) - t.equal(res.statusCode, 302) - t.equal(res.headers.location, './static/index.html') + t.equal(res.statusCode, 200) + t.equal(res.headers['content-type'], 'text/html; charset=utf-8') }) test('should return empty log level of route /documentation', async (t) => { @@ -540,6 +533,23 @@ test('should return empty log level of route /documentation', async (t) => { method: 'GET', url: '/documentation/' }) - t.equal(res.statusCode, 302) - t.equal(res.headers.location, './static/index.html') + t.equal(res.statusCode, 200) + t.equal(res.headers['content-type'], 'text/html; charset=utf-8') +}) + +test('/documentation should display index html with correct asset urls', async (t) => { + t.plan(4) + const fastify = Fastify() + await fastify.register(fastifySwagger, swaggerOption) + await fastify.register(fastifySwaggerUi, { theme: { js: [{ filename: 'theme-js.js' }] } }) + + const res = await fastify.inject({ + method: 'GET', + url: '/documentation' + }) + + t.equal(res.payload.includes('href="/documentation/static/index.css"'), true) + t.equal(res.payload.includes('src="/documentation/static/theme/theme-js.js"'), true) + t.equal(res.payload.includes('href="/documentation/index.css"'), false) + t.equal(res.payload.includes('src="/documentation/theme/theme-js.js"'), false) }) diff --git a/test/theme.test.js b/test/theme.test.js index 894004b..755e2c7 100644 --- a/test/theme.test.js +++ b/test/theme.test.js @@ -20,7 +20,7 @@ test('swagger route does not return additional theme', async (t) => { const res = await fastify.inject({ method: 'GET', - url: '/documentation/static/index.html' + url: '/documentation' }) t.equal(typeof res.payload, 'string') @@ -63,7 +63,7 @@ test('swagger route returns additional theme', async (t) => { const res = await fastify.inject({ method: 'GET', - url: '/documentation/static/index.html' + url: '/documentation' }) t.equal(typeof res.payload, 'string') @@ -119,7 +119,7 @@ test('swagger route returns additional theme - only js', async (t) => { const res = await fastify.inject({ method: 'GET', - url: '/documentation/static/index.html' + url: '/documentation' }) t.equal(typeof res.payload, 'string') @@ -156,7 +156,7 @@ test('swagger route returns additional theme - only css', async (t) => { const res = await fastify.inject({ method: 'GET', - url: '/documentation/static/index.html' + url: '/documentation' }) t.equal(typeof res.payload, 'string') @@ -199,7 +199,7 @@ test('swagger route returns additional theme - only favicon', async (t) => { const res = await fastify.inject({ method: 'GET', - url: '/documentation/static/index.html' + url: '/documentation' }) t.equal(typeof res.payload, 'string') @@ -235,7 +235,7 @@ test('swagger route returns additional theme - only title', async (t) => { const res = await fastify.inject({ method: 'GET', - url: '/documentation/static/index.html' + url: '/documentation' }) t.equal(typeof res.payload, 'string')