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: convert MyInfo hash update to domain method #562

Merged
merged 5 commits into from
Nov 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 7 additions & 35 deletions src/app/controllers/myinfo.server.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,45 +83,17 @@ exports.addMyInfo = (myInfoService) => async (req, res, next) => {

Promise.props(readOnlyHashPromises)
.then((readOnlyHashes) => {
// Add to DB only if uinFin-form combo not already present
let filter = {
uinFin: hashedUinFin,
form: formId,
}
MyInfoHash.findOneAndUpdate(
filter,
{
$set: _.extend(
{
fields: readOnlyHashes,
expireAt: Date.now() + myInfoService.spCookieMaxAge,
},
filter,
),
},
{
upsert: true,
},
(err) => {
if (err) {
res.locals.myInfoError = true
logger.error({
message: 'Error writing to DB',
meta: {
action: 'addMyInfo',
...createReqMeta(req),
formId,
},
error: err,
})
}
return next()
},
return MyInfoHash.updateHashes(
hashedUinFin,
formId,
readOnlyHashes,
myInfoService.spCookieMaxAge,
)
})
.then(() => next())
.catch((error) => {
logger.error({
message: 'Error hashing MyInfo fields',
message: 'Error saving MyInfo hashes',
meta: {
action: 'addMyInfo',
...createReqMeta(req),
Expand Down
35 changes: 30 additions & 5 deletions src/app/models/myinfo_hash.server.model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Model, Mongoose, Schema } from 'mongoose'
import { Mongoose, Schema } from 'mongoose'

import { IMyInfoHashSchema } from '../../types'
import { IHashes, IMyInfoHashModel, IMyInfoHashSchema } from '../../types'

import { FORM_SCHEMA_ID } from './form.server.model'

Expand Down Expand Up @@ -43,18 +43,43 @@ MyInfoHashSchema.index({
})
MyInfoHashSchema.index({ expireAt: 1 }, { expireAfterSeconds: 0 })

MyInfoHashSchema.statics.updateHashes = async function (
this: IMyInfoHashModel,
hashedUinFin: string,
formId: string,
readOnlyHashes: IHashes,
spCookieMaxAge: number,
): Promise<IMyInfoHashSchema | null> {
return this.findOneAndUpdate(
{
uinFin: hashedUinFin,
form: formId,
},
{
$set: {
fields: readOnlyHashes,
expireAt: new Date(Date.now() + spCookieMaxAge),
},
},
{ upsert: true, new: true },
)
}

const compileMyInfoHashModel = (db: Mongoose) =>
db.model<IMyInfoHashSchema>(MYINFO_HASH_SCHEMA_ID, MyInfoHashSchema)
db.model<IMyInfoHashSchema, IMyInfoHashModel>(
MYINFO_HASH_SCHEMA_ID,
MyInfoHashSchema,
)

/**
* Retrieves the MyInfoHash model on the given Mongoose instance. If the model is
* not registered yet, the model will be registered and returned.
* @param db The mongoose instance to retrieve the MyInfoHash model from
* @returns The MyInfoHash model
*/
const getMyInfoHashModel = (db: Mongoose) => {
const getMyInfoHashModel = (db: Mongoose): IMyInfoHashModel => {
try {
return db.model(MYINFO_HASH_SCHEMA_ID) as Model<IMyInfoHashSchema>
return db.model(MYINFO_HASH_SCHEMA_ID) as IMyInfoHashModel
} catch {
return compileMyInfoHashModel(db)
}
Expand Down
19 changes: 15 additions & 4 deletions src/types/myinfo_hash.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Document } from 'mongoose'
import { Document, Model } from 'mongoose'

import { MyInfoAttribute } from './field'
import { IFormSchema } from './form'

type IHashes = {
[key in MyInfoAttribute]: string
}
export type IHashes = Partial<
{
[key in MyInfoAttribute]: string
}
>

interface IMyInfoHash {
uinFin: string
Expand All @@ -17,3 +19,12 @@ interface IMyInfoHash {
}

export interface IMyInfoHashSchema extends IMyInfoHash, Document {}

export interface IMyInfoHashModel extends Model<IMyInfoHashSchema> {
updateHashes: (
hashedUinFin: string,
formId: string,
readOnlyHashes: IHashes,
spCookieMaxAge: number,
) => Promise<IMyInfoHashSchema | null>
}
148 changes: 148 additions & 0 deletions tests/unit/backend/models/myinfo_hash.server.model.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { ObjectId } from 'bson'
import { omit, pick } from 'lodash'
import mongoose from 'mongoose'

import getMyInfoHashModel from 'src/app/models/myinfo_hash.server.model'

import dbHandler from '../helpers/jest-db'

const MyInfoHash = getMyInfoHashModel(mongoose)

const DEFAULT_PARAMS = {
uinFin: 'testUinFin',
form: new ObjectId(),
fields: { name: 'mockHash' },
expireAt: new Date(Date.now()),
created: new Date(Date.now()),
}
const DEFAULT_COOKIE_MAX_AGE = 5

describe('MyInfo Hash Model', () => {
beforeAll(async () => await dbHandler.connect())
beforeEach(async () => await dbHandler.clearDatabase())
afterAll(async () => await dbHandler.closeDatabase())

describe('Schema', () => {
it('should create and save successfully', async () => {
// Act
const actual = await MyInfoHash.create(DEFAULT_PARAMS)

// Assert
// All fields should exist
// Object Id should be defined when successfully saved to MongoDB.
expect(actual._id).toBeDefined()
expect(
pick(actual, ['uinFin', 'form', 'fields', 'created', 'expireAt']),
).toEqual(DEFAULT_PARAMS)
})

it('should throw validation error on missing uinFin', async () => {
// Arrange
const missingParams = omit(DEFAULT_PARAMS, 'uinFin')

// Act
const myInfoHash = new MyInfoHash(missingParams)
const actualPromise = myInfoHash.save()

// Assert
await expect(actualPromise).rejects.toThrowError(
mongoose.Error.ValidationError,
)
})

it('should throw validation error on missing form', async () => {
// Arrange
const missingParams = omit(DEFAULT_PARAMS, 'form')

// Act
const myInfoHash = new MyInfoHash(missingParams)
const actualPromise = myInfoHash.save()

// Assert
await expect(actualPromise).rejects.toThrowError(
mongoose.Error.ValidationError,
)
})

it('should throw validation error on missing fields', async () => {
// Arrange
const missingParams = omit(DEFAULT_PARAMS, 'fields')

// Act
const myInfoHash = new MyInfoHash(missingParams)
const actualPromise = myInfoHash.save()

// Assert
await expect(actualPromise).rejects.toThrowError(
mongoose.Error.ValidationError,
)
})

it('should throw validation error on missing expireAt', async () => {
// Arrange
const missingParams = omit(DEFAULT_PARAMS, 'expireAt')

// Act
const myInfoHash = new MyInfoHash(missingParams)
const actualPromise = myInfoHash.save()

// Assert
await expect(actualPromise).rejects.toThrowError(
mongoose.Error.ValidationError,
)
})
})

describe('Statics', () => {
describe('updateHashes', () => {
it('should create successfully when document does not exist', async () => {
// Should have no documents yet.
await expect(MyInfoHash.countDocuments()).resolves.toEqual(0)

// Act
const actual = await MyInfoHash.updateHashes(
DEFAULT_PARAMS.uinFin,
DEFAULT_PARAMS.form.toHexString(),
DEFAULT_PARAMS.fields,
DEFAULT_COOKIE_MAX_AGE,
)

// Assert
// Should now have one document.
await expect(MyInfoHash.countDocuments()).resolves.toEqual(1)
const found = await MyInfoHash.findOne({})
// Both the returned document and the found document should match
expect(pick(actual, ['uinFin', 'form', 'fields'])).toEqual(
pick(DEFAULT_PARAMS, ['uinFin', 'form', 'fields']),
)
expect(pick(found, ['uinFin', 'form', 'fields'])).toEqual(
pick(DEFAULT_PARAMS, ['uinFin', 'form', 'fields']),
)
})

it('should update successfully when a document already exists', async () => {
// Arrange
// Insert mock document into collection.
await MyInfoHash.create(DEFAULT_PARAMS)
// Should have the added document.
await expect(MyInfoHash.countDocuments()).resolves.toEqual(1)

const mockFields = { sex: 'F' }

// Act
const actual = await MyInfoHash.updateHashes(
DEFAULT_PARAMS.uinFin,
DEFAULT_PARAMS.form.toHexString(),
mockFields,
DEFAULT_COOKIE_MAX_AGE,
)
// Assert
await expect(MyInfoHash.countDocuments()).resolves.toEqual(1)
const found = await MyInfoHash.findOne({})
// Both the returned document and the found document should match
expect(actual!.fields).toEqual(mockFields)
expect(found!.fields).toEqual(mockFields)
})
})
})
})