diff --git a/src/controllers/pinController.ts b/src/controllers/pinController.ts index 70812bf..f59ef6a 100644 --- a/src/controllers/pinController.ts +++ b/src/controllers/pinController.ts @@ -33,6 +33,7 @@ import { UnauthorizedErrorResponse, InvalidTokenErrorResponse, forbiddenError, + addressScoreResults, } from '../helpers/types'; import PINGenerator from '../helpers/PINGenerator'; import logger from '../middleware/logger'; @@ -510,15 +511,12 @@ export class PINController extends Controller { } /** - * Internal method for creating or recreating a PIN for VHERS. The process is the same. + * Helper function to determine the address score */ - private async createOrRecreatePin( + private async addressScoreRank( @Body() requestBody: createPinRequestBody, - ): Promise { - const gen: PINGenerator = new PINGenerator(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result: any[] = []; - + scoreTrial?: boolean, + ): Promise { // Validate that the input request is correct const faults = this.pinRequestBodyValidate(requestBody); if (faults.length > 0) { @@ -540,15 +538,37 @@ export class PINController extends Controller { } } - // Find Active PIN entry (or entries if more than one pid to insert or update - let pinResults = await findPin(undefined, where); + let pinResults; + if (scoreTrial && scoreTrial === true) { + const select = { + livePinId: true, + pids: true, + titleNumber: true, + landTitleDistrict: true, + givenName: true, + lastName_1: true, + lastName_2: true, + incorporationNumber: true, + addressLine_1: true, + addressLine_2: true, + city: true, + provinceAbbreviation: true, + provinceLong: true, + country: true, + postalCode: true, + }; + // Find Active PIN entry (or entries if more than one pid to insert or update + pinResults = await findPin(select, where); + } else { + pinResults = await findPin(undefined, where); + } pinResults = sortActivePinResults(pinResults); const updateResults = []; const borderlineResults = []; - const thresholds = (await this.dynamicImportCaller()).thresholds; + const weightsThresholds = await this.dynamicImportCaller(); const pinResultKeys = Object.keys(pinResults); - const contactMessages = new Set(); + const contactMessages = new Set(); if (pinResultKeys.length > 0) { for (const key of pinResultKeys) { @@ -572,9 +592,14 @@ export class PINController extends Controller { if (err instanceof Error) logger.error(err.message); continue; // skip this entry } - if ( + if (scoreTrial && scoreTrial === true) { + updateResults.push({ + ActivePin: pinResults[key][i], + matchScore, + }); + } else if ( matchScore.weightedAverage >= - thresholds.overallThreshold + weightsThresholds.thresholds.overallThreshold ) { updateResults.push({ ActivePin: pinResults[key][i], @@ -582,7 +607,7 @@ export class PINController extends Controller { }); } else if ( matchScore.weightedAverage >= - thresholds.borderlineThreshold + weightsThresholds.thresholds.borderlineThreshold ) { borderlineResults.push({ ActivePin: pinResults[key][i], @@ -600,80 +625,123 @@ export class PINController extends Controller { b.matchScore.weightedAverage - a.matchScore.weightedAverage, ); } + return { + updateResults, + borderlineResults, + contactMessages, + weightsThresholds, + }; + } - if (updateResults.length <= 0 && borderlineResults.length <= 0) { - let errMessage; - if (contactMessages.size > 0) { - errMessage = ''; - for (const message of contactMessages) { - errMessage += message + `\n`; - } - errMessage = errMessage.substring(0, errMessage.length - 1); - } else { - errMessage = `Pids ${requestBody.pids} does not match the address and name / incorporation number given:\n`; - let newLineFlag = false; - // Line 1 - if (requestBody.givenName) - errMessage += `${requestBody.givenName} `; - errMessage += `${requestBody.lastName_1} `; - if (requestBody.lastName_2) - errMessage += `${requestBody.lastName_2} `; - if (requestBody.incorporationNumber) - errMessage += `Inc. # ${requestBody.incorporationNumber}`; - // Line 2 - if (requestBody.addressLine_1) - errMessage += `\n${requestBody.addressLine_1}`; - // Line 3 - if (requestBody.addressLine_2) - errMessage += `\n${requestBody.addressLine_2}`; - // Line 4 - if (requestBody.city) { + /** + * Helper function dealing with the error messages for the adress score + */ + private noAddressResultErrMessage( + @Body() requestBody: createPinRequestBody, + contactMessages: Set, + ): string { + let errMessage; + if (contactMessages.size > 0) { + errMessage = ''; + for (const message of contactMessages) { + errMessage += message + `\n`; + } + errMessage = errMessage.substring(0, errMessage.length - 1); + } else { + errMessage = `Pids ${requestBody.pids} does not match the address and name / incorporation number given:\n`; + let newLineFlag = false; + // Line 1 + if (requestBody.givenName) + errMessage += `${requestBody.givenName} `; + errMessage += `${requestBody.lastName_1} `; + if (requestBody.lastName_2) + errMessage += `${requestBody.lastName_2} `; + if (requestBody.incorporationNumber) + errMessage += `Inc. # ${requestBody.incorporationNumber}`; + // Line 2 + if (requestBody.addressLine_1) + errMessage += `\n${requestBody.addressLine_1}`; + // Line 3 + if (requestBody.addressLine_2) + errMessage += `\n${requestBody.addressLine_2}`; + // Line 4 + if (requestBody.city) { + newLineFlag = true; + errMessage += `\n${requestBody.city}`; + } + if (requestBody.provinceAbbreviation) { + if (!newLineFlag) { newLineFlag = true; - errMessage += `\n${requestBody.city}`; - } - if (requestBody.provinceAbbreviation) { - if (!newLineFlag) { - newLineFlag = true; - errMessage += `\n${requestBody.provinceAbbreviation}`; - } else { - errMessage += `, ${requestBody.provinceAbbreviation}`; - } - } - if (requestBody.country) { - if (!newLineFlag) { - newLineFlag = true; - errMessage += `\n${requestBody.country}`; - } else { - errMessage += `, ${requestBody.country}`; - } + errMessage += `\n${requestBody.provinceAbbreviation}`; + } else { + errMessage += `, ${requestBody.provinceAbbreviation}`; } - if (requestBody.postalCode) { - if (!newLineFlag) - errMessage += `\n${requestBody.postalCode}`; - else errMessage += ` ${requestBody.postalCode}`; + } + if (requestBody.country) { + if (!newLineFlag) { + newLineFlag = true; + errMessage += `\n${requestBody.country}`; + } else { + errMessage += `, ${requestBody.country}`; } } + if (requestBody.postalCode) { + if (!newLineFlag) errMessage += `\n${requestBody.postalCode}`; + else errMessage += ` ${requestBody.postalCode}`; + } + } + return errMessage; + } + /** + * Internal method for creating or recreating a PIN for VHERS. The process is the same. + */ + private async createOrRecreatePin( + @Body() requestBody: createPinRequestBody, + ): Promise { + const gen: PINGenerator = new PINGenerator(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result: any[] = []; + + const score = await this.addressScoreRank(requestBody); + + if ( + score.updateResults.length <= 0 && + score.borderlineResults.length <= 0 + ) { + const errMessage = this.noAddressResultErrMessage( + requestBody, + score.contactMessages, + ); throw new NotFoundError(errMessage); - } else if (updateResults.length <= 0 && borderlineResults.length > 0) { + } else if ( + score.updateResults.length <= 0 && + score.borderlineResults.length > 0 + ) { // Give an error message related to the closest result let errMessage = `Close result: consider checking your `; if ( - borderlineResults[0].matchScore.streetAddressScore < - thresholds.streetAddressThreshold - ) { - errMessage += `address`; - } else if ( - borderlineResults[0].matchScore.postalCodeScore && - borderlineResults[0].matchScore.postalCodeScore < - thresholds.postalCodeThreshold + Object.hasOwn( + score.borderlineResults[0].matchScore, + 'postalCodeScore', + ) && + score.borderlineResults[0].matchScore.postalCodeScore < + score.weightsThresholds.weights.postalCodeWeight ) { errMessage += `postal code`; } else if ( - borderlineResults[0].matchScore.incorporationNumberScore && - borderlineResults[0].matchScore.incorporationNumberScore < - thresholds.incorporationNumberThreshold + Object.hasOwn( + score.borderlineResults[0].matchScore, + 'incorporationNumberScore', + ) && + score.borderlineResults[0].matchScore.incorporationNumberScore < + score.weightsThresholds.weights.incorporationNumberWeight ) { errMessage += `incorporation number`; + } else if ( + score.borderlineResults[0].matchScore.streetAddressScore < + score.weightsThresholds.weights.streetAddressWeight + ) { + errMessage += `address`; } else { errMessage += `name`; // we're not going to tell them about things that barely affect // the score like country or province, and if they got the number of owners @@ -687,7 +755,7 @@ export class PINController extends Controller { requestBody.pinLength, requestBody.allowedChars, ); - const resultToUpdate = updateResults[0].ActivePin; // this will be the one with the highest match score + const resultToUpdate = score.updateResults[0].ActivePin; // this will be the one with the highest match score resultToUpdate.pin = pin.pin; const emailPhone: emailPhone = { email: requestBody.email, @@ -739,11 +807,15 @@ export class PINController extends Controller { } const username: string = payload.username && payload.username !== '' ? payload.username : ''; - const name: string = - (payload.given_name || payload.family_name) && - (payload.given_name !== '' || payload.family_name !== '') - ? payload.given_name + ' ' + payload.family_name - : ''; + let name: string = ''; + if (payload.given_name) { + name = payload.given_name; + if (payload.family_name) name = name + ' ' + payload.family_name; + } else if (payload.family_name) { + name = payload.family_name; + } else { + name = ''; + } if (username === '' || name === '') { throw new AuthenticationError( `Username or given / family name does not exist for requester`, @@ -841,137 +913,14 @@ export class PINController extends Controller { @Body() requestBody: createPinRequestBody, ) { try { - // Validate that the input request is correct - const faults = this.pinRequestBodyValidate(requestBody); - if (faults.length > 0) { - throw new AggregateError( - faults, - 'Validation Error(s) occured in createPin request body:', - ); - } - - // Grab input pid(s) - const pids: string[] = pidStringSplitAndSort(requestBody.pids); - let where; - if (pids.length === 1) { - where = { pids: Like(`%` + pids[0] + `%`) }; + const score = await this.addressScoreRank(requestBody, true); + if (score.updateResults.length > 0) { + return score.updateResults[0]; } else { - where = []; - for (let i = 0; i < pids.length; i++) { - where.push({ pids: Like(`%` + pids[i] + `%`) }); - } - } - const select = { - livePinId: true, - pids: true, - titleNumber: true, - landTitleDistrict: true, - givenName: true, - lastName_1: true, - lastName_2: true, - incorporationNumber: true, - addressLine_1: true, - addressLine_2: true, - city: true, - provinceAbbreviation: true, - provinceLong: true, - country: true, - postalCode: true, - }; - // Find Active PIN entry (or entries if more than one pid to insert or update - let pinResults = await findPin(select, where); - pinResults = sortActivePinResults(pinResults); - - const updateResults = []; - // const borderlineResults = []; - // const thresholds = (await this.dynamicImportCaller()).thresholds; - const pinResultKeys = Object.keys(pinResults); - const contactMessages = new Set(); - - if (pinResultKeys.length > 0) { - for (const key of pinResultKeys) { - const titleOwners = pinResults[key].length; - for (let i = 0; i < titleOwners; i++) { - const matchScore = await this.pinResultValidate( - requestBody, - pinResults[key][i], - titleOwners, - ); - if (!('weightedAverage' in matchScore)) { - // it's a string array - for (const message of matchScore) { - contactMessages.add(message); - } - continue; // bad match, skip - } else { - updateResults.push({ - ActivePin: pinResults[key][i], - matchScore, - }); - } - } - } - updateResults.sort( - (a, b) => - b.matchScore.weightedAverage - - a.matchScore.weightedAverage, + const errMessage = this.noAddressResultErrMessage( + requestBody, + score.contactMessages, ); - } - - if (updateResults.length > 0) { - return updateResults[0]; - } else { - let errMessage; - if (contactMessages.size > 0) { - errMessage = ''; - for (const message of contactMessages) { - errMessage += message + `\n`; - } - errMessage = errMessage.substring(0, errMessage.length - 1); - } else { - errMessage = `Pids ${requestBody.pids} does not match the address and name / incorporation number given:\n`; - let newLineFlag = false; - // Line 1 - if (requestBody.givenName) - errMessage += `${requestBody.givenName} `; - errMessage += `${requestBody.lastName_1} `; - if (requestBody.lastName_2) - errMessage += `${requestBody.lastName_2} `; - if (requestBody.incorporationNumber) - errMessage += `Inc. # ${requestBody.incorporationNumber}`; - // Line 2 - if (requestBody.addressLine_1) - errMessage += `\n${requestBody.addressLine_1}`; - // Line 3 - if (requestBody.addressLine_2) - errMessage += `\n${requestBody.addressLine_2}`; - // Line 4 - if (requestBody.city) { - newLineFlag = true; - errMessage += `\n${requestBody.city}`; - } - if (requestBody.provinceAbbreviation) { - if (!newLineFlag) { - newLineFlag = true; - errMessage += `\n${requestBody.provinceAbbreviation}`; - } else { - errMessage += `, ${requestBody.provinceAbbreviation}`; - } - } - if (requestBody.country) { - if (!newLineFlag) { - newLineFlag = true; - errMessage += `\n${requestBody.country}`; - } else { - errMessage += `, ${requestBody.country}`; - } - } - if (requestBody.postalCode) { - if (!newLineFlag) - errMessage += `\n${requestBody.postalCode}`; - else errMessage += ` ${requestBody.postalCode}`; - } - } throw new NotFoundError(errMessage); } } catch (err) { diff --git a/src/helpers/types.ts b/src/helpers/types.ts index 93ca8a9..2b2174d 100644 --- a/src/helpers/types.ts +++ b/src/helpers/types.ts @@ -800,3 +800,13 @@ export enum requestListQueryParam { pending = 'pending', completed = 'completed', } + +/** + * Intermediate address score results + */ +export interface addressScoreResults { + updateResults: any[]; + borderlineResults: any[]; + contactMessages: Set; + weightsThresholds: any; +} diff --git a/src/tests/commonResponses.ts b/src/tests/commonResponses.ts index d324738..4e51c7e 100644 --- a/src/tests/commonResponses.ts +++ b/src/tests/commonResponses.ts @@ -1049,6 +1049,41 @@ export const SampleSuperAdminTokenPayload = { ], }; +export const NamelessTokenPayload = { + identity_provider: 'idir', + sid: 'f2291e4e-ea0b-4eb4-bc35-06a9bb7d1eb4', + idir_user_guid: '12FC98EA15007D2F704B95DEFC3D2DDF', + idir_username: '', + username: '', + given_name: '', + family_name: '', + preferred_username: '12fc98ea15007d2f704b95defc3d2ddf@idir', + email: 'example@test.com', + role: 'SuperAdmin', + permissions: [ + 'USER_ACCESS', + 'VIEW_PIN', + 'PROPERTY_SEARCH', + 'ACCESS_REQUEST', + ], +}; + +export const LastNameOnlyTokenPayload = { + identity_provider: 'idir', + sid: 'f2291e4e-ea0b-4eb4-bc35-06a9bb7d1eb4', + idir_user_guid: '12FC98EA15007D2F704B95DEFC3D2DDF', + preferred_username: '12fc98ea15007d2f704b95defc3d2ddf@idir', + email: 'example@test.com', + role: 'SuperAdmin', + family_name: 'abcd', + permissions: [ + 'USER_ACCESS', + 'VIEW_PIN', + 'PROPERTY_SEARCH', + 'ACCESS_REQUEST', + ], +}; + export const SampleBCEIDBUsinessAdminTokenPayload = { identity_provider: 'bceidbusiness', sid: 'f2291e4e-ea0b-4eb4-bc35-06a9bb7d1eb4', @@ -1104,3 +1139,41 @@ export const validFindUserResponse: Users[] = [ deactivationReason: null, }, ]; + +export const weightsThresholds = { + thresholds: { + overallThreshold: 1, + borderlineThreshold: 0, + givenNameThreshold: 0.95, + lastNamesThreshold: 0.95, + incorporationNumberThreshold: 1, + ownerNumberThreshold: 1, + streetAddressThreshold: 0.85, + cityThreshold: 0.8, + provinceAbbreviationThreshold: 0.95, + countryThreshold: 0.8, + postalCodeThreshold: 1, + }, + weights: { + givenNameWeight: 0.2, + lastNamesWeight: 0.2, + incorporationNumberWeight: 0.2, + ownerNumberWeight: 0.125, + streetAddressWeight: 0.125, + cityWeight: 0.02, + provinceAbbreviationWeight: 0.01, + countryWeight: 0.01, + postalCodeWeight: 0.31, + }, + fuzzinessCoefficients: { + givenNameFuzzyCoefficient: 0.95, + lastNamesFuzzyCoefficient: 0.95, + incorporationNumberFuzzyCoefficient: 0, + streetAddressFuzzyCoefficient: 0.95, + cityFuzzyCoefficient: 0.95, + provinceAbbreviationFuzzyCoefficient: 0.95, + countryFuzzyCoefficient: 0.95, + postalCodeFuzzyCoefficient: 0, + }, + streetAddressLooseMatchReductionCoefficient: 0.25, +}; diff --git a/src/tests/routes/pins.spec.ts b/src/tests/routes/pins.spec.ts index 7b70bd7..dc5d04d 100644 --- a/src/tests/routes/pins.spec.ts +++ b/src/tests/routes/pins.spec.ts @@ -15,6 +15,7 @@ import { TypeORMError, } from 'typeorm'; import { + createPinRequestBody, emailPhone, expirationReason, serviceBCCreateRequestBody, @@ -40,15 +41,21 @@ import { createOrRecreatePinServiceBCSuccessResponseSinglePid, SampleSuperAdminTokenPayload, DeletePINSuccessResponse, + weightsThresholds, + NamelessTokenPayload, + SampleBCEIDBUsinessAdminTokenPayload, + LastNameOnlyTokenPayload, } from '../commonResponses'; import { PINController } from '../../controllers/pinController'; import jwt from 'jsonwebtoken'; +import * as auth from '../../helpers/auth'; +import { AuthenticationError } from '../../middleware/AuthenticationError'; jest.spyOn(DataSource.prototype, 'getMetadata').mockImplementation( () => ({}) as EntityMetadata, ); const key = 'cf430240-e5b6-4224-bd71-a02e098cc6e8'; // don't use this as the actual key... -let token: string; +let token: string | null; describe('Pin endpoints', () => { beforeAll(() => { process.env.VHERS_API_KEY = key; @@ -101,23 +108,14 @@ describe('Pin endpoints', () => { async ( updatedPins: ActivePin[], sendToInfo: emailPhone, + propertyAddress: string, requesterUsername?: string, + requesterName?: string, ) => { - if (updatedPins[0].pin === 'ABCD1234') return [[''], '']; - return [ - [ - `An error occured while updating updatedPins[0] in batchUpdatePin: unknown error`, - ], - `create`, - ]; + return [[], 'create']; }, ); - jest.spyOn( - PINController.prototype as any, - 'createOrRecreatePin', - ).mockResolvedValueOnce(createOrRecreatePinServiceBCSuccessResponse); - const reqBody = validCreatePinBodyInc; const res = await request(app) .post('/pins/vhers-create') @@ -125,7 +123,6 @@ describe('Pin endpoints', () => { .set({ 'x-api-key': key }); expect(res.statusCode).toBe(200); expect(res.body.length).toBe(1); - expect(res.body[0].pin).toBe('ABCD1234'); expect(res.body[0].pids).toBe('1234|5678'); }); @@ -144,7 +141,19 @@ describe('Pin endpoints', () => { pin1.provinceAbbreviation = 'BC'; pin1.country = 'Canada'; pin1.postalCode = 'V1V1V1'; - const result = [pin1]; + const pin2 = new ActivePin(); + pin2.pids = '1234'; + pin2.titleNumber = 'EFGH'; + pin2.landTitleDistrict = 'BC'; + pin2.livePinId = 'cf430240-e5b6-4224-bd71-a02e098cc6e8'; + pin2.lastName_1 = 'None'; + pin2.incorporationNumber = '91011'; + pin2.addressLine_1 = '123 example s'; + pin2.city = 'Vancouver'; + pin2.provinceAbbreviation = 'BC'; + pin2.country = 'Canada'; + pin2.postalCode = 'V1V1V1'; + const result = [pin2, pin1]; if ((where as any).pids instanceof FindOperator) return result as ActivePin[]; @@ -165,7 +174,7 @@ describe('Pin endpoints', () => { sendToInfo: emailPhone, requesterUsername?: string, ) => { - if (updatedPins[0].pin === 'ABCD1234') return [[''], '']; + if (updatedPins[0].pin === 'ABCD1234') return [[], 'recreate']; return [ [ `An error occured while updating updatedPins[0] in batchUpdatePin: unknown error`, @@ -175,22 +184,16 @@ describe('Pin endpoints', () => { }, ); - jest.spyOn( - PINController.prototype as any, - 'createOrRecreatePin', - ).mockResolvedValueOnce( - createOrRecreatePinServiceBCSuccessResponseSinglePid, - ); - const reqBody = validCreatePinBodySinglePid; + reqBody.numberOfOwners = 2; const res = await request(app) .post('/pins/vhers-create') .send(reqBody) .set({ 'x-api-key': key }); expect(res.statusCode).toBe(200); expect(res.body.length).toBe(1); - expect(res.body[0].pin).toBe('ABCD1234'); expect(res.body[0].pids).toBe('1234'); + reqBody.numberOfOwners = 1; }); test('vhers-create on API key not matching returns 400', async () => { @@ -421,7 +424,196 @@ describe('Pin endpoints', () => { ); }); - test('vhers-create on request body on no batch update returns 422', async () => { + test('vhers-create on borderline result (address) returns 422', async () => { + jest.spyOn(ActivePIN, 'findPin').mockImplementationOnce( + async (select?: object | undefined, where?: object | undefined) => { + const pin1 = new ActivePin(); + pin1.pids = '1234|5678'; + pin1.titleNumber = 'EFGH'; + pin1.landTitleDistrict = 'BC'; + pin1.livePinId = 'cf430240-e5b6-4224-bd71-a02e098cc6e8'; + pin1.lastName_1 = 'None'; + pin1.incorporationNumber = '91011'; + pin1.addressLine_1 = '123 exam blvd'; + pin1.city = 'Vancouver'; + pin1.provinceAbbreviation = 'BC'; + pin1.country = 'Canada'; + pin1.postalCode = 'V1V1V1'; + const pin2 = new ActivePin(); + pin2.pids = '1234'; + pin2.titleNumber = 'EFGH'; + pin2.landTitleDistrict = 'BC'; + pin2.livePinId = 'cf430240-e5b6-4224-bd71-a02e098cc6e8'; + pin2.lastName_1 = 'None'; + pin2.incorporationNumber = '91011'; + pin2.addressLine_1 = '123 e blvd'; + pin2.city = 'Vancouver'; + pin2.provinceAbbreviation = 'BC'; + pin2.country = 'Canada'; + pin2.postalCode = 'V1V1V1'; + const result = [pin2, pin1]; + if ( + (where as any)[0].pids instanceof FindOperator && + (where as any)[1].pids instanceof FindOperator + ) + return result as ActivePin[]; + return []; + }, + ); + jest.spyOn(PINController.prototype as any, 'dynamicImportCaller') + .mockImplementationOnce(() => { + return weightsThresholds; + }) + .mockImplementationOnce(() => { + return weightsThresholds; + }) + .mockImplementationOnce(() => { + return weightsThresholds; + }); + + const reqBody = validCreatePinBodyInc; + reqBody.numberOfOwners = 2; + const res = await request(app) + .post('/pins/vhers-create') + .send(reqBody) + .set({ 'x-api-key': key }); + expect(res.statusCode).toBe(422); + expect(res.body.message).toBe( + 'Close result: consider checking your address', + ); + reqBody.numberOfOwners = 1; + }); + + test('vhers-create on borderline result (postal code) returns 422', async () => { + jest.spyOn(ActivePIN, 'findPin').mockImplementationOnce( + async (select?: object | undefined, where?: object | undefined) => { + const pin1 = new ActivePin(); + pin1.pids = '1234|5678'; + pin1.titleNumber = 'EFGH'; + pin1.landTitleDistrict = 'BC'; + pin1.livePinId = 'cf430240-e5b6-4224-bd71-a02e098cc6e8'; + pin1.lastName_1 = 'None'; + pin1.incorporationNumber = '91011'; + pin1.addressLine_1 = '123 example st'; + pin1.city = 'Vancouver'; + pin1.provinceAbbreviation = 'BC'; + pin1.country = 'Canada'; + pin1.postalCode = 'V1V1V2'; + const result = [pin1]; + if ( + (where as any)[0].pids instanceof FindOperator && + (where as any)[1].pids instanceof FindOperator + ) + return result as ActivePin[]; + return []; + }, + ); + jest.spyOn(PINController.prototype as any, 'dynamicImportCaller') + .mockImplementationOnce(() => { + return weightsThresholds; + }) + .mockImplementationOnce(() => { + return weightsThresholds; + }); + + const reqBody = validCreatePinBodyInc; + const res = await request(app) + .post('/pins/vhers-create') + .send(reqBody) + .set({ 'x-api-key': key }); + expect(res.statusCode).toBe(422); + expect(res.body.message).toBe( + 'Close result: consider checking your postal code', + ); + }); + + test('vhers-create on borderline result (postal code) returns 422', async () => { + jest.spyOn(ActivePIN, 'findPin').mockImplementationOnce( + async (select?: object | undefined, where?: object | undefined) => { + const pin1 = new ActivePin(); + pin1.pids = '1234|5678'; + pin1.titleNumber = 'EFGH'; + pin1.landTitleDistrict = 'BC'; + pin1.livePinId = 'cf430240-e5b6-4224-bd71-a02e098cc6e8'; + pin1.lastName_1 = 'None'; + pin1.incorporationNumber = '91015'; + pin1.addressLine_1 = '123 example st'; + pin1.city = 'Vancouver'; + pin1.provinceAbbreviation = 'BC'; + pin1.country = 'Canada'; + pin1.postalCode = 'V1V1V1'; + const result = [pin1]; + if ( + (where as any)[0].pids instanceof FindOperator && + (where as any)[1].pids instanceof FindOperator + ) + return result as ActivePin[]; + return []; + }, + ); + jest.spyOn(PINController.prototype as any, 'dynamicImportCaller') + .mockImplementationOnce(() => { + return weightsThresholds; + }) + .mockImplementationOnce(() => { + return weightsThresholds; + }); + + const reqBody = validCreatePinBodyInc; + const res = await request(app) + .post('/pins/vhers-create') + .send(reqBody) + .set({ 'x-api-key': key }); + expect(res.statusCode).toBe(422); + expect(res.body.message).toBe( + 'Close result: consider checking your incorporation number', + ); + }); + + test('vhers-create on borderline result (postal code) returns 422', async () => { + jest.spyOn(ActivePIN, 'findPin').mockImplementationOnce( + async (select?: object | undefined, where?: object | undefined) => { + const pin1 = new ActivePin(); + pin1.pids = '1234|5678'; + pin1.titleNumber = 'EFGH'; + pin1.landTitleDistrict = 'BC'; + pin1.livePinId = 'cf430240-e5b6-4224-bd71-a02e098cc6e8'; + pin1.lastName_1 = 'Incorrect name'; + pin1.incorporationNumber = '91011'; + pin1.addressLine_1 = '123 example st'; + pin1.city = 'Vancouver'; + pin1.provinceAbbreviation = 'BC'; + pin1.country = 'Canada'; + pin1.postalCode = 'V1V1V1'; + const result = [pin1]; + if ( + (where as any)[0].pids instanceof FindOperator && + (where as any)[1].pids instanceof FindOperator + ) + return result as ActivePin[]; + return []; + }, + ); + jest.spyOn(PINController.prototype as any, 'dynamicImportCaller') + .mockImplementationOnce(() => { + return weightsThresholds; + }) + .mockImplementationOnce(() => { + return weightsThresholds; + }); + + const reqBody = validCreatePinBodyInc; + const res = await request(app) + .post('/pins/vhers-create') + .send(reqBody) + .set({ 'x-api-key': key }); + expect(res.statusCode).toBe(422); + expect(res.body.message).toBe( + 'Close result: consider checking your name', + ); + }); + + test('vhers-create on no batch update returns 422', async () => { jest.spyOn(ActivePIN, 'findPin').mockImplementationOnce( async (select?: object | undefined, where?: object | undefined) => { const pin1 = new ActivePin(); @@ -455,8 +647,9 @@ describe('Pin endpoints', () => { async ( updatedPins: ActivePin[], sendToInfo: emailPhone, - requesterName?: string, + propertyAddress: string, requesterUsername?: string, + requesterName?: string, ) => { return [ [ @@ -558,20 +751,10 @@ describe('Pin endpoints', () => { sendToInfo: emailPhone, requesterUsername?: string, ) => { - return [ - [ - `An error occured while updating updatedPins[0] in batchUpdatePin: unknown error`, - ], - `create`, - ]; + return [[], 'create']; }, ); - jest.spyOn( - PINController.prototype as any, - 'createOrRecreatePinServiceBC', - ).mockResolvedValueOnce(createOrRecreatePinServiceBCSuccessResponse); - const reqBody = validCreatePinBodyIncServiceBC; const res = await request(app) @@ -584,6 +767,114 @@ describe('Pin endpoints', () => { expect(res.body[0].pids).toBe('1234|5678'); }); + test('create should not return pin for admin', async () => { + const JWT_SECRET = 'abcd'; + token = jwt.sign(SampleBCEIDBUsinessAdminTokenPayload, JWT_SECRET, { + expiresIn: 30 * 60 * 1000, + }); + jest.spyOn(ActivePIN, 'findPin').mockImplementationOnce( + async (select?: object | undefined, where?: object | undefined) => { + const pin1 = new ActivePin(); + pin1.pids = '1234|5678'; + pin1.titleNumber = 'EFGH'; + pin1.landTitleDistrict = 'BC'; + pin1.livePinId = 'cf430240-e5b6-4224-bd71-a02e098cc6e8'; + pin1.lastName_1 = 'None'; + pin1.incorporationNumber = '91011'; + pin1.addressLine_1 = '123 example st'; + pin1.city = 'Vancouver'; + pin1.provinceAbbreviation = 'BC'; + pin1.country = 'Canada'; + pin1.postalCode = 'V1V1V1'; + const result = [pin1]; + return result as ActivePin[]; + }, + ); + jest.spyOn(PINGenerator.prototype, 'create').mockImplementationOnce( + async ( + pinLength?: number | undefined, + allowedChars?: string | undefined, + ) => { + return { pin: 'ABCD1234' }; + }, + ); + jest.spyOn(ActivePIN, 'batchUpdatePin').mockImplementationOnce( + async ( + updatedPins: ActivePin[], + sendToInfo: emailPhone, + propertyAddress: string, + requesterUsername?: string, + requesterName?: string, + ) => { + return [[], 'create']; + }, + ); + + const reqBody = validCreatePinBodyIncServiceBC; + + const res = await request(app) + .post('/pins/create') + .send(reqBody) + .set('Cookie', `token=${token}`); + expect(res.statusCode).toBe(200); + expect(res.body.length).toBe(1); + expect(res.body[0].pids).toBe('1234|5678'); + token = jwt.sign(SampleSuperAdminTokenPayload, JWT_SECRET, { + expiresIn: 30 * 60 * 1000, + }); + }); + + test('create on JWT error returns 403', async () => { + jest.spyOn(auth, 'decodingJWT').mockImplementationOnce(() => { + throw new Error('Oops!'); + }); + const reqBody = validCreatePinBodyIncServiceBC; + const res = await request(app) + .post('/pins/create') + .send(reqBody) + .set('Cookie', `token=${token}`); + expect(res.statusCode).toBe(403); + expect(res.body.message).toBe('Unable to decode JWT'); + }); + + test('create on blank name and username returns 403', async () => { + const JWT_SECRET = 'abcd'; + token = jwt.sign(NamelessTokenPayload, JWT_SECRET, { + expiresIn: 30 * 60 * 1000, + }); + const reqBody = validCreatePinBodyIncServiceBC; + const res = await request(app) + .post('/pins/create') + .send(reqBody) + .set('Cookie', `token=${token}`); + expect(res.statusCode).toBe(403); + expect(res.body.message).toBe( + `Username or given / family name does not exist for requester`, + ); + token = jwt.sign(SampleSuperAdminTokenPayload, JWT_SECRET, { + expiresIn: 30 * 60 * 1000, + }); + }); + + test('create on no first name and username returns 403', async () => { + const JWT_SECRET = 'abcd'; + token = jwt.sign(LastNameOnlyTokenPayload, JWT_SECRET, { + expiresIn: 30 * 60 * 1000, + }); + const reqBody = validCreatePinBodyIncServiceBC; + const res = await request(app) + .post('/pins/create') + .send(reqBody) + .set('Cookie', `token=${token}`); + expect(res.statusCode).toBe(403); + expect(res.body.message).toBe( + `Username or given / family name does not exist for requester`, + ); + token = jwt.sign(SampleSuperAdminTokenPayload, JWT_SECRET, { + expiresIn: 30 * 60 * 1000, + }); + }); + test('create on request body on no batch update returns 422', async () => { jest.spyOn(ActivePIN, 'findPin').mockImplementationOnce( async (select?: object | undefined, where?: object | undefined) => { @@ -738,7 +1029,7 @@ describe('Pin endpoints', () => { sendToInfo: emailPhone, requesterUsername?: string, ) => { - if (updatedPins[0].pin === 'ABCD1234') return [[''], '']; + if (updatedPins[0].pin === 'ABCD1234') return [[], '']; return [ [ `An error occured while updating updatedPins[0] in batchUpdatePin: unknown error`, @@ -748,11 +1039,6 @@ describe('Pin endpoints', () => { }, ); - jest.spyOn( - PINController.prototype as any, - 'createOrRecreatePin', - ).mockResolvedValueOnce(createOrRecreatePinServiceBCSuccessResponse); - const reqBody = validCreatePinBodyInc; const res = await request(app) .post('/pins/vhers-regenerate') @@ -760,7 +1046,6 @@ describe('Pin endpoints', () => { .set({ 'x-api-key': key }); expect(res.statusCode).toBe(200); expect(res.body.length).toBe(1); - expect(res.body[0].pin).toBe('ABCD1234'); expect(res.body[0].pids).toBe('1234|5678'); }); @@ -900,9 +1185,11 @@ describe('Pin endpoints', () => { async ( updatedPins: ActivePin[], sendToInfo: emailPhone, + propertyAddress: string, requesterUsername?: string, + requesterName?: string, ) => { - if (updatedPins[0].pin === 'ABCD1234') return [[''], '']; + if (updatedPins[0].pin === 'ABCD1234') return [[], '']; return [ [ `An error occured while updating updatedPins[0] in batchUpdatePin: unknown error`, @@ -917,11 +1204,6 @@ describe('Pin endpoints', () => { 'pinRequestBodyValidate', ).mockResolvedValueOnce([]); - jest.spyOn( - PINController.prototype as any, - 'createOrRecreatePinServiceBC', - ).mockResolvedValueOnce(createOrRecreatePinServiceBCSuccessResponse); - const reqBody = validCreatePinBodyIncServiceBC; const res = await request(app) .post('/pins/regenerate') @@ -933,6 +1215,22 @@ describe('Pin endpoints', () => { expect(res.body[0].pids).toBe('1234|5678'); }); + test('regenerate on authentication error returns 403', async () => { + jest.spyOn( + PINController.prototype as any, + 'createOrRecreatePinServiceBC', + ).mockImplementationOnce(async () => { + throw new AuthenticationError('Auth error', 403); + }); + const reqBody = validCreatePinBodyIncServiceBC; + const res = await request(app) + .post('/pins/regenerate') + .send(reqBody) + .set('Cookie', `token=${token}`); + expect(res.statusCode).toBe(403); + expect(res.body.message).toBe('Auth error'); + }); + test('regenerate on request body validation fail returns 422', async () => { const reqBody = invalidCreatePinBodyWrongPhoneServiceBC; const res = await request(app) @@ -1304,4 +1602,138 @@ describe('Pin endpoints', () => { expect(res.body.reason.errorType).toBe('Error'); expect(res.body.reason.errorMessage).toBe('An unknown error occured'); }); + + /* + /thresholds endpoint tests + */ + test('thresholds displays the thresholds and weights', async () => { + const res = await request(app) + .get('/pins/thresholds') + .set({ 'x-api-key': key }); + expect(res.body.thresholds).toBeDefined(); + expect(res.body.weights).toBeDefined(); + }); + + test('thresholds on any error returns 500', async () => { + jest.spyOn( + PINController.prototype as any, + 'dynamicImportCaller', + ).mockImplementationOnce(async () => { + throw new Error('Unknown error'); + }); + const res = await request(app) + .get('/pins/thresholds') + .set({ 'x-api-key': key }); + expect(res.statusCode).toBe(500); + expect(res.body.message).toBe( + 'Error in weightsThresholds: failed to grab thresholds', + ); + }); + + /* + /score endpoint tests + */ + test('score returns an address score with valid query', async () => { + jest.spyOn(ActivePIN, 'findPin').mockImplementationOnce( + async (select?: object | undefined, where?: object | undefined) => { + const pin1 = new ActivePin(); + pin1.pids = '1234|5678'; + pin1.titleNumber = 'EFGH'; + pin1.landTitleDistrict = 'BC'; + pin1.livePinId = 'cf430240-e5b6-4224-bd71-a02e098cc6e8'; + pin1.lastName_1 = 'None'; + pin1.incorporationNumber = '91011'; + pin1.addressLine_1 = '123 example st'; + pin1.city = 'Vancouver'; + pin1.provinceAbbreviation = 'BC'; + pin1.country = 'Canada'; + pin1.postalCode = 'V1V1V1'; + const result = [pin1]; + return result as ActivePin[]; + }, + ); + + const reqBody = validCreatePinBodyInc; + const res = await request(app) + .post('/pins/score') + .send(reqBody) + .set({ 'x-api-key': key }); + expect(res.statusCode).toBe(200); + expect(res.body.ActivePin).toBeDefined(); + expect(res.body.matchScore).toBeDefined(); + }); + + test('score with no matches returns 422', async () => { + jest.spyOn(ActivePIN, 'findPin').mockImplementationOnce( + async (select?: object | undefined, where?: object | undefined) => { + return []; + }, + ); + + const reqBody = validCreatePinBodyInc; + const res = await request(app) + .post('/pins/score') + .send(reqBody) + .set({ 'x-api-key': key }); + expect(res.statusCode).toBe(422); + expect(res.body.message).toBe( + 'Pids 1234|5678 does not match the address and name / incorporation number given:\n' + + 'None Inc. # 91011\n' + + '123 example st\n' + + 'Vancouver, BC, Canada V1V1V1', + ); + }); + + test('score with aggregate error returns 422', async () => { + jest.spyOn( + PINController.prototype as any, + 'addressScoreRank', + ).mockImplementationOnce(async () => { + throw new AggregateError(['error 1'], 'errors occured'); + }); + + const reqBody = validCreatePinBodyInc; + const res = await request(app) + .post('/pins/score') + .send(reqBody) + .set({ 'x-api-key': key }); + expect(res.statusCode).toBe(422); + expect(res.body.message).toBe('errors occured'); + expect(res.body.faults.length).toEqual(1); + expect(res.body.faults[0]).toBe('error 1'); + }); + + test('score with range error returns 422', async () => { + jest.spyOn( + PINController.prototype as any, + 'addressScoreRank', + ).mockImplementationOnce(async () => { + throw new RangeError('error occured'); + }); + + const reqBody = validCreatePinBodyInc; + const res = await request(app) + .post('/pins/score') + .send(reqBody) + .set({ 'x-api-key': key }); + expect(res.statusCode).toBe(422); + expect(res.body.message).toBe('error occured'); + }); + + test('score with unknown error returns 500', async () => { + jest.spyOn( + PINController.prototype as any, + 'addressScoreRank', + ).mockImplementationOnce(async () => { + throw new Error('error occured'); + }); + + const reqBody = validCreatePinBodyInc; + const res = await request(app) + .post('/pins/score') + .send(reqBody) + .set({ 'x-api-key': key }); + expect(res.statusCode).toBe(500); + expect(res.body.message).toBe('error occured'); + }); });