Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: migrate frontend routes and google analytics factory to ts #1405

Merged
merged 10 commits into from
Apr 8, 2021
19 changes: 0 additions & 19 deletions src/app/factories/google-analytics.factory.js

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { StatusCodes } from 'http-status-codes'

import expressHandler from 'tests/unit/backend/helpers/jest-express'

import frontendServerController from '../frontend.server.controller'
import * as FrontendServerController from '../frontend.server.controller'

describe('frontend.server.controller', () => {
afterEach(() => jest.clearAllMocks())
Expand All @@ -24,7 +24,7 @@ describe('frontend.server.controller', () => {
}
describe('datalayer', () => {
it('should return the correct response when the request is valid', () => {
frontendServerController.datalayer(mockReq, mockRes)
FrontendServerController.addGoogleAnalyticsData(mockReq, mockRes)
expect(mockRes.send).toHaveBeenCalledWith(
expect.stringContaining("'app_name': 'xyz'"),
)
Expand All @@ -35,20 +35,20 @@ describe('frontend.server.controller', () => {
expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.OK)
})
it('should return BAD_REQUEST if the request is not valid', () => {
frontendServerController.datalayer(mockBadReq, mockRes)
FrontendServerController.addGoogleAnalyticsData(mockBadReq, mockRes)
expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.BAD_REQUEST)
})
})

describe('environment', () => {
it('should return the correct response when the request is valid', () => {
frontendServerController.environment(mockReq, mockRes)
FrontendServerController.addEnvVarData(mockReq, mockRes)
expect(mockRes.send).toHaveBeenCalledWith('efg')
expect(mockRes.type).toHaveBeenCalledWith('text/javascript')
expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.OK)
})
it('should return BAD_REQUEST if the request is not valid', () => {
frontendServerController.environment(mockBadReq, mockRes)
FrontendServerController.addEnvVarData(mockBadReq, mockRes)
expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.BAD_REQUEST)
})
})
Expand All @@ -65,15 +65,15 @@ describe('frontend.server.controller', () => {
'window.location.hash = "#!/formId?fieldId1=abc&fieldId2=<>'"'
// Note this is different from mockReqModified.query.redirectPath as
// there are html-encoded characters
frontendServerController.redirectLayer(mockReqModified, mockRes)
FrontendServerController.generateRedirectUrl(mockReqModified, mockRes)
expect(mockRes.send).toHaveBeenCalledWith(
expect.stringContaining(redirectString),
)
expect(mockRes.type).toHaveBeenCalledWith('text/javascript')
expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.OK)
})
it('should return BAD_REQUEST if the request is not valid', () => {
frontendServerController.redirectLayer(mockBadReq, mockRes)
FrontendServerController.generateRedirectUrl(mockBadReq, mockRes)
expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.BAD_REQUEST)
})
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { StatusCodes } from 'http-status-codes'

import { FeatureNames, RegisteredFeature } from 'src/config/feature-manager'

import expressHandler from 'tests/unit/backend/helpers/jest-express'

import { createGoogleAnalyticsFactory } from '../google-analytics.factory'

describe('google-analytics.factory', () => {
afterEach(() => jest.clearAllMocks())
const mockReq = expressHandler.mockRequest({
others: {
app: {
locals: {
GATrackingID: 'abc',
appName: 'xyz',
environment: 'efg',
},
},
},
})
const mockRes = expressHandler.mockResponse()

it('should call res correctly if google-analytics feature is disabled', () => {
const MOCK_DISABLED_GA_FEATURE: RegisteredFeature<FeatureNames.GoogleAnalytics> = {
isEnabled: false,
}

const GoogleAnalyticsFactory = createGoogleAnalyticsFactory(
MOCK_DISABLED_GA_FEATURE,
)

GoogleAnalyticsFactory.addGoogleAnalyticsData(mockReq, mockRes)

expect(mockRes.type).toHaveBeenCalledWith('text/javascript')
expect(mockRes.sendStatus).toHaveBeenCalledWith(StatusCodes.OK)
expect(mockRes.send).not.toHaveBeenCalled()
})

it('should call res correctly if google-analytics feature is enabled', () => {
const MOCK_ENABLED_GA_FEATURE: RegisteredFeature<FeatureNames.GoogleAnalytics> = {
isEnabled: true,
}

const GoogleAnalyticsFactory = createGoogleAnalyticsFactory(
MOCK_ENABLED_GA_FEATURE,
)

GoogleAnalyticsFactory.addGoogleAnalyticsData(mockReq, mockRes)

expect(mockRes.send).toHaveBeenCalledWith(expect.stringContaining('gtag'))
expect(mockRes.type).toHaveBeenCalledWith('text/javascript')
expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.OK)
})
})
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
'use strict'
import ejs from 'ejs'
import { RequestHandler } from 'express'
import { ParamsDictionary } from 'express-serve-static-core'
import { StatusCodes } from 'http-status-codes'

const ejs = require('ejs')
const { StatusCodes } = require('http-status-codes')
const logger = require('../../config/logger').createLoggerWithLabel(module)
const { createReqMeta } = require('../utils/request')
import featureManager from '../../../config/feature-manager'
import { createLoggerWithLabel } from '../../../config/logger'
import { createReqMeta } from '../../utils/request'

const logger = createLoggerWithLabel(module)

/**
* Google Tag Manager initialisation Javascript code templated
* with environment variables.
* @returns {String} Templated Javascript code for the frontend
* Handler for GET /frontend/datalayer endpoint.
* @param req - Express request object
* @param res - Express response object
* @returns {String} Templated Javascript code for the frontend to initialise Google Tag Manager
*/
module.exports.datalayer = function (req, res) {
export const addGoogleAnalyticsData: RequestHandler<
ParamsDictionary,
string | { message: string }
> = (req, res) => {
const js = `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
Expand Down Expand Up @@ -40,10 +48,15 @@ module.exports.datalayer = function (req, res) {
}

/**
* Custom Javascript code templated with environment variables.
* @returns {String} Templated Javascript code for the frontend
* Handler for GET /frontend/environment endpoint.
* @param req - Express request object
* @param res - Express response object
* @returns {String} Templated Javascript code with environment variables for the frontend
*/
module.exports.environment = function (req, res) {
export const addEnvVarData: RequestHandler<
ParamsDictionary,
{ message: string }
> = (req, res) => {
try {
return res
.type('text/javascript')
Expand All @@ -65,10 +78,15 @@ module.exports.environment = function (req, res) {
}

/**
* Custom Javascript code that redirects to specific form url
* @returns {String} Templated Javascript code for the frontend
* Handler for GET /frontend/redirect endpoint.
* @param req - Express request object
* @param res - Express response object
* @returns {String} Templated Javascript code for the frontend that redirects to specific form url
*/
module.exports.redirectLayer = function (req, res) {
export const generateRedirectUrl: RequestHandler<
ParamsDictionary,
string | { message: string }
> = (req, res) => {
const js = `
// Update hash to match form id
window.location.hash = "#!/<%= redirectPath%>"
Expand All @@ -79,7 +97,6 @@ module.exports.redirectLayer = function (req, res) {
// Prefer to replace just '&' instead of using <%- to output unescaped values into the template
// As this could potentially introduce security vulnerability
// See https://ejs.co/#docs for tags

try {
const ejsRendered = ejs.render(js, req.query).replace(/&amp;/g, '&')
return res.type('text/javascript').status(StatusCodes.OK).send(ejsRendered)
Expand All @@ -97,3 +114,16 @@ module.exports.redirectLayer = function (req, res) {
})
}
}

/**
* Handler for GET /frontend/features endpoint.
* @param req - Express request object
* @param res - Express response object
* @returns {String} Current featureManager states
*/
export const showFeaturesStates: RequestHandler<
unknown,
typeof featureManager.states
> = (req, res) => {
return res.json(featureManager.states)
}
49 changes: 49 additions & 0 deletions src/app/modules/frontend/frontend.server.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { celebrate, Joi, Segments } from 'celebrate'
import { Router } from 'express'

import * as FrontendServerController from './frontend.server.controller'
import { GoogleAnalyticsFactory } from './google-analytics.factory'

export const FrontendRouter = Router()

/**
* Generate the templated Javascript code for the frontend to initialise Google Tag Manager
* Code depends on whether googleAnalyticsFeature.isEnabled
* @route GET /frontend/datalayer
* @return 200 when code generation is successful
* @return 400 when code generation fails
*/
FrontendRouter.get('/datalayer', GoogleAnalyticsFactory.addGoogleAnalyticsData)

/**
* Generate the templated Javascript code with environment variables for the frontend
* @route GET /frontend/environment
* @return 200 when code generation is successful
* @return 400 when code generation fails
*/
FrontendRouter.get('/environment', FrontendServerController.addEnvVarData)

/**
* Generate a json of current activated features
* @route GET /frontend/features
* @return json with featureManager.states
*/
FrontendRouter.get('/features', FrontendServerController.showFeaturesStates)

/**
* Generate the javascript code to redirect to the correct url
* @route GET /frontend/redirect
* @return 200 when redirect code is successful
* @return 400 when redirect code fails
*/
FrontendRouter.get(
'/redirect',
celebrate({
[Segments.QUERY]: {
redirectPath: Joi.string()
.regex(/^[a-fA-F0-9]{24}(\/(preview|template|use-template))?/)
.required(),
},
}),
FrontendServerController.generateRedirectUrl,
)
43 changes: 43 additions & 0 deletions src/app/modules/frontend/google-analytics.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { RequestHandler } from 'express'
import { ParamsDictionary } from 'express-serve-static-core'
import { StatusCodes } from 'http-status-codes'

import FeatureManager, {
FeatureNames,
RegisteredFeature,
} from '../../../config/feature-manager'

import * as FrontendServerController from './frontend.server.controller'

interface IGoogleAnalyticsFactory {
addGoogleAnalyticsData: RequestHandler<
ParamsDictionary,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unknown here as well

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as above, ParamsDictionary is needed

string | { message: string }
>
}

const googleAnalyticsFeature = FeatureManager.get(FeatureNames.GoogleAnalytics)

/**
* Factory function which returns the correct handler
* for /frontend/datalayer endpoint depending on googleAnalyticsFeature.isEnabled
* @param googleAnalyticsFeature
*/
export const createGoogleAnalyticsFactory = (
googleAnalyticsFeature: RegisteredFeature<FeatureNames.GoogleAnalytics>,
): IGoogleAnalyticsFactory => {
if (!googleAnalyticsFeature.isEnabled) {
return {
addGoogleAnalyticsData: (req, res) => {
res.type('text/javascript').sendStatus(StatusCodes.OK)
},
}
}
return {
addGoogleAnalyticsData: FrontendServerController.addGoogleAnalyticsData,
}
}

export const GoogleAnalyticsFactory = createGoogleAnalyticsFactory(
googleAnalyticsFeature,
)
27 changes: 0 additions & 27 deletions src/app/routes/frontend.server.routes.js

This file was deleted.

5 changes: 1 addition & 4 deletions src/app/routes/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
module.exports = [
require('./frontend.server.routes.js'),
require('./public-forms.server.routes.js'),
]
module.exports = [require('./public-forms.server.routes.js')]
2 changes: 2 additions & 0 deletions src/loaders/express/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { BillingRouter } from '../../app/modules/billing/billing.routes'
import { BounceRouter } from '../../app/modules/bounce/bounce.routes'
import { ExamplesRouter } from '../../app/modules/examples/examples.routes'
import { AdminFormsRouter } from '../../app/modules/form/admin-form/admin-form.routes'
import { FrontendRouter } from '../../app/modules/frontend/frontend.server.routes'
import { HomeRouter } from '../../app/modules/home/home.routes'
import { MYINFO_ROUTER_PREFIX } from '../../app/modules/myinfo/myinfo.constants'
import { MyInfoRouter } from '../../app/modules/myinfo/myinfo.routes'
Expand Down Expand Up @@ -149,6 +150,7 @@ const loadExpressApp = async (connection: Connection) => {
})

app.use('/', HomeRouter)
app.use('/frontend', FrontendRouter)
app.use('/auth', AuthRouter)
app.use('/user', UserRouter)
app.use('/emailnotifications', BounceRouter)
Expand Down