diff --git a/src/app/services/myinfo/__tests__/myinfo.factory.spec.ts b/src/app/services/myinfo/__tests__/myinfo.factory.spec.ts new file mode 100644 index 0000000000..2606e7dcdc --- /dev/null +++ b/src/app/services/myinfo/__tests__/myinfo.factory.spec.ts @@ -0,0 +1,121 @@ +import { IPersonBasic, IPersonBasicRequest } from '@opengovsg/myinfo-gov-client' +import { pick } from 'lodash' +import { mocked } from 'ts-jest/utils' + +import config from 'src/config/config' +import { ISpcpMyInfo } from 'src/config/feature-manager' +import { Environment } from 'src/types' + +import { createMyInfoFactory } from '../myinfo.factory' +import * as MyInfoServiceModule from '../myinfo.service' + +import { MOCK_APP_TITLE, MOCK_NODE_ENV } from './myinfo.test.constants' + +jest.mock('../myinfo.service', () => ({ + MyInfoService: jest.fn(), +})) +const MockMyInfoService = mocked(MyInfoServiceModule, true) +jest.mock('src/config/config') +const MockConfig = mocked(config, true) +MockConfig.nodeEnv = MOCK_NODE_ENV as Environment +MockConfig.app = { + title: MOCK_APP_TITLE, + description: '', + appUrl: '', + keywords: '', + images: [''], + twitterImage: '', +} + +describe('myinfo.factory', () => { + it('should return error functions when isEnabled is false', async () => { + const MyInfoFactory = createMyInfoFactory({ + isEnabled: false, + props: {} as ISpcpMyInfo, + }) + const error = new Error( + 'spcp-myinfo is not activated, but a feature-specific function was called.', + ) + const fetchMyInfoPersonDataResult = await MyInfoFactory.fetchMyInfoPersonData( + {} as IPersonBasicRequest, + ) + const prefillMyInfoFieldsResult = MyInfoFactory.prefillMyInfoFields( + {} as IPersonBasic, + [], + ) + const saveMyInfoHashesResult = await MyInfoFactory.saveMyInfoHashes( + '', + '', + [], + ) + const fetchMyInfoHashesResult = await MyInfoFactory.fetchMyInfoHashes( + '', + '', + ) + const checkMyInfoHashesResult = await MyInfoFactory.checkMyInfoHashes( + [], + {}, + ) + expect(fetchMyInfoPersonDataResult._unsafeUnwrapErr()).toEqual(error) + expect(prefillMyInfoFieldsResult._unsafeUnwrapErr()).toEqual(error) + expect(saveMyInfoHashesResult._unsafeUnwrapErr()).toEqual(error) + expect(fetchMyInfoHashesResult._unsafeUnwrapErr()).toEqual(error) + expect(checkMyInfoHashesResult._unsafeUnwrapErr()).toEqual(error) + }) + + it('should return error functions when props is falsey', async () => { + const MyInfoFactory = createMyInfoFactory({ + isEnabled: true, + props: undefined, + }) + const error = new Error( + 'spcp-myinfo is not activated, but a feature-specific function was called.', + ) + const fetchMyInfoPersonDataResult = await MyInfoFactory.fetchMyInfoPersonData( + {} as IPersonBasicRequest, + ) + const prefillMyInfoFieldsResult = MyInfoFactory.prefillMyInfoFields( + {} as IPersonBasic, + [], + ) + const saveMyInfoHashesResult = await MyInfoFactory.saveMyInfoHashes( + '', + '', + [], + ) + const fetchMyInfoHashesResult = await MyInfoFactory.fetchMyInfoHashes( + '', + '', + ) + const checkMyInfoHashesResult = await MyInfoFactory.checkMyInfoHashes( + [], + {}, + ) + expect(fetchMyInfoPersonDataResult._unsafeUnwrapErr()).toEqual(error) + expect(prefillMyInfoFieldsResult._unsafeUnwrapErr()).toEqual(error) + expect(saveMyInfoHashesResult._unsafeUnwrapErr()).toEqual(error) + expect(fetchMyInfoHashesResult._unsafeUnwrapErr()).toEqual(error) + expect(checkMyInfoHashesResult._unsafeUnwrapErr()).toEqual(error) + }) + + it('should call the MyInfoService constructor when isEnabled is true and props is truthy', () => { + const mockProps = ({ + myInfoClientMode: 'mock1', + myInfoKeyPath: 'mock2', + spCookieMaxAge: 200, + spEsrvcId: 'mock3', + } as unknown) as ISpcpMyInfo + createMyInfoFactory({ + isEnabled: true, + props: mockProps, + }) + + expect(MockMyInfoService.MyInfoService).toHaveBeenCalledWith({ + myInfoConfig: pick(mockProps, ['myInfoClientMode', 'myInfoKeyPath']), + nodeEnv: MOCK_NODE_ENV, + realm: MOCK_APP_TITLE, + singpassEserviceId: mockProps.spEsrvcId, + spCookieMaxAge: mockProps.spCookieMaxAge, + }) + }) +}) diff --git a/src/app/services/myinfo/__tests__/myinfo.service.spec.ts b/src/app/services/myinfo/__tests__/myinfo.service.spec.ts new file mode 100644 index 0000000000..29fff007dc --- /dev/null +++ b/src/app/services/myinfo/__tests__/myinfo.service.spec.ts @@ -0,0 +1,293 @@ +import { + IPersonBasic, + Mode as MyInfoClientMode, +} from '@opengovsg/myinfo-gov-client' +import bcrypt from 'bcrypt' +import mongoose from 'mongoose' +import { mocked } from 'ts-jest/utils' + +import { ProcessedFieldResponse } from 'src/app/modules/submission/submission.types' +import getMyInfoHashModel from 'src/app/services/myinfo/myinfo_hash.model' +import { MyInfoService } from 'src/app/services/myinfo/myinfo.service' +import { + Environment, + IFieldSchema, + IHashes, + IMyInfoHashSchema, +} from 'src/types' + +import dbHandler from 'tests/unit/backend/helpers/jest-db' + +import { IPossiblyPrefilledField } from '../myinfo.types' + +import { + MOCK_COOKIE_AGE, + MOCK_ESRVC_ID, + MOCK_FETCH_PARAMS, + MOCK_FORM_FIELDS, + MOCK_FORM_ID, + MOCK_HASHES, + MOCK_KEY_PATH, + MOCK_MATCHED_ATTRS, + MOCK_MYINFO_DATA, + MOCK_POPULATED_FORM_FIELDS, + MOCK_REALM, + MOCK_RESPONSES, + MOCK_UINFIN, +} from './myinfo.test.constants' + +const MyInfoHash = getMyInfoHashModel(mongoose) + +const mockGetPersonBasic = jest.fn() +jest.mock('@opengovsg/myinfo-gov-client', () => ({ + MyInfoGovClient: jest.fn().mockImplementation(() => ({ + getPersonBasic: mockGetPersonBasic, + })), + Mode: jest.requireActual('@opengovsg/myinfo-gov-client').Mode, + CATEGORICAL_DATA_DICT: jest.requireActual('@opengovsg/myinfo-gov-client') + .CATEGORICAL_DATA_DICT, + MyInfoSource: jest.requireActual('@opengovsg/myinfo-gov-client').MyInfoSource, +})) + +jest.mock('bcrypt') +const MockBcrypt = mocked(bcrypt, true) + +describe('MyInfoService', () => { + let myInfoService = new MyInfoService({ + myInfoConfig: { + myInfoClientMode: MyInfoClientMode.Staging, + myInfoKeyPath: MOCK_KEY_PATH, + }, + nodeEnv: Environment.Test, + realm: MOCK_REALM, + singpassEserviceId: MOCK_ESRVC_ID, + spCookieMaxAge: MOCK_COOKIE_AGE, + }) + + beforeAll(async () => await dbHandler.connect()) + beforeEach(() => jest.clearAllMocks()) + afterEach(async () => await dbHandler.clearDatabase()) + afterAll(async () => await dbHandler.closeDatabase()) + + describe('class constructor', () => { + it('should instantiate without errors', () => { + expect(myInfoService).toBeTruthy() + }) + }) + + describe('fetchMyInfoPersonData', () => { + beforeEach(() => { + myInfoService = new MyInfoService({ + myInfoConfig: { + myInfoClientMode: MyInfoClientMode.Staging, + myInfoKeyPath: MOCK_KEY_PATH, + }, + nodeEnv: Environment.Test, + realm: MOCK_REALM, + singpassEserviceId: MOCK_ESRVC_ID, + spCookieMaxAge: MOCK_COOKIE_AGE, + }) + }) + + it('should call MyInfoGovClient.getPersonBasic with the correct parameters', async () => { + mockGetPersonBasic.mockResolvedValueOnce(MOCK_MYINFO_DATA) + const result = await myInfoService.fetchMyInfoPersonData( + MOCK_FETCH_PARAMS, + ) + + expect(mockGetPersonBasic).toHaveBeenCalledWith(MOCK_FETCH_PARAMS) + expect(result._unsafeUnwrap()).toEqual(MOCK_MYINFO_DATA) + }) + + it('should throw FetchMyInfoError when getPersonBasic fails once', async () => { + mockGetPersonBasic.mockRejectedValueOnce(new Error()) + const result = await myInfoService.fetchMyInfoPersonData( + MOCK_FETCH_PARAMS, + ) + + expect(mockGetPersonBasic).toHaveBeenCalledWith(MOCK_FETCH_PARAMS) + expect(result._unsafeUnwrapErr()).toEqual( + new Error('Error while requesting MyInfo data'), + ) + }) + + it('should throw CircuitBreakerError when getPersonBasic fails 5 times', async () => { + mockGetPersonBasic.mockRejectedValue(new Error()) + for (let i = 0; i < 5; i++) { + await myInfoService.fetchMyInfoPersonData(MOCK_FETCH_PARAMS) + } + const result = await myInfoService.fetchMyInfoPersonData( + MOCK_FETCH_PARAMS, + ) + + // Last function call doesn't count as breaker is open, so expect 5 calls + expect(mockGetPersonBasic).toHaveBeenCalledTimes(5) + expect(result._unsafeUnwrapErr()).toEqual( + new Error('Circuit breaker tripped'), + ) + }) + }) + + describe('prefillMyInfoFields', () => { + it('should prefill fields correctly', () => { + const result = myInfoService.prefillMyInfoFields( + (MOCK_MYINFO_DATA as unknown) as IPersonBasic, + MOCK_FORM_FIELDS as IFieldSchema[], + ) + expect(result._unsafeUnwrap()).toEqual(MOCK_POPULATED_FORM_FIELDS) + }) + }) + + describe('saveMyInfoHashes', () => { + it('should call updateHashes with the correct parameters', async () => { + const mockReturnValue = { mock: 'value' } + const mockUpdateHashes = jest + .spyOn(MyInfoHash, 'updateHashes') + .mockResolvedValueOnce( + (mockReturnValue as unknown) as IMyInfoHashSchema, + ) + MockBcrypt.hash.mockImplementation((v) => Promise.resolve(v)) + const expectedHashes = {} as Record + MOCK_POPULATED_FORM_FIELDS.forEach((field) => { + if (field.disabled && field.myInfo?.attr) { + expectedHashes[field.myInfo.attr] = field.fieldValue + } + }) + + const result = await myInfoService.saveMyInfoHashes( + MOCK_UINFIN, + MOCK_FORM_ID, + MOCK_POPULATED_FORM_FIELDS as IPossiblyPrefilledField[], + ) + + expect(mockUpdateHashes).toHaveBeenCalledWith( + MOCK_UINFIN, + MOCK_FORM_ID, + expectedHashes, + MOCK_COOKIE_AGE, + ) + expect(result._unsafeUnwrap()).toEqual(mockReturnValue) + }) + + it('should throw HashingError when hashing fails', async () => { + MockBcrypt.hash.mockRejectedValue('') + + const result = await myInfoService.saveMyInfoHashes( + MOCK_UINFIN, + MOCK_FORM_ID, + MOCK_POPULATED_FORM_FIELDS as IPossiblyPrefilledField[], + ) + + expect(result._unsafeUnwrapErr()).toEqual( + new Error('Error occurred while hashing data'), + ) + }) + + it('should throw DatabaseError when database update fails', async () => { + MockBcrypt.hash.mockImplementation((v) => Promise.resolve(v)) + jest.spyOn(MyInfoHash, 'updateHashes').mockRejectedValueOnce('') + const result = await myInfoService.saveMyInfoHashes( + MOCK_UINFIN, + MOCK_FORM_ID, + MOCK_POPULATED_FORM_FIELDS as IPossiblyPrefilledField[], + ) + expect(result._unsafeUnwrapErr()).toEqual( + new Error('Failed to save MyInfo hashes to database'), + ) + }) + }) + + describe('fetchMyInfoHashes', () => { + it('should return the result of MyInfoHash.findHashes when it is non-null', async () => { + const mockReturnValue = { name: 'mockReturnValue' } + const mockFindHashes = jest + .spyOn(MyInfoHash, 'findHashes') + .mockResolvedValue(mockReturnValue) + + const result = await myInfoService.fetchMyInfoHashes( + MOCK_UINFIN, + MOCK_FORM_ID, + ) + + expect(mockFindHashes).toHaveBeenCalledWith(MOCK_UINFIN, MOCK_FORM_ID) + expect(result._unsafeUnwrap()).toEqual(mockReturnValue) + }) + + it('should throw MissingHashError when the result of MyInfoHash.findHashes is null', async () => { + const mockFindHashes = jest + .spyOn(MyInfoHash, 'findHashes') + .mockResolvedValue(null) + + const result = await myInfoService.fetchMyInfoHashes( + MOCK_UINFIN, + MOCK_FORM_ID, + ) + + expect(mockFindHashes).toHaveBeenCalledWith(MOCK_UINFIN, MOCK_FORM_ID) + expect(result._unsafeUnwrapErr()).toEqual( + new Error('Requested hashes not found in database'), + ) + }) + + it('should throw DatabaseError when querying the database fails', async () => { + const mockFindHashes = jest + .spyOn(MyInfoHash, 'findHashes') + .mockRejectedValue('') + + const result = await myInfoService.fetchMyInfoHashes( + MOCK_UINFIN, + MOCK_FORM_ID, + ) + + expect(mockFindHashes).toHaveBeenCalledWith(MOCK_UINFIN, MOCK_FORM_ID) + expect(result._unsafeUnwrapErr()).toEqual( + new Error('Error while fetching MyInfo hashes from database'), + ) + }) + }) + + describe('checkMyInfoHashes', () => { + it('should return the set of hashed attributes when the hashes match', async () => { + MockBcrypt.compare.mockResolvedValue(true) + + const result = await myInfoService.checkMyInfoHashes( + (MOCK_RESPONSES as unknown) as ProcessedFieldResponse[], + MOCK_HASHES as IHashes, + ) + + expect(result._unsafeUnwrap()).toEqual(MOCK_MATCHED_ATTRS) + }) + + it('should return HashingError when hashing fails', async () => { + MockBcrypt.compare.mockRejectedValue('') + + const result = await myInfoService.checkMyInfoHashes( + (MOCK_RESPONSES as unknown) as ProcessedFieldResponse[], + MOCK_HASHES as IHashes, + ) + + expect(result._unsafeUnwrapErr()).toEqual( + new Error('Error occurred while hashing data'), + ) + }) + + it('should return HashDidNotMatchError when the hashes do not match', async () => { + // Return false for the first hash + MockBcrypt.compare.mockImplementation((answer) => { + if (answer === MOCK_RESPONSES[0].answer) { + return Promise.resolve(false) + } + return Promise.resolve(true) + }) + + const result = await myInfoService.checkMyInfoHashes( + (MOCK_RESPONSES as unknown) as ProcessedFieldResponse[], + MOCK_HASHES as IHashes, + ) + + expect(result._unsafeUnwrapErr()).toEqual( + new Error('Responses did not match hashed values'), + ) + }) + }) +}) diff --git a/src/app/services/myinfo/__tests__/myinfo.test.constants.ts b/src/app/services/myinfo/__tests__/myinfo.test.constants.ts new file mode 100644 index 0000000000..0e56b323b0 --- /dev/null +++ b/src/app/services/myinfo/__tests__/myinfo.test.constants.ts @@ -0,0 +1,159 @@ +import { ObjectId } from 'bson' +import { merge, zipWith } from 'lodash' + +export const MOCK_MYINFO_DATA = { + name: { + lastupdated: '2015-06-01', + source: '1', + classification: 'C', + value: 'TAN XIAO HUI', + }, + mobileno: { + code: '65', + prefix: '+', + lastupdated: '2017-12-13', + source: '4', + classification: 'C', + nbr: '97324992', + }, + mailadd: { + country: 'US', + unit: '', + street: '5TH AVENUE', + lastupdated: '2016-03-11', + block: '725', + source: '2', + postal: 'NY 10022', + classification: 'C', + floor: '', + building: 'TRUMP TOWER', + }, + employment: { + lastupdated: '2017-10-11', + source: '2', + classification: 'C', + value: 'ALPHA', + }, +} + +export const MOCK_MYINFO_FORMAT_DATA = { + homeno: { + code: '65', + prefix: '+', + lastupdated: '2017-11-20', + source: '2', + classification: 'C', + nbr: '66132665', + }, + mobileno: { + code: '65', + prefix: '+', + lastupdated: '2017-12-13', + source: '4', + classification: 'C', + nbr: '97324992', + }, + regadd: { + country: 'SG', + unit: '128', + street: 'BEDOK NORTH AVENUE 1', + lastupdated: '2016-03-11', + block: '548', + source: '1', + postal: '460548', + classification: 'C', + floor: '09', + building: '', + }, + mailadd: { + country: 'US', + unit: '', + street: '5TH AVENUE', + lastupdated: '2016-03-11', + block: '725', + source: '2', + postal: 'NY 10022', + classification: 'C', + floor: '', + building: 'TRUMP TOWER', + }, + billadd: { + country: 'SG', + street: 'SERANGOON AVE 3', + lastupdated: '2016-03-11', + block: '329', + source: '1', + postal: '550329', + classification: 'C', + floor: '09', + unit: '360', + building: '', + }, + workpassexpirydate: { + lastupdated: '2018-03-02', + source: '1', + classification: 'C', + value: '2018-12-31', + }, +} + +export const MOCK_FORM_FIELDS = [ + // Some MyInfo fields + { fieldType: 'textfield', isVisible: true, myInfo: { attr: 'name' } }, + { fieldType: 'textfield', isVisible: false, myInfo: { attr: 'mobileno' } }, + { fieldType: 'textfield', isVisible: true, myInfo: { attr: 'homeno' } }, + { fieldType: 'textfield', isVisible: true, myInfo: { attr: 'mailadd' } }, + // Some non-MyInfo fields + { fieldType: 'dropdown' }, + { fieldType: 'textfield' }, +] + +export const MOCK_HASHES = { + name: 'name', + mobileno: 'mobileno', + mailadd: 'mailadd', +} + +// Based on MOCK_FORM_FIELDS and MOCK_HASHES, only expect these attributes to +// be matched based on hashes. mobileno does not match because its isVisible is false, +// and homeno does not match because it does not have a hash. +export const MOCK_MATCHED_ATTRS = new Set(['name', 'mailadd']) + +const populatedValues = [ + { fieldValue: 'TAN XIAO HUI', disabled: true }, + { fieldValue: '+65 97324992', disabled: false }, + { fieldValue: '', disabled: false }, + { + fieldValue: 'TRUMP TOWER, 725 5TH AVENUE, UNITED STATES NY 10022', + disabled: false, + }, + {}, + {}, +] + +export const MOCK_POPULATED_FORM_FIELDS = zipWith( + MOCK_FORM_FIELDS, + populatedValues, + (v1, v2) => merge(v1, v2), +) + +export const MOCK_RESPONSES = zipWith( + MOCK_FORM_FIELDS, + populatedValues, + (v1, v2) => merge(v1, v2, { answer: v2.fieldValue, fieldValue: undefined }), +) + +export const MOCK_COOKIE_AGE = 2000 +export const MOCK_KEY_PATH = + './node_modules/@opengovsg/mockpass/static/certs/key.pem' +export const MOCK_REALM = 'mockRealm' +export const MOCK_ESRVC_ID = 'mockEsrvcId' +export const MOCK_UINFIN = 'uinFin' +export const MOCK_FETCH_PARAMS = { + uinFin: MOCK_UINFIN, + requestedAttributes: ['mockAttr'], + singpassEserviceId: 'mockEsrvcId', +} +export const MOCK_FORM_ID = new ObjectId().toHexString() +export const MOCK_NODE_ENV = 'nodeEnv' +export const MOCK_APP_TITLE = 'appTitle' diff --git a/src/app/services/myinfo/__tests__/myinfo.util.spec.ts b/src/app/services/myinfo/__tests__/myinfo.util.spec.ts new file mode 100644 index 0000000000..72e9f95fb0 --- /dev/null +++ b/src/app/services/myinfo/__tests__/myinfo.util.spec.ts @@ -0,0 +1,167 @@ +import { IPersonBasic } from '@opengovsg/myinfo-gov-client' +import { cloneDeep } from 'lodash' + +import { MyInfoAttribute } from 'src/types' + +import { getMyInfoValue } from '../myinfo.util' + +import { MOCK_MYINFO_FORMAT_DATA } from './myinfo.test.constants' + +describe('myinfo.util', () => { + describe('getMyInfoValue', () => { + it('should return empty string if valid myInfoAttr does not exist in invalid myInfoData', () => { + // Arrange + const invalidData = {} + + // Act + const actual = getMyInfoValue( + 'workpassexpirydate' as MyInfoAttribute.WorkpassExpiryDate, + invalidData as IPersonBasic, + ) + + // Assert + expect(actual).toEqual('') + }) + + it('should return empty string if invalid myInfoAttr does not exist in valid myInfoData', () => { + // Act + const actual = getMyInfoValue( + 'invalid' as MyInfoAttribute, + (MOCK_MYINFO_FORMAT_DATA as unknown) as IPersonBasic, + ) + + // Assert + expect(actual).toEqual('') + }) + + it('should correctly return attr.value', () => { + // Act + const actual = getMyInfoValue( + 'workpassexpirydate' as MyInfoAttribute.WorkpassExpiryDate, + (MOCK_MYINFO_FORMAT_DATA as unknown) as IPersonBasic, + ) + + // Assert + expect(actual).toEqual(MOCK_MYINFO_FORMAT_DATA.workpassexpirydate.value) + }) + + describe('Phone numbers', () => { + it('should correctly return formatted home phone numbers if valid', () => { + // Arrange + // code: '65', + // prefix: '+', + // nbr: '66132665', + const expected = '+65 66132665' + + // Act + const actual = getMyInfoValue( + 'homeno' as MyInfoAttribute.HomeNo, + (MOCK_MYINFO_FORMAT_DATA as unknown) as IPersonBasic, + ) + + // Assert + expect(actual).toEqual(expected) + }) + + it('should correctly return formatted mobile phone numbers if valid', () => { + // Arrange + // code: '65', + // prefix: '+', + // nbr: '97324992', + const expected = '+65 97324992' + + // Act + const actual = getMyInfoValue( + 'mobileno' as MyInfoAttribute.MobileNo, + (MOCK_MYINFO_FORMAT_DATA as unknown) as IPersonBasic, + ) + + // Assert + expect(actual).toEqual(expected) + }) + }) + + describe('Addresses', () => { + it('should correctly return formatted registered addresses', () => { + // Arrange + // unit: '128', + // street: 'BEDOK NORTH AVENUE 1', + // block: '548', + // postal: '460548', + // floor: '09', + // building: '', + const expected = '548 BEDOK NORTH AVENUE 1, #09-128, SINGAPORE 460548' + + // Act + const actual = getMyInfoValue( + 'regadd' as MyInfoAttribute.RegisteredAddress, + (MOCK_MYINFO_FORMAT_DATA as unknown) as IPersonBasic, + ) + + // Assert + expect(actual).toEqual(expected) + }) + + it('should correctly return formatted mailing addresses', () => { + // Arrange + // country: 'US', + // unit: '', + // street: '5TH AVENUE', + // lastupdated: '2016-03-11', + // block: '725', + // source: '2', + // postal: 'NY 10022', + // classification: 'C', + // floor: '', + // building: 'TRUMP TOWER', + const expected = 'TRUMP TOWER, 725 5TH AVENUE, UNITED STATES NY 10022' + + // Act + const actual = getMyInfoValue( + 'mailadd' as MyInfoAttribute.MailingAddress, + (MOCK_MYINFO_FORMAT_DATA as unknown) as IPersonBasic, + ) + + // Assert + expect(actual).toEqual(expected) + }) + + it('should correctly return formatted billing addresses', () => { + // Arrange + // country: 'SG', + // street: 'SERANGOON AVE 3', + // block: '329', + // postal: '550329', + // floor: '09', + // unit: '360', + const expected = '329 SERANGOON AVE 3, #09-360, SINGAPORE 550329' + + // Act + const actual = getMyInfoValue( + 'billadd' as MyInfoAttribute.BillingAddress, + (MOCK_MYINFO_FORMAT_DATA as unknown) as IPersonBasic, + ) + + // Assert + expect(actual).toEqual(expected) + }) + + it('should return empty string if address missing one of [block, street, country, postal] keys', () => { + // Arrange + const mockBillAdd = cloneDeep(MOCK_MYINFO_FORMAT_DATA.billadd) + mockBillAdd.block = '' + + // Act + const actual = getMyInfoValue( + 'billadd' as MyInfoAttribute.BillingAddress, + ({ + billAdd: mockBillAdd, + } as unknown) as IPersonBasic, + ) + + // Assert + expect(actual).toEqual('') + }) + }) + }) +}) diff --git a/tests/unit/backend/models/myinfo_hash.server.model.spec.ts b/src/app/services/myinfo/__tests__/myinfo_hash.model.spec.ts similarity index 98% rename from tests/unit/backend/models/myinfo_hash.server.model.spec.ts rename to src/app/services/myinfo/__tests__/myinfo_hash.model.spec.ts index c3f2714593..f686b60502 100644 --- a/tests/unit/backend/models/myinfo_hash.server.model.spec.ts +++ b/src/app/services/myinfo/__tests__/myinfo_hash.model.spec.ts @@ -6,7 +6,7 @@ import { mocked } from 'ts-jest/utils' import config from 'src/config/config' -import dbHandler from '../helpers/jest-db' +import dbHandler from '../../../../../tests/unit/backend/helpers/jest-db' jest.mock('src/config/config') const MockConfig = mocked(config, true)