Skip to content

Commit

Permalink
feat(modal): introduce the backend for announcement modal
Browse files Browse the repository at this point in the history
Allow a new env var - `USER_ANNOUNCEMENT` - to be specified at runtime,
parsed into a simple JSON payload to be at `/api/user/announcement`

- Declare `USER_ANNOUNCEMENT` at config, to be parsed into `userModal`
- Inject `config.userAnnouncement` into UserController via inversify
- Expose this via `/api/user/announcement`
- Provide coverage tests that verify parsing and serving
  • Loading branch information
LoneRifle committed Oct 5, 2020
1 parent ceebf8d commit c0bbdba
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 2 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ After these have been set up, set the environment variables according to the tab
|SENTRY_PROJECT|No|The relevant Sentry project. e.g. `project-prod`|
|SENTRY_URL|No|The Sentry url. e.g. `https://sentry.io/`|
|LOGIN_MESSAGE|No|A text message that will be displayed on the login page as a snackbar.|
|USER_MESSAGE|No|A text message that will be displayed as a banner, once the user has logged in.|
|USER_ANNOUNCEMENT|No|A string of the form `message;title;subtitle;url;image-path` that will be displayed as a modal once to the user on login.|
|CSP_REPORT_URI|No|A URI to report CSP violations to.|
|CSP_ONLY_REPORT_VIOLATIONS|No|Only report CSP violations, do not enforce.|
|CLOUDMERSIVE_KEY|No|API key for access to Cloudmersive.|
Expand Down
2 changes: 2 additions & 0 deletions src/server/api/user/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,6 @@ router.get(

router.get('/message', userController.getUserMessage)

router.get('/announcement', userController.getUserAnnouncement)

export = router
11 changes: 11 additions & 0 deletions src/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import winston, { createLogger, format, transports } from 'winston'
import minimatch from 'minimatch'
import { parse } from 'url'
import generateOTP, { OtpFunction } from './util/otp'
import { parseAnnouncementString } from './util/modal'

// Check environment
export const DEV_ENV: boolean = process.env.NODE_ENV === 'development'
Expand Down Expand Up @@ -138,6 +139,16 @@ export const emailValidator = new minimatch.Minimatch(
)
export const loginMessage = process.env.LOGIN_MESSAGE
export const userMessage = process.env.USER_MESSAGE
/**
* A string that embodies the fields to be used in a modal.
* This is of the following form:
* <message>;<title>;<subtitle>;<link>;<image>
* An example of this is:
* message;title;subtitle;https://go.gov.sg/;/favicon.ico.
*/
export const userAnnouncement = parseAnnouncementString(
process.env.USER_ANNOUNCEMENT,
)
export const s3Bucket = process.env.AWS_S3_BUCKET as string
export const linksToRotate = process.env.ROTATED_LINKS

Expand Down
1 change: 1 addition & 0 deletions src/server/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const DependencyIds = {
urlThreatScanService: Symbol.for('urlThreatScanService'),
urlCheckController: Symbol.for('urlCheckController'),
userMessage: Symbol.for('userMessage'),
userAnnouncement: Symbol.for('userAnnouncement'),
}

export const ERROR_404_PATH = '404.error.ejs'
21 changes: 21 additions & 0 deletions src/server/controllers/UserController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,33 @@ import { StorableUrlState } from '../repositories/enums'

import { logger } from '../config'

interface AnnouncementResponse {
message?: string
title?: string
subtitle?: string
url?: string
image?: string
}

@injectable()
export class UserController implements UserControllerInterface {
private urlManagementService: UrlManagementServiceInterface

private userMessage: string

private userAnnouncement: AnnouncementResponse

public constructor(
@inject(DependencyIds.urlManagementService)
urlManagementService: UrlManagementServiceInterface,
@inject(DependencyIds.userMessage)
userMessage: string,
@inject(DependencyIds.userAnnouncement)
userAnnouncement: AnnouncementResponse,
) {
this.urlManagementService = urlManagementService
this.userMessage = userMessage
this.userAnnouncement = userAnnouncement
}

public createUrl: (
Expand Down Expand Up @@ -218,6 +231,14 @@ export class UserController implements UserControllerInterface {
res.send(this.userMessage)
return
}

public getUserAnnouncement: (
req: Express.Request,
res: Express.Response,
) => Promise<void> = async (_, res) => {
res.send(this.userAnnouncement)
return
}
}

export default UserController
5 changes: 5 additions & 0 deletions src/server/controllers/interfaces/UserControllerInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,9 @@ export interface UserControllerInterface {
): Promise<void>

getUserMessage(req: Express.Request, res: Express.Response): Promise<void>

getUserAnnouncement(
req: Express.Request,
res: Express.Response,
): Promise<void>
}
5 changes: 4 additions & 1 deletion src/server/inversify.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import {
accessEndpoint,
bucketEndpoint,
cloudmersiveKey,
logger,
s3Bucket,
userAnnouncement,
userMessage,
} from './config'

Expand Down Expand Up @@ -71,6 +71,9 @@ function bindIfUnbound<T>(

export default () => {
container.bind(DependencyIds.userMessage).toConstantValue(userMessage)
container
.bind(DependencyIds.userAnnouncement)
.toConstantValue(userAnnouncement)

bindIfUnbound(DependencyIds.urlRepository, UrlRepository)
bindIfUnbound(DependencyIds.urlMapper, UrlMapper)
Expand Down
27 changes: 27 additions & 0 deletions src/server/util/modal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export function parseAnnouncementString(
announcementString?: string,
): {
message?: string
title?: string
subtitle?: string
url?: string
image?: string
} {
if (!announcementString) {
return {
message: undefined,
title: undefined,
subtitle: undefined,
url: undefined,
image: undefined,
}
}
const [message, title, subtitle, url, image] = (
announcementString || ''
).split(';')
return { message, title, subtitle, url, image }
}

export default {
parseAnnouncementString,
}
21 changes: 20 additions & 1 deletion test/server/controllers/UserController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,19 @@ const urlManagementService = {
}

const userMessage = 'The quick brown fox jumps over a lazy dog'
const userAnnouncement = {
title: 'title',
message: 'message',
url: 'https://go.gov.sg',
image: '/favicon.ico',
}

describe('UserController', () => {
const controller = new UserController(urlManagementService, userMessage)
const controller = new UserController(
urlManagementService,
userMessage,
userAnnouncement,
)

describe('createUrl', () => {
it('rejects multiple file uploads', async () => {
Expand Down Expand Up @@ -371,4 +381,13 @@ describe('UserController', () => {
await controller.getUserMessage(req, res)
expect(send).toHaveBeenCalledWith(userMessage)
})

it('sends userAnnouncement', async () => {
const req = createRequestWithUser(undefined)
const res = httpMocks.createResponse()
const send = jest.spyOn(res, 'send')

await controller.getUserAnnouncement(req, res)
expect(send).toHaveBeenCalledWith(userAnnouncement)
})
})
59 changes: 59 additions & 0 deletions test/server/util/modal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { parseAnnouncementString } from '../../../src/server/util/modal'

describe('parseAnnouncementString', () => {
it('has all undefined for undefined', () => {
expect(parseAnnouncementString(undefined)).toStrictEqual({
message: undefined,
title: undefined,
subtitle: undefined,
url: undefined,
image: undefined,
})
})

it('has all undefined message for blank', () => {
expect(parseAnnouncementString('')).toStrictEqual({
message: undefined,
title: undefined,
subtitle: undefined,
url: undefined,
image: undefined,
})
})

it('has only message for message', () => {
expect(parseAnnouncementString('message')).toStrictEqual({
message: 'message',
title: undefined,
subtitle: undefined,
url: undefined,
image: undefined,
})
})

it('has blank url and subtitle if not specified but image specified', () => {
expect(
parseAnnouncementString('message;title;;;/favicon.ico'),
).toStrictEqual({
message: 'message',
title: 'title',
subtitle: '',
url: '',
image: '/favicon.ico',
})
})

it('has fields populated if fields populated', () => {
expect(
parseAnnouncementString(
'message;title;subtitle;https://go.gov.sg/;/favicon.ico',
),
).toStrictEqual({
message: 'message',
title: 'title',
subtitle: 'subtitle',
url: 'https://go.gov.sg/',
image: '/favicon.ico',
})
})
})

0 comments on commit c0bbdba

Please sign in to comment.