diff --git a/ghost/core/test/e2e-frontend/preview_routes.test.js b/ghost/core/test/e2e-frontend/preview_routes.test.js index ec28e89db3..9258fe1b96 100644 --- a/ghost/core/test/e2e-frontend/preview_routes.test.js +++ b/ghost/core/test/e2e-frontend/preview_routes.test.js @@ -34,6 +34,10 @@ describe('Frontend Routing: Preview Routes', function () { request = supertest.agent(config.get('url')); }); + after(async function () { + await testUtils.stopGhost(); + }); + before(addPosts); it('should display draft posts accessed via uuid', async function () { diff --git a/ghost/core/test/e2e-server/1-options-requests.test.js b/ghost/core/test/e2e-server/1-options-requests.test.js new file mode 100644 index 0000000000..9e51d3301b --- /dev/null +++ b/ghost/core/test/e2e-server/1-options-requests.test.js @@ -0,0 +1,157 @@ +const assert = require('assert'); +const {agentProvider} = require('../utils/e2e-framework'); +const config = require('../../core/shared/config'); + +describe('OPTIONS requests', function () { + let adminAgent; + let membersAgent; + let frontendAgent; + let contentAPIAgent; + let ghostServer; + + before(async function () { + const agents = await agentProvider.getAgentsWithFrontend(); + adminAgent = agents.adminAgent; + membersAgent = agents.membersAgent; + frontendAgent = agents.frontendAgent; + contentAPIAgent = agents.contentAPIAgent; + ghostServer = agents.ghostServer; + }); + + after(async function () { + await ghostServer.stop(); + }); + + describe('CORS headers in Admin API', function () { + it('Handles same origin request', async function () { + await adminAgent + .options('site') + .expectStatus(204) + .matchHeaderSnapshot(); + }); + + it('Rejects no origin header request', async function () { + await adminAgent + .options('site', { + headers: { + origin: null + } + }) + .expectStatus(200) + .matchHeaderSnapshot(); + }); + + it('Rejects unsupported origin header request', async function () { + await adminAgent + .options('site', { + headers: { + origin: 'https://otherdomain.tld/' + } + }) + .expectStatus(200) + .matchHeaderSnapshot(); + }); + }); + + describe('CORS headers in Members API', function () { + it('Responds with no referer vary header value when same referer', async function () { + await membersAgent + .options('member') + .expectStatus(204) + .matchHeaderSnapshot(); + }); + + it('Does not allow CORS with when no origin is present in the request', async function () { + await membersAgent + .options('member', { + headers: { + origin: null + } + }) + .expectStatus(204) + .matchHeaderSnapshot(); + }); + + it('Responds with no referer vary header value when different referer', async function () { + await membersAgent + .options('member', { + headers: { + origin: 'https://otherdomain.tld/' + } + }) + .expectStatus(204) + .matchHeaderSnapshot(); + }); + }); + + describe('CORS headers in Frontend', function () { + it('Responds with no referer vary header value when same referer', async function () { + const res = await frontendAgent + .set('Origin', config.get('url')) + .options('/') + .expect(204); + + assert.equal(res.headers['access-control-allow-origin'], 'http://127.0.0.1:2369'); + assert.equal(res.headers['access-control-allow-credentials'], 'true'); + assert.equal(res.headers['access-control-allow-methods'], 'GET,HEAD,PUT,PATCH,POST,DELETE'); + assert.equal(res.headers['access-control-max-age'], '86400'); + assert.equal(res.headers.vary, 'Origin, Access-Control-Request-Headers'); + + assert.equal(res.headers['cache-control'], undefined); + assert.equal(res.headers.allow, undefined); + }); + + it('Does not allow CORS with when no origin is present in the request', async function () { + const res = await frontendAgent + .options('/') + .set('origin', null) + .expect(200); + + assert.equal(res.headers['cache-control'], 'public, max-age=0'); + assert.equal(res.headers.vary, 'Accept-Encoding'); + assert.equal(res.headers.allow, 'POST,GET,HEAD'); + }); + + it('Responds with no referer vary header value when different referer', async function () { + const res = await frontendAgent + .options('/') + .set('origin', 'https://otherdomain.tld/') + .expect(200); + + assert.equal(res.headers['cache-control'], 'public, max-age=0'); + assert.equal(res.headers.vary, 'Accept-Encoding'); + assert.equal(res.headers.allow, 'POST,GET,HEAD'); + }); + }); + + describe('CORS headers in Content API', function () { + it('Responds with no referer vary header value when same referer', async function () { + await contentAPIAgent + .options('site') + .expectStatus(204) + .matchHeaderSnapshot(); + }); + + it('Does not allow CORS with when no origin is present in the request', async function () { + await contentAPIAgent + .options('site', { + headers: { + origin: null + } + }) + .expectStatus(204) + .matchHeaderSnapshot(); + }); + + it('Responds with no referer vary header value when different referer', async function () { + await contentAPIAgent + .options('site', { + headers: { + origin: 'https://otherdomain.tld/' + } + }) + .expectStatus(204) + .matchHeaderSnapshot(); + }); + }); +}); diff --git a/ghost/core/test/e2e-server/__snapshots__/1-options-requests.test.js.snap b/ghost/core/test/e2e-server/__snapshots__/1-options-requests.test.js.snap new file mode 100644 index 0000000000..8602b05ebc --- /dev/null +++ b/ghost/core/test/e2e-server/__snapshots__/1-options-requests.test.js.snap @@ -0,0 +1,109 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OPTIONS requests CORS headers in Admin API Handles same origin request 1: [headers] 1`] = ` +Object { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "http://127.0.0.1:2369", + "access-control-max-age": "86400", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-length": "0", + "vary": "Accept-Version, Origin, Access-Control-Request-Headers", + "x-powered-by": "Express", +} +`; + +exports[`OPTIONS requests CORS headers in Admin API Rejects no origin header request 1: [headers] 1`] = ` +Object { + "allow": "GET,HEAD", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-length": "8", + "content-type": "text/html; charset=utf-8", + "etag": "W/\\"8-ZRAf8oNBS3Bjb/SU2GYZCmbtmXg\\"", + "vary": "Accept-Version, Accept-Encoding", + "x-powered-by": "Express", +} +`; + +exports[`OPTIONS requests CORS headers in Admin API Rejects unsupported origin header request 1: [headers] 1`] = ` +Object { + "allow": "GET,HEAD", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-length": "8", + "content-type": "text/html; charset=utf-8", + "etag": "W/\\"8-ZRAf8oNBS3Bjb/SU2GYZCmbtmXg\\"", + "vary": "Accept-Version, Accept-Encoding", + "x-powered-by": "Express", +} +`; + +exports[`OPTIONS requests CORS headers in Content API Does not allow CORS with when no origin is present in the request 1: [headers] 1`] = ` +Object { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "*", + "access-control-max-age": "86400", + "cache-control": "public, max-age=0", + "content-length": "0", + "vary": "Accept-Version, Access-Control-Request-Headers", + "x-powered-by": "Express", +} +`; + +exports[`OPTIONS requests CORS headers in Content API Responds with no referer vary header value when different referer 1: [headers] 1`] = ` +Object { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "*", + "access-control-max-age": "86400", + "cache-control": "public, max-age=0", + "content-length": "0", + "vary": "Accept-Version, Access-Control-Request-Headers", + "x-powered-by": "Express", +} +`; + +exports[`OPTIONS requests CORS headers in Content API Responds with no referer vary header value when same referer 1: [headers] 1`] = ` +Object { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "*", + "access-control-max-age": "86400", + "cache-control": "public, max-age=0", + "content-length": "0", + "vary": "Accept-Version, Access-Control-Request-Headers", + "x-powered-by": "Express", +} +`; + +exports[`OPTIONS requests CORS headers in Members API Does not allow CORS with when no origin is present in the request 1: [headers] 1`] = ` +Object { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "*", + "access-control-max-age": "86400", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-length": "0", + "vary": "Access-Control-Request-Headers", + "x-powered-by": "Express", +} +`; + +exports[`OPTIONS requests CORS headers in Members API Responds with no referer vary header value when different referer 1: [headers] 1`] = ` +Object { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "*", + "access-control-max-age": "86400", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-length": "0", + "vary": "Access-Control-Request-Headers", + "x-powered-by": "Express", +} +`; + +exports[`OPTIONS requests CORS headers in Members API Responds with no referer vary header value when same referer 1: [headers] 1`] = ` +Object { + "access-control-allow-methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "access-control-allow-origin": "*", + "access-control-max-age": "86400", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-length": "0", + "vary": "Access-Control-Request-Headers", + "x-powered-by": "Express", +} +`; diff --git a/ghost/core/test/e2e-server/click-tracking.test.js b/ghost/core/test/e2e-server/click-tracking.test.js index 13e77d72b3..d0e33d7951 100644 --- a/ghost/core/test/e2e-server/click-tracking.test.js +++ b/ghost/core/test/e2e-server/click-tracking.test.js @@ -3,6 +3,8 @@ const fetch = require('node-fetch').default; const {agentProvider, mockManager, fixtureManager} = require('../utils/e2e-framework'); const urlUtils = require('../../core/shared/url-utils'); +// @NOTE: this test suite cannot be run in isolation - most likely because it needs +// to have full frontend part of Ghost initialized, not just the backend describe('Click Tracking', function () { let agent; diff --git a/ghost/core/test/utils/e2e-framework.js b/ghost/core/test/utils/e2e-framework.js index 327cb239f5..a43ca6fa70 100644 --- a/ghost/core/test/utils/e2e-framework.js +++ b/ghost/core/test/utils/e2e-framework.js @@ -293,10 +293,11 @@ const getAgentsForMembers = async () => { /** * @NOTE: for now method returns a supertest agent for Frontend instead of test agent with snapshot support. - * frontendAgent should be returning an instance of TestAgent. - * @returns {Promise<{adminAgent: InstanceType, membersAgent: InstanceType, frontendAgent: InstanceType, contentAPIAgent: InstanceType}>} agents + * frontendAgent should be returning an instance of TestAgent (related: https://github.com/TryGhost/Toolbox/issues/471) + * @returns {Promise<{adminAgent: InstanceType, membersAgent: InstanceType, frontendAgent: InstanceType, contentAPIAgent: InstanceType, ghostServer: Express.Application}>} agents */ const getAgentsWithFrontend = async () => { + let ghostServer; let membersAgent; let adminAgent; let frontendAgent; @@ -307,7 +308,9 @@ const getAgentsWithFrontend = async () => { server: true }; try { - const app = (await startGhost(bootOptions)).rootApp; + ghostServer = await startGhost(bootOptions); + const app = ghostServer.rootApp; + const originURL = configUtils.config.get('url'); membersAgent = new MembersAPITestAgent(app, { @@ -332,7 +335,9 @@ const getAgentsWithFrontend = async () => { adminAgent, membersAgent, frontendAgent, - contentAPIAgent + contentAPIAgent, + // @NOTE: ghost server should not be exposed ideally, it's a hack (see commit message) + ghostServer }; };