src/server/index.test.jsx
import React from 'react';
import request from 'supertest';
import * as reactDomServer from 'react-dom/server';
import dotenv from 'dotenv';
import getRouteProps from '#app/routes/utils/fetchPageData/utils/getRouteProps';
import defaultToggles from '#lib/config/toggles';
import loggerMock from '#testHelpers/loggerMock';
import {
ROUTING_INFORMATION,
SERVER_SIDE_RENDER_REQUEST_RECEIVED,
SERVER_SIDE_REQUEST_FAILED,
} from '#lib/logger.const';
import { FRONT_PAGE, MEDIA_PAGE } from '#app/routes/utils/pageTypes';
import Document from './Document/component';
import routes from '../app/routes';
import * as renderDocument from './Document';
import sendCustomMetrics from './utilities/customMetrics';
import { NON_200_RESPONSE } from './utilities/customMetrics/metrics.const';
import { getMvtVaryHeaders } from './utilities/mvtHeader';
// mimic the logic in `src/index.js` which imports the `server/index.jsx`
dotenv.config({ path: './envConfig/local.env' });
const path = require('path');
const express = require('express');
const server = require('./index').default;
const getToggles = require('../app/lib/utilities/getToggles/withCache').default;
const sendFileSpy = jest.spyOn(express.response, 'sendFile');
const validateHttpHeader = (headers, headerKey, expectedHeaderValue) => {
const headerKeys = Object.keys(headers);
const headerValues = Object.values(headers);
const indexOfXFrame = headerKeys.indexOf(headerKey);
expect(headerKeys).toContain(headerKey);
expect(headerValues[indexOfXFrame]).toEqual(expectedHeaderValue);
};
jest.mock('react-dom/server', () => ({
renderToString: jest.fn().mockImplementation(() => '<h1>Mock app</h1>'),
renderToStaticMarkup: jest
.fn()
.mockImplementation(() => '<html><body><h1>Mock app</h1></body></html>'),
}));
jest.mock('react-helmet', () => ({
Helmet: {
renderStatic: jest.fn().mockReturnValue({ head: 'tags' }),
},
}));
jest.mock('@loadable/server', () => {
class ChunkExtractor {
collectChunks = arg => arg;
getScriptElements = () => '__mock_script_elements__';
getLinkElements = () => '__mock_link_elements__';
}
return {
ChunkExtractor,
ChunkExtractorManager: jest.fn(),
};
});
jest.mock('#app/routes/utils/fetchPageData/utils/getRouteProps');
jest.mock('#app/lib/utilities/getToggles/withCache');
getToggles.mockImplementation(() => defaultToggles.local);
const mockRouteProps = ({
id,
service,
isAmp,
isApp,
dataResponse,
responseType,
variant,
pageType,
}) => {
const getInitialData =
responseType === 'reject'
? jest.fn().mockRejectedValueOnce(dataResponse)
: jest.fn().mockResolvedValueOnce(dataResponse);
// Add a leading slash to match what is received from the application routing regex.
const mockVariantParam = variant ? `/${variant}` : undefined;
getRouteProps.mockReturnValue({
isAmp,
isApp,
service,
variant,
route: { getInitialData, pageType },
match: {
params: { id, service, variant: mockVariantParam },
},
});
};
jest.mock('./utilities/customMetrics');
jest.mock('./utilities/mvtHeader');
const renderDocumentSpy = jest.spyOn(renderDocument, 'default');
const makeRequest = async (requestPath, headers = {}) =>
request(server).get(requestPath).set(headers);
const QUERY_STRING = '?param=test&query=1';
const testRenderedData =
({ url, service, isAmp, isApp, successDataResponse, variant }) =>
async () => {
const { text, status } = await makeRequest(url);
expect(status).toBe(200);
expect(reactDomServer.renderToString).toHaveBeenCalled();
expect(reactDomServer.renderToStaticMarkup).toHaveBeenCalledWith(
<Document
app={{
css: '',
ids: [],
html: '<h1>Mock app</h1>',
}}
data={successDataResponse}
helmet={{ head: 'tags' }}
isAmp={isAmp}
isApp={isApp}
legacyScripts="__mock_script_elements__"
modernScripts="__mock_script_elements__"
links="__mock_link_elements__"
/>,
);
const expectedProps = {
bbcOrigin: undefined,
data: successDataResponse,
isAmp,
isApp,
service,
routes,
url,
};
if (variant) {
expectedProps.variant = variant;
}
expect(renderDocumentSpy).toHaveBeenCalledWith(expectedProps);
expect(getRouteProps).toHaveBeenCalledWith(url.split('?')[0]);
expect(text).toEqual(
'<!doctype html><html><body><h1>Mock app</h1></body></html>',
);
};
const assertNon200ResponseCustomMetrics = ({
requestUrl,
pageType,
statusCode = 500,
}) => {
it('should send custom metrics for non 200 response status code', async () => {
await makeRequest(requestUrl);
expect(sendCustomMetrics).toBeCalledWith({
metricName: NON_200_RESPONSE,
pageType,
requestUrl,
statusCode,
});
});
};
const testFrontPages = ({ platform, service, variant, queryString = '' }) => {
const isAmp = platform === 'amp';
const isApp = platform === 'app';
const extension =
{
amp: '.amp',
app: '.app',
}[platform] || '';
const serviceURL = `/${service}${
variant ? `/${variant}` : ''
}${extension}${queryString}`;
describe(`Front Page: ${serviceURL}`, () => {
const successDataResponse = {
isAmp,
data: { some: 'data' },
service: 'someService',
status: 200,
variant,
};
const notFoundDataResponse = {
isAmp,
data: { some: 'data' },
service: 'someService',
status: 404,
variant,
};
describe('Successful render', () => {
describe('200 status code', () => {
beforeEach(() => {
mockRouteProps({
service,
isAmp,
isApp,
dataResponse: successDataResponse,
variant,
});
});
const configs = {
url: serviceURL,
service,
isAmp,
isApp,
successDataResponse,
variant,
};
it('should respond with rendered data', testRenderedData(configs));
});
describe('404 status code', () => {
const pageType = 'Front Page';
beforeEach(() => {
mockRouteProps({
service,
isAmp,
isApp,
dataResponse: notFoundDataResponse,
variant,
pageType,
});
});
it('should respond with a rendered 404', async () => {
const { status, text } = await makeRequest(serviceURL);
expect(status).toBe(404);
expect(text).toEqual(
'<!doctype html><html><body><h1>Mock app</h1></body></html>',
);
});
assertNon200ResponseCustomMetrics({
requestUrl: serviceURL,
pageType,
statusCode: 404,
});
});
});
describe('Unknown error within the data fetch, react router or its dependencies', () => {
const pageType = FRONT_PAGE;
beforeEach(() => {
mockRouteProps({
service,
isAmp,
isApp,
dataResponse: Error('Error!'),
responseType: 'reject',
variant,
pageType,
});
});
it('should respond with a 500', async () => {
const { status, text } = await makeRequest(serviceURL);
expect(status).toEqual(500);
expect(text).toEqual('Internal server error');
});
assertNon200ResponseCustomMetrics({
requestUrl: serviceURL,
pageType,
});
});
});
};
const testArticles = ({ platform, service, variant, queryString = '' }) => {
const isAmp = platform === 'amp';
const isApp = platform === 'app';
const extension =
{
amp: '.amp',
app: '.app',
}[platform] || '';
describe(`Optimo Article: /${service}/articles/optimoID/${extension}${queryString}`, () => {
const successDataResponse = {
isAmp,
data: { some: 'data' },
service: 'someService',
status: 200,
};
const notFoundDataResponse = {
isAmp,
data: { some: 'data' },
service: 'someService',
status: 404,
};
const id = `c0000000001o`;
const articleURL = `/${service}/articles/${id}${extension}${queryString}`;
describe('Successful render', () => {
describe('200 status code', () => {
beforeEach(() => {
mockRouteProps({
id,
service,
isAmp,
isApp,
dataResponse: successDataResponse,
variant,
});
});
const configs = {
url: articleURL,
service,
isAmp,
isApp,
successDataResponse,
variant,
};
it('should respond with rendered data', testRenderedData(configs));
});
describe('404 status code', () => {
const pageType = 'articles';
beforeEach(() => {
mockRouteProps({
id,
service,
isAmp,
isApp,
dataResponse: notFoundDataResponse,
variant,
pageType,
});
});
it('should respond with a rendered 404', async () => {
const { status, text } = await makeRequest(articleURL);
expect(status).toBe(404);
expect(text).toEqual(
'<!doctype html><html><body><h1>Mock app</h1></body></html>',
);
});
assertNon200ResponseCustomMetrics({
requestUrl: articleURL,
pageType,
statusCode: 404,
});
});
});
describe('Unknown error within the data fetch, react router or its dependencies', () => {
const pageType = 'articles';
beforeEach(() => {
mockRouteProps({
id,
service,
isAmp,
isApp,
dataResponse: Error('Error!'),
responseType: 'reject',
variant,
pageType,
});
});
it('should respond with a 500', async () => {
const { status, text } = await makeRequest(articleURL);
expect(status).toEqual(500);
expect(text).toEqual('Internal server error');
});
assertNon200ResponseCustomMetrics({
requestUrl: articleURL,
pageType,
});
});
});
};
const testAssetPages = ({
platform,
service,
assetUri,
variant,
queryString = '',
}) => {
const isAmp = platform === 'amp';
const isApp = platform === 'app';
const extension =
{
amp: '.amp',
app: '.app',
}[platform] || '';
describe(`CPS Asset: /${service}/${assetUri}${extension}${queryString}`, () => {
const successDataResponse = {
isAmp,
data: { some: 'data' },
service: 'someService',
status: 200,
};
const notFoundDataResponse = {
isAmp,
data: { some: 'data' },
service: 'someService',
status: 404,
};
const articleURL = `/${service}/${assetUri}${extension}${queryString}`;
describe('Successful render', () => {
describe('200 status code', () => {
beforeEach(() => {
mockRouteProps({
assetUri,
service,
isAmp,
isApp,
dataResponse: successDataResponse,
variant,
});
});
const configs = {
url: articleURL,
service,
isAmp,
isApp,
successDataResponse,
variant,
};
it('should respond with rendered data', testRenderedData(configs));
});
describe('404 status code', () => {
const pageType = 'CPS Asset';
beforeEach(() => {
mockRouteProps({
assetUri,
service,
isAmp,
isApp,
dataResponse: notFoundDataResponse,
variant,
pageType,
});
});
it('should respond with a rendered 404', async () => {
const { status, text } = await makeRequest(articleURL);
expect(status).toBe(404);
expect(text).toEqual(
'<!doctype html><html><body><h1>Mock app</h1></body></html>',
);
});
assertNon200ResponseCustomMetrics({
requestUrl: articleURL,
pageType,
statusCode: 404,
});
});
});
describe('Unknown error within the data fetch, react router or its dependencies', () => {
const pageType = 'cpsAsset';
beforeEach(() => {
mockRouteProps({
assetUri,
service,
isAmp,
isApp,
dataResponse: Error('Error!'),
responseType: 'reject',
variant,
pageType,
});
});
it('should respond with a 500', async () => {
const { status, text } = await makeRequest(articleURL);
expect(status).toEqual(500);
expect(text).toEqual('Internal server error');
});
assertNon200ResponseCustomMetrics({
requestUrl: articleURL,
pageType,
});
});
});
};
const testMediaPages = ({
platform,
service,
serviceId,
mediaId,
queryString = '',
}) => {
describe(`${platform} radio page - live radio`, () => {
const isAmp = platform === 'amp';
const successDataResponse = {
isAmp,
data: { some: 'data' },
service: 'someService',
status: 200,
};
const notFoundDataResponse = {
isAmp,
data: { some: 'data' },
service: 'someService',
status: 404,
};
const extension = isAmp ? '.amp' : '';
const mediaPageURL = `/${service}/${serviceId}/${mediaId}${extension}${queryString}`;
describe('Successful render', () => {
describe('200 status code', () => {
beforeEach(() => {
mockRouteProps({
service,
isAmp,
dataResponse: successDataResponse,
});
});
const configs = {
url: mediaPageURL,
service,
isAmp,
successDataResponse,
};
it('should respond with rendered data', testRenderedData(configs));
});
});
describe('404 status code', () => {
const pageType = MEDIA_PAGE;
beforeEach(() => {
mockRouteProps({
service,
isAmp,
dataResponse: notFoundDataResponse,
pageType,
});
});
it('should respond with a rendered 404', async () => {
const { status, text } = await makeRequest(mediaPageURL);
expect(status).toBe(404);
expect(text).toEqual(
'<!doctype html><html><body><h1>Mock app</h1></body></html>',
);
});
assertNon200ResponseCustomMetrics({
requestUrl: mediaPageURL,
pageType,
statusCode: 404,
});
});
describe('Unknown error within the data fetch, react router or its dependencies', () => {
const pageType = 'liveRadio';
beforeEach(() => {
mockRouteProps({
service,
isAmp,
dataResponse: Error('Error!'),
responseType: 'reject',
pageType,
});
});
it('should respond with a 500', async () => {
const { status, text } = await makeRequest(mediaPageURL);
expect(status).toEqual(500);
expect(text).toEqual('Internal server error');
});
assertNon200ResponseCustomMetrics({
requestUrl: mediaPageURL,
pageType,
});
});
});
};
const testTvPages = ({
platform,
service,
serviceId,
brandEpisode,
mediaId,
queryString = '',
}) => {
describe(`${platform} tv brand page`, () => {
const isAmp = platform === 'amp';
const isApp = platform === 'app';
const extension =
{
amp: '.amp',
app: '.app',
}[platform] || '';
const successDataResponse = {
isAmp,
data: { some: 'data' },
service: 'someService',
status: 200,
};
const notFoundDataResponse = {
isAmp,
data: { some: 'data' },
service: 'someService',
status: 404,
};
const mediaPageURL = `/${service}/${serviceId}/${brandEpisode}/${mediaId}${extension}${queryString}`;
describe('Successful render', () => {
describe('200 status code', () => {
beforeEach(() => {
mockRouteProps({
service,
isAmp,
isApp,
dataResponse: successDataResponse,
});
});
const configs = {
url: mediaPageURL,
service,
isAmp,
isApp,
successDataResponse,
};
it('should respond with rendered data', testRenderedData(configs));
});
});
describe('404 status code', () => {
const pageType = 'On Demand TV Brand';
beforeEach(() => {
mockRouteProps({
service,
isAmp,
isApp,
dataResponse: notFoundDataResponse,
pageType,
});
});
it('should respond with a rendered 404', async () => {
const { status, text } = await makeRequest(mediaPageURL);
expect(status).toBe(404);
expect(text).toEqual(
'<!doctype html><html><body><h1>Mock app</h1></body></html>',
);
});
assertNon200ResponseCustomMetrics({
requestUrl: mediaPageURL,
pageType,
statusCode: 404,
});
});
describe('Unknown error within the data fetch, react router or its dependencies', () => {
const pageType = 'onDemandTVBrand';
beforeEach(() => {
mockRouteProps({
service,
isAmp,
isApp,
dataResponse: Error('Error!'),
responseType: 'reject',
pageType,
});
});
it('should respond with a 500', async () => {
const { status, text } = await makeRequest(mediaPageURL);
expect(status).toEqual(500);
expect(text).toEqual('Internal server error');
});
assertNon200ResponseCustomMetrics({
requestUrl: mediaPageURL,
pageType,
});
});
});
};
const testOnDemandTvEpisodePages = ({
platform,
service,
serviceId,
brandEpisode,
mediaId,
queryString = '',
}) => {
describe(`${platform} tv episode page`, () => {
const isAmp = platform === 'amp';
const isApp = platform === 'app';
const extension =
{
amp: '.amp',
app: '.app',
}[platform] || '';
const successDataResponse = {
isAmp,
data: { some: 'data' },
service: 'someService',
status: 200,
};
const notFoundDataResponse = {
isAmp,
data: { some: 'data' },
service: 'someService',
status: 404,
};
const mediaPageURL = `/${service}/${serviceId}/${brandEpisode}/${mediaId}${extension}${queryString}`;
describe('Successful render', () => {
describe('200 status code', () => {
beforeEach(() => {
mockRouteProps({
service,
isAmp,
isApp,
dataResponse: successDataResponse,
});
});
const configs = {
url: mediaPageURL,
service,
isAmp,
isApp,
successDataResponse,
};
it('should respond with rendered data', testRenderedData(configs));
});
});
describe('404 status code', () => {
const pageType = 'On Demand TV Episode';
beforeEach(() => {
mockRouteProps({
service,
isAmp,
isApp,
dataResponse: notFoundDataResponse,
pageType,
});
});
it('should respond with a rendered 404', async () => {
const { status, text } = await makeRequest(`/${service}`);
expect(status).toBe(404);
expect(text).toEqual(
'<!doctype html><html><body><h1>Mock app</h1></body></html>',
);
});
assertNon200ResponseCustomMetrics({
requestUrl: mediaPageURL,
pageType,
statusCode: 404,
});
});
describe('Unknown error within the data fetch, react router or its dependencies', () => {
const pageType = 'onDemandTVEpisode';
beforeEach(() => {
mockRouteProps({
service,
isAmp,
isApp,
dataResponse: Error('Error!'),
responseType: 'reject',
pageType,
});
});
it('should respond with a 500', async () => {
const { status, text } = await makeRequest(mediaPageURL);
expect(status).toEqual(500);
expect(text).toEqual('Internal server error');
});
assertNon200ResponseCustomMetrics({
requestUrl: mediaPageURL,
pageType,
});
});
});
};
describe('Server', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('/status', () => {
it('should respond with a 200', async () => {
const { statusCode, text } = await makeRequest('/status');
expect(statusCode).toBe(200);
expect(text).toEqual('Ok');
});
});
describe('Service workers', () => {
it('should serve a file for existing service workers', async () => {
await makeRequest('/news/articles/sw.js');
expect(sendFileSpy.mock.calls[0][0]).toEqual(
path.join(__dirname, '/public/sw.js'),
);
});
it('should not serve a file for non-existing service workers', async () => {
const { statusCode } = await makeRequest('/some-service/articles/sw.js');
expect(sendFileSpy.mock.calls.length).toEqual(0);
expect(statusCode).toEqual(500);
});
it('should serve the sw.js with cache control information', async () => {
const { header } = await makeRequest('/pidgin/sw.js');
expect(header['cache-control']).toBe(
'public, stale-if-error=6000, stale-while-revalidate=600, max-age=300',
);
});
});
describe('Manifest json', () => {
it('should serve a file for valid service paths', async () => {
await makeRequest('/news/articles/manifest.json');
expect(sendFileSpy.mock.calls[0][0]).toEqual(
path.join(__dirname, '/public/news/manifest.json'),
);
});
it('should not serve a manifest file for non-existing services', async () => {
const { statusCode } = await makeRequest('/some-service/manifest.json');
expect(sendFileSpy.mock.calls.length).toEqual(0);
expect(statusCode).toEqual(500);
});
it('should serve a response cache control of 7 days', async () => {
const { header } = await makeRequest('/news/articles/manifest.json');
expect(header['cache-control']).toBe(
'public, stale-if-error=1209600, stale-while-revalidate=1209600, max-age=604800',
);
});
});
describe('Most Read json', () => {
it('should serve a file for valid service paths with variants', async () => {
const { body } = await makeRequest('/zhongwen/mostread/trad.json');
expect(body.data).toEqual(
expect.objectContaining({ items: expect.any(Object) }),
);
});
it('should serve a file for valid service paths without variants', async () => {
const { body } = await makeRequest('/pidgin/mostread.json');
expect(body.data).toEqual(
expect.objectContaining({ items: expect.any(Object) }),
);
});
it('should respond with a 500 for non-existing services', async () => {
const { statusCode } = await makeRequest('/some-service/mostread.json');
expect(statusCode).toEqual(500);
});
});
describe('STY secondary column json', () => {
it('should serve a file for valid service paths with variants', async () => {
const { body } = await makeRequest(
'/zhongwen/sty-secondary-column/trad.json',
);
expect(body).toEqual(
expect.objectContaining({
topStories: expect.any(Object),
features: expect.any(Object),
}),
);
});
it('should serve a file for valid service paths without variants', async () => {
const { body } = await makeRequest('/mundo/sty-secondary-column.json');
expect(body).toEqual(
expect.objectContaining({
topStories: expect.any(Object),
features: expect.any(Object),
}),
);
});
it('should respond with a 500 for non-existing services', async () => {
const { statusCode } = await makeRequest(
'/some-service/sty-secondary-column.json',
);
expect(statusCode).toEqual(500);
});
});
describe('Recommendations json', () => {
// This is being skipped due to variants not needing recommendations
it.skip('should serve a file for valid service paths with variants', async () => {
const { body } = await makeRequest(
'/zhongwen/uk-23283128/recommendations/trad.json',
);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({
headlines: expect.any(Object),
}),
]),
);
});
it('should serve a file for valid service paths without variants', async () => {
const { body } = await makeRequest(
'/mundo/23263889/recommendations.json',
);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({
headlines: expect.any(Object),
}),
]),
);
});
it('should respond with a 500 for non-existing services', async () => {
const { statusCode } = await makeRequest(
'/some-service/recommendations.json',
);
expect(statusCode).toEqual(500);
});
});
describe('Data', () => {
describe('for articles', () => {
it('should respond with JSON', async () => {
const { body } = await makeRequest('/news/articles/c0g992jmmkko.json');
expect(body).toEqual(
expect.objectContaining({ data: expect.any(Object) }),
);
});
describe('with non-existent data', () => {
it('should respond with a 404', async () => {
const { statusCode } = await makeRequest(
'/news/articles/cERROR00025o.json',
);
expect(statusCode).toEqual(404);
});
});
describe('Trailing slash redirects', () => {
it('should respond with a 301', async () => {
const { statusCode } = await makeRequest(
'/news/articles/c6v11qzyv8po/',
);
expect(statusCode).toEqual(301);
});
});
});
describe('for home pages', () => {
it('should respond with JSON', async () => {
const { body } = await makeRequest('/serbian/cyr.json');
expect(body.data.pageType).toEqual('home');
});
describe('with non-existent data', () => {
beforeEach(() => {
const notFoundDataResponse = {
isAmp: false,
data: { some: 'data' },
service: 'someService',
status: 404,
};
mockRouteProps({
service: 'someService',
isAmp: false,
dataResponse: notFoundDataResponse,
});
});
it('should respond with a 404', async () => {
const { statusCode } = await makeRequest('/ERROR.json');
expect(statusCode).toEqual(404);
});
});
});
describe('for radio page - live radio', () => {
it('should respond with JSON', async () => {
const { body } = await makeRequest(
'/korean/bbc_korean_radio/liveradio.json',
);
expect(body).toEqual(
expect.objectContaining({ content: expect.any(Object) }),
);
});
describe('with non-existent data', () => {
it('should respond with a 404', async () => {
const { statusCode } = await makeRequest(
'/korean/bbc_korean_radio/ERROR.json',
);
expect(statusCode).toEqual(404);
});
});
});
describe('for radio schedules', () => {
it('should respond with JSON for service with radio schedule', async () => {
const { body } = await makeRequest(
'/arabic/bbc_arabic_radio/schedule.json',
);
expect(body).toEqual(
expect.objectContaining({ schedules: expect.any(Object) }),
);
});
it('should respond with 404 for service without radio schedule', async () => {
const { statusCode } = await makeRequest(
'/pidgin/bbc_pidgin_radio/schedule.json',
);
expect(statusCode).toEqual(404);
});
it('should respond with 404 for invalid service paths', async () => {
const { statusCode } = await makeRequest(
'/arabic/bbc_pidgin_radio/schedule.json',
);
expect(statusCode).toEqual(404);
});
});
describe('for tv brand page', () => {
it('should respond with JSON', async () => {
const { body } = await makeRequest(
'/pashto/bbc_pashto_tv/tv_programmes/w13xttn4.json',
);
expect(body).toEqual(
expect.objectContaining({ content: expect.any(Object) }),
);
});
describe('with non-existent data', () => {
it('should respond with a 404', async () => {
const { statusCode } = await makeRequest(
'/pashto/bbc_pashto_radio/ERROR.json',
);
expect(statusCode).toEqual(404);
});
});
});
describe('for cps asset pages', () => {
it('should respond with JSON', async () => {
const { body } = await makeRequest('/pidgin/tori-49450859.json');
expect(body.data.article).toEqual(
expect.objectContaining({ content: expect.any(Object) }),
);
});
describe('with non-existent data', () => {
it('should respond with a 404', async () => {
const { statusCode } = await makeRequest(
'/pidgin/tori-00000000.json',
);
expect(statusCode).toEqual(404);
});
});
});
describe('for legacy asset pages', () => {
it('should respond with JSON', async () => {
const { body } = await makeRequest(
'/hausa/multimedia/2012/07/120712_click.json',
);
expect(body.data.article).toEqual(
expect.objectContaining({ content: expect.any(Object) }),
);
});
describe('with non-existent data', () => {
it('should respond with a 404', async () => {
const { statusCode } = await makeRequest(
'/hausa/multimedia/2012/07/120712_non-existent.json',
);
expect(statusCode).toEqual(404);
});
});
});
});
testFrontPages({ platform: 'canonical', service: 'igbo' });
testFrontPages({
platform: 'canonical',
service: 'igbo',
queryString: QUERY_STRING,
});
testFrontPages({ platform: 'amp', service: 'igbo' });
testFrontPages({
platform: 'amp',
service: 'igbo',
queryString: QUERY_STRING,
});
testFrontPages({
platform: 'canonical',
service: 'ukchina',
variant: 'simp',
});
testFrontPages({
platform: 'canonical',
service: 'ukchina',
variant: 'simp',
queryString: QUERY_STRING,
});
testFrontPages({ platform: 'amp', service: 'serbian', variant: 'lat' });
testFrontPages({
platform: 'amp',
service: 'serbian',
variant: 'lat',
queryString: QUERY_STRING,
});
testArticles({ platform: 'amp', service: 'news' });
testArticles({ platform: 'amp', service: 'news', queryString: QUERY_STRING });
testArticles({ platform: 'canonical', service: 'news' });
testArticles({
platform: 'canonical',
service: 'news',
queryString: QUERY_STRING,
});
testArticles({ platform: 'amp', service: 'zhongwen', variant: 'trad' });
testArticles({
platform: 'amp',
service: 'zhongwen',
variant: 'trad',
queryString: QUERY_STRING,
});
testArticles({ platform: 'canonical', service: 'zhongwen', variant: 'simp' });
testArticles({
platform: 'canonical',
service: 'zhongwen',
variant: 'simp',
queryString: QUERY_STRING,
});
testArticles({ platform: 'app', service: 'yoruba' });
testArticles({
platform: 'amp',
service: 'yoruba',
queryString: QUERY_STRING,
});
testMediaPages({
platform: 'amp',
service: 'korean',
serviceId: 'bbc_korean_radio',
mediaId: 'liveradio',
});
testMediaPages({
platform: 'amp',
service: 'korean',
serviceId: 'bbc_korean_radio',
mediaId: 'liveradio',
queryString: QUERY_STRING,
});
testMediaPages({
platform: 'canonical',
service: 'korean',
serviceId: 'bbc_korean_radio',
mediaId: 'liveradio',
});
testMediaPages({
platform: 'canonical',
service: 'korean',
serviceId: 'bbc_korean_radio',
mediaId: 'liveradio',
queryString: QUERY_STRING,
});
testTvPages({
platform: 'amp',
service: 'pashto',
serviceId: 'bbc_pashto_tv',
brandEpisode: 'tv_programmes',
mediaId: 'p0340yr4',
});
testTvPages({
platform: 'amp',
service: 'pashto',
serviceId: 'bbc_pashto_tv',
brandEpisode: 'tv_programmes',
mediaId: 'p0340yr4',
queryString: QUERY_STRING,
});
testTvPages({
platform: 'canonical',
service: 'pashto',
serviceId: 'bbc_pashto_tv',
brandEpisode: 'tv_programmes',
mediaId: 'p0340yr4',
});
testTvPages({
platform: 'canonical',
service: 'pashto',
serviceId: 'bbc_pashto_tv',
brandEpisode: 'tv_programmes',
mediaId: 'p0340yr4',
queryString: QUERY_STRING,
});
testOnDemandTvEpisodePages({
platform: 'amp',
service: 'pashto',
serviceId: 'bbc_pashto_tv',
brandEpisode: 'tv',
mediaId: 'w172xcldhhrhmcf',
});
testOnDemandTvEpisodePages({
platform: 'canonical',
service: 'pashto',
serviceId: 'bbc_pashto_tv',
mediaId: 'w172xcldhhrhmcf',
});
testAssetPages({
platform: 'amp',
service: 'pidgin',
assetUri: 'tori-49450859',
});
testAssetPages({
platform: 'amp',
service: 'pidgin',
assetUri: 'tori-49450859',
queryString: QUERY_STRING,
});
testAssetPages({
platform: 'canonical',
service: 'pidgin',
assetUri: 'tori-49450859',
});
testAssetPages({
platform: 'canonical',
service: 'pidgin',
assetUri: 'tori-49450859',
queryString: QUERY_STRING,
});
testAssetPages({
platform: 'amp',
service: 'serbian',
assetUri: 'srbija-49427344',
variant: 'cyr',
});
testAssetPages({
platform: 'canonical',
service: 'serbian',
assetUri: 'srbija-49427344',
variant: 'cyr',
queryString: QUERY_STRING,
});
// Legacy asset pages
testAssetPages({
platform: 'amp',
service: 'hausa',
assetUri: 'multimedia/2012/07/120712_click',
});
testAssetPages({
platform: 'amp',
service: 'hausa',
assetUri: 'multimedia/2012/07/120712_click',
queryString: QUERY_STRING,
});
testAssetPages({
platform: 'canonical',
service: 'hausa',
assetUri: 'multimedia/2012/07/120712_click',
});
testAssetPages({
platform: 'canonical',
service: 'hausa',
assetUri: 'multimedia/2012/07/120712_click',
queryString: QUERY_STRING,
});
describe('Unknown routes', () => {
const service = 'igbo';
const isAmp = false;
const dataResponse = {
isAmp,
data: { some: 'data' },
service: 'news',
status: 404,
};
describe('404 status code', () => {
beforeEach(() => {
mockRouteProps({
service,
isAmp,
dataResponse,
});
});
it('should respond with rendered data', async () => {
const { text, status } = await makeRequest(`/${service}/foobar`);
expect(status).toBe(404);
expect(reactDomServer.renderToString).toHaveBeenCalled();
expect(reactDomServer.renderToStaticMarkup).toHaveBeenCalledWith(
<Document
app={{
css: '',
ids: [],
html: '<h1>Mock app</h1>',
}}
data={dataResponse}
helmet={{ head: 'tags' }}
isAmp={isAmp}
legacyScripts="__mock_script_elements__"
modernScripts="__mock_script_elements__"
links="__mock_link_elements__"
/>,
);
expect(renderDocumentSpy).toHaveBeenCalled();
expect(text).toEqual(
'<!doctype html><html><body><h1>Mock app</h1></body></html>',
);
});
});
});
});
describe('Server HTTP Headers - Status Endpoint', () => {
let statusRequest;
beforeAll(async () => {
statusRequest = await request(server).get('/status');
});
it(`should not contain 'x-powered-by'`, () => {
const headerKeys = Object.keys(statusRequest.headers);
expect(headerKeys).not.toContain('x-powered-by');
});
it(`should have 'x-frame-options' set to 'DENY'`, () => {
validateHttpHeader(statusRequest.headers, 'x-frame-options', 'DENY');
});
it(`should have X-DNS-Prefetch-Control set to 'off' `, () => {
validateHttpHeader(statusRequest.headers, 'x-dns-prefetch-control', 'off');
});
it(`should have Strict-Transport-Security set to 'max-age=15552000; includeSubDomains' `, () => {
validateHttpHeader(
statusRequest.headers,
'strict-transport-security',
'max-age=15552000; includeSubDomains',
);
});
it(`should have X-Download-Options set to 'noopen' `, () => {
validateHttpHeader(statusRequest.headers, 'x-download-options', 'noopen');
});
it(`should have X-Content-Type-Options set to 'nosniff' `, () => {
validateHttpHeader(
statusRequest.headers,
'x-content-type-options',
'nosniff',
);
});
it(`should have X-XSS-Protection set to '1; mode=block' `, () => {
validateHttpHeader(statusRequest.headers, 'x-xss-protection', '0');
});
});
describe('Server HTTP Headers - Page Endpoints', () => {
beforeEach(() => {
jest.clearAllMocks();
});
const successDataResponse = {
isAmp: false,
data: { some: 'data' },
service: 'someService',
status: 200,
};
it(`should set a cache-control header`, async () => {
const { header } = await makeRequest('/mundo');
expect(header['cache-control']).toBe(
'public, stale-if-error=300, stale-while-revalidate=120, max-age=30',
);
});
it(`should set a Referrer-Policy header`, async () => {
const { header } = await makeRequest('/mundo');
expect(header['referrer-policy']).toBe('no-referrer-when-downgrade');
});
it(`should add mvt experiment header names to vary if they are enabled`, async () => {
mockRouteProps({
dataResponse: successDataResponse,
});
getMvtVaryHeaders.mockReturnValue('mvt-simorgh_dark_mode');
const { header } = await makeRequest('/mundo/c0000000001o');
expect(header.vary).toBe(
'X-Country, mvt-simorgh_dark_mode, Accept-Encoding',
);
});
it(`should not add mvt experiment header names to vary if they are not enabled`, async () => {
mockRouteProps({
dataResponse: successDataResponse,
});
getMvtVaryHeaders.mockReturnValue('');
const { header } = await makeRequest('/mundo/articles/c0000000001o');
expect(header.vary).toBe('X-Country, Accept-Encoding');
});
it(`should not add mvt experiment header names to vary if on AMP`, async () => {
mockRouteProps({
dataResponse: successDataResponse,
isAmp: true,
});
getMvtVaryHeaders.mockReturnValue('mvt-simorgh_dark_mode');
const { header } = await makeRequest('/mundo/articles/c0000000001o');
expect(header.vary).toBe('X-Country, Accept-Encoding');
});
it(`should set isUK value to true when 'x-bbc-edge-isuk' is set to 'yes'`, async () => {
mockRouteProps({
dataResponse: successDataResponse,
isAmp: true,
});
await makeRequest('/mundo/articles/c0000000001o', {
'x-bbc-edge-isuk': 'yes',
});
expect(renderDocumentSpy).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ isUK: true }),
}),
);
});
it(`should set isUK value to false when 'x-bbc-edge-isuk' is NOT 'yes'`, async () => {
mockRouteProps({
dataResponse: successDataResponse,
isAmp: true,
});
getMvtVaryHeaders.mockReturnValue('mvt-simorgh_dark_mode');
await makeRequest('/mundo/articles/c0000000001o', {
'x-bbc-edge-isuk': 'no',
});
expect(renderDocumentSpy).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ isUK: false }),
}),
);
});
it(`should set isUK value to true when 'x-country' is set to 'gb' and 'x-bbc-edge-isuk' is not available`, async () => {
mockRouteProps({
dataResponse: successDataResponse,
isAmp: true,
});
getMvtVaryHeaders.mockReturnValue('mvt-simorgh_dark_mode');
await makeRequest('/mundo/articles/c0000000001o', {
'x-country': 'gb',
});
expect(renderDocumentSpy).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ isUK: true }),
}),
);
});
it(`should set isUK value to null when 'x-country' and 'x-bbc-edge-isuk' is not available`, async () => {
mockRouteProps({
dataResponse: successDataResponse,
isAmp: true,
});
getMvtVaryHeaders.mockReturnValue('mvt-simorgh_dark_mode');
await makeRequest('/mundo/articles/c0000000001o', {});
expect(renderDocumentSpy).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ isUK: null }),
}),
);
});
});
describe('Routing Information Logging', () => {
const service = 'igbo';
const isAmp = false;
const url = `/${service}`;
const dataResponse = {
isAmp,
pageData: {
metadata: {
type: 'Page Type from Data',
},
},
service,
status: 200,
};
it(`on 404 response should log a warning`, async () => {
const pageType = 'Matching Page Type from Route';
const status = 404;
mockRouteProps({
service,
isAmp,
dataResponse: { ...dataResponse, status },
pageType,
});
await makeRequest(url);
expect(loggerMock.warn).toHaveBeenCalledWith(ROUTING_INFORMATION, {
url,
status,
pageType,
});
});
it(`on 500 response should log an error`, async () => {
const pageType = 'Matching Page Type from Route';
const status = 500;
mockRouteProps({
service,
isAmp,
dataResponse: { ...dataResponse, status },
pageType,
});
await makeRequest(url);
expect(loggerMock.error).toHaveBeenCalledWith(ROUTING_INFORMATION, {
url,
status,
pageType,
});
});
it(`on 200 response should log page type derived from response data at debug level`, async () => {
mockRouteProps({
service,
isAmp,
dataResponse,
});
await makeRequest(url);
expect(loggerMock.debug).toHaveBeenCalledWith(ROUTING_INFORMATION, {
url,
status: 200,
pageType: 'Page Type from Data',
});
});
});
describe('Exclusion of sensitive HTTP headers from logs', () => {
const SAFE_HEADER = 'x-safe-header';
const SENSITIVE_HEADER = 'x-sensitive-header';
const act = () =>
request(server)
.get('/pidgin')
.set(SAFE_HEADER, 'test')
.set(SENSITIVE_HEADER, 'test');
const assertHeaderWasLogged = (logger, logCategory, header) => {
expect(logger).toHaveBeenCalledWith(
logCategory,
expect.objectContaining({
headers: expect.objectContaining({
[header]: 'test',
}),
}),
);
};
const assertHeaderWasNotLogged = (logger, logCategory, header) => {
expect(logger).toHaveBeenCalledWith(
logCategory,
expect.objectContaining({
headers: expect.not.objectContaining({
[header]: 'test',
}),
}),
);
};
beforeEach(() => {
jest.clearAllMocks();
process.env.SENSITIVE_HTTP_HEADERS = 'x-sensitive-header,x-another-one';
});
afterEach(() => {
jest.clearAllMocks();
delete process.env.SENSITIVE_HTTP_HEADERS;
});
it(`when the environment variable isn't set`, async () => {
delete process.env.SENSITIVE_HTTP_HEADERS;
await act();
assertHeaderWasLogged(
loggerMock.debug,
SERVER_SIDE_RENDER_REQUEST_RECEIVED,
SAFE_HEADER,
);
assertHeaderWasLogged(
loggerMock.debug,
SERVER_SIDE_RENDER_REQUEST_RECEIVED,
SENSITIVE_HEADER,
);
});
it(`when simorgh responds successfully`, async () => {
await act();
assertHeaderWasLogged(
loggerMock.debug,
SERVER_SIDE_RENDER_REQUEST_RECEIVED,
SAFE_HEADER,
);
assertHeaderWasNotLogged(
loggerMock.debug,
SERVER_SIDE_RENDER_REQUEST_RECEIVED,
SENSITIVE_HEADER,
);
});
it(`when simorgh fails due to a getInitialData error`, async () => {
mockRouteProps({
dataResponse: Error('Oh no'),
});
await act();
assertHeaderWasLogged(
loggerMock.debug,
SERVER_SIDE_RENDER_REQUEST_RECEIVED,
SAFE_HEADER,
);
assertHeaderWasNotLogged(
loggerMock.debug,
SERVER_SIDE_RENDER_REQUEST_RECEIVED,
SENSITIVE_HEADER,
);
assertHeaderWasLogged(
loggerMock.error,
SERVER_SIDE_REQUEST_FAILED,
SAFE_HEADER,
);
assertHeaderWasNotLogged(
loggerMock.error,
SERVER_SIDE_REQUEST_FAILED,
SENSITIVE_HEADER,
);
});
it(`when simorgh fails due to a renderDocument error`, async () => {
renderDocument.default.mockImplementation(() => Error('Oh no'));
await act();
assertHeaderWasLogged(
loggerMock.debug,
SERVER_SIDE_RENDER_REQUEST_RECEIVED,
SAFE_HEADER,
);
assertHeaderWasNotLogged(
loggerMock.debug,
SERVER_SIDE_RENDER_REQUEST_RECEIVED,
SENSITIVE_HEADER,
);
assertHeaderWasLogged(
loggerMock.error,
SERVER_SIDE_REQUEST_FAILED,
SAFE_HEADER,
);
assertHeaderWasNotLogged(
loggerMock.error,
SERVER_SIDE_REQUEST_FAILED,
SENSITIVE_HEADER,
);
});
});