From 4bd29efebf5c53511f030ed3e6ef9ba19947c160 Mon Sep 17 00:00:00 2001 From: kennsippell Date: Tue, 26 Nov 2024 01:12:05 -0700 Subject: [PATCH] Ready to test --- src/config/config-worker.ts | 2 +- src/lib/manage-hierarchy.ts | 137 +++++++++++----- src/lib/queues.ts | 4 +- src/liquid/place/manage_hierarchy_form.html | 4 +- src/plugins/bullmq.ts | 4 +- src/routes/manage-hierarchy.ts | 2 +- src/services/hierarchy-view-model.ts | 12 +- src/worker/cht-conf-worker.ts | 3 +- src/worker/main.ts | 4 +- test/integration/manage-hierarchy.spec.ts | 4 +- test/lib/manage-hierarchy.spec.ts | 169 ++++++++++++++++++++ test/lib/move.spec.ts | 100 ------------ 12 files changed, 285 insertions(+), 160 deletions(-) create mode 100644 test/lib/manage-hierarchy.spec.ts delete mode 100644 test/lib/move.spec.ts diff --git a/src/config/config-worker.ts b/src/config/config-worker.ts index ae6ee3f9..4f239a09 100644 --- a/src/config/config-worker.ts +++ b/src/config/config-worker.ts @@ -11,7 +11,7 @@ export const WorkerConfig = { host: environment.REDIS_HOST, port: Number(environment.REDIS_PORT), }, - moveContactQueue: 'MOVE_CONTACT_QUEUE', + queueName: 'MOVE_CONTACT_QUEUE', defaultJobOptions: { attempts: 3, // Max retries for a failed job backoff: { diff --git a/src/lib/manage-hierarchy.ts b/src/lib/manage-hierarchy.ts index 72aca8c2..de9b98fe 100644 --- a/src/lib/manage-hierarchy.ts +++ b/src/lib/manage-hierarchy.ts @@ -1,66 +1,129 @@ import { ContactType } from '../config'; import SessionCache from '../services/session-cache'; -import { ChtApi } from './cht-api'; +import { ChtApi, RemotePlace } from './cht-api'; import RemotePlaceResolver from './remote-place-resolver'; import Place from '../services/place'; -import { JobParams, IQueue, getMoveContactQueue } from './queues'; +import { JobParams, IQueue, getChtConfQueue } from './queues'; import Auth from './authentication'; import { ChtConfJobData } from '../worker/cht-conf-worker'; +import _ from 'lodash'; export const HIERARCHY_ACTIONS = ['move', 'merge', 'delete']; export type HierarchyAction = typeof HIERARCHY_ACTIONS[number]; export default class ManageHierarchyLib { - constructor() { } + private constructor() { } - public static async move( - formData: any, contactType: ContactType, sessionCache: SessionCache, chtApi: ChtApi, moveContactQueue: IQueue = getMoveContactQueue() + public static async scheduleJob( + formData: any, + contactType: ContactType, + sessionCache: SessionCache, + chtApi: ChtApi, + queueName: IQueue = getChtConfQueue() ) { - const fromLineage = await resolve('from_', formData, contactType, sessionCache, chtApi); - const toLineage = await resolve('to_', formData, contactType, sessionCache, chtApi); + const { sourceLineage, destinationLineage, jobParam } = await getJobDetails(formData, contactType, sessionCache, chtApi); - const toId = toLineage[1]?.id; - const fromId = fromLineage[0]?.id; - if (!toId || !fromId) { - throw Error('Unexpected error: Move failed'); - } - - if (toId === fromLineage[1]?.id) { - throw Error(`Place "${fromLineage[0]?.name}" already has "${toLineage[1]?.name}" as parent`); - } - - const jobData = this.getJobData(fromId, toId, chtApi); - const jobName = this.getJobName(fromLineage[0]?.name, fromLineage[1]?.name, toLineage[1]?.name); - const jobParam: JobParams = { - jobName, - jobData, - }; - await moveContactQueue.add(jobParam); + await queueName.add(jobParam); return { - toLineage, - fromLineage, + destinationLineage, + sourceLineage, success: true }; } - private static getJobName(sourceChpName?: string, sourceChuName?: string, destinationChuName?: string): string { - return `move_[${sourceChpName}]_from_[${sourceChuName}]_to_[${destinationChuName}]`; + public static parseHierarchyAction(action: string = ''): HierarchyAction { + if (!HIERARCHY_ACTIONS.includes(action)) { + throw Error(`invalid action: "${action}"`); + } + + return action as HierarchyAction; } +} - private static getJobData(sourceId: string, destinationId: string, chtApi: ChtApi): ChtConfJobData { - const { authInfo } = chtApi.chtSession; - return { - instanceUrl: `http${authInfo.useHttp ? '' : 's'}://${authInfo.domain}`, - sessionToken: Auth.encodeTokenForWorker(chtApi.chtSession), - action: 'move', - sourceId, - destinationId, - }; +async function getJobDetails(formData: any, contactType: ContactType, sessionCache: SessionCache, chtApi: ChtApi) { + const hierarchyAction = ManageHierarchyLib.parseHierarchyAction(formData.op); + const sourceLineage = await resolve('source_', formData, contactType, sessionCache, chtApi); + const destinationLineage = hierarchyAction === 'delete' ? [] : await resolve('destination_', formData, contactType, sessionCache, chtApi); + + const { sourceId, destinationId } = getSourceAndDestination(); + const jobData = getJobData(hierarchyAction, sourceId, destinationId, chtApi); + const jobName = getJobName(jobData.action, sourceLineage, destinationLineage); + const jobParam: JobParams = { + jobName, + jobData, + }; + + return { + sourceLineage, + destinationLineage, + jobParam + }; + + function getSourceAndDestination() { + if (hierarchyAction === 'move') { + const sourceId = sourceLineage[0]?.id; + const destinationId = destinationLineage[1]?.id; + if (!destinationId || !sourceId) { + throw Error('Unexpected error: Move failed due to missing information'); + } + + if (destinationId === sourceLineage[1]?.id) { + throw Error(`Place "${sourceLineage[0]?.name}" already has "${destinationLineage[1]?.name}" as parent`); + } + + return { sourceId, destinationId }; + } + + if (hierarchyAction === 'merge') { + const sourceId = sourceLineage[0]?.id; + const destinationId = destinationLineage[0]?.id; + if (!destinationId || !sourceId) { + throw Error('Unexpected error: Merge failed due to missing information'); + } + + if (destinationId === sourceId) { + throw Error(`Cannot merge "${destinationId}" with self`); + } + + return { sourceId, destinationId }; + } + + const sourceId = sourceLineage[0]?.id; + if (!sourceId) { + throw Error('Unexpected error: Delete failed due to missing information'); + } + + return { sourceId, destinationId: '' }; } } +function getJobName(action: string, sourceLineage: (RemotePlace | undefined)[], destinationLineage: (RemotePlace | undefined)[]): string { + const sourceDescription = describeLineage(sourceLineage); + const destinationDescription = describeLineage(destinationLineage); + const formattedDestinationDescription = destinationDescription && `_to_[${destinationDescription}]`; + return `${action}_[${sourceDescription}]${formattedDestinationDescription}`; + + function describeLineage(lineage: (RemotePlace | undefined)[]) : string | undefined { + return _.reverse([...lineage]) + .map(element => element?.name) + .filter(Boolean) + .join('.'); + } +} + +function getJobData(action: HierarchyAction, sourceId: string, destinationId: string, chtApi: ChtApi): ChtConfJobData { + const { authInfo } = chtApi.chtSession; + return { + action, + instanceUrl: `http${authInfo.useHttp ? '' : 's'}://${authInfo.domain}`, + sessionToken: Auth.encodeTokenForWorker(chtApi.chtSession), + sourceId, + destinationId, + }; +} + async function resolve(prefix: string, formData: any, contactType: ContactType, sessionCache: SessionCache, chtApi: ChtApi) { const place = new Place(contactType); place.setPropertiesFromFormData(formData, prefix); diff --git a/src/lib/queues.ts b/src/lib/queues.ts index 398495d4..e9529fb3 100644 --- a/src/lib/queues.ts +++ b/src/lib/queues.ts @@ -35,8 +35,8 @@ export class BullQueue implements IQueue { } } -export const getMoveContactQueue = () => new BullQueue( - WorkerConfig.moveContactQueue, +export const getChtConfQueue = () => new BullQueue( + WorkerConfig.queueName, WorkerConfig.redisConnection, WorkerConfig.defaultJobOptions ); diff --git a/src/liquid/place/manage_hierarchy_form.html b/src/liquid/place/manage_hierarchy_form.html index 6558ae60..9ce66b48 100644 --- a/src/liquid/place/manage_hierarchy_form.html +++ b/src/liquid/place/manage_hierarchy_form.html @@ -28,7 +28,7 @@

{{ sourceDescription }}

hierarchy=hierarchy data=data required=hierarchy.required - prefix="from_" + prefix="source_" %} {% endfor %} @@ -43,7 +43,7 @@

{{ destinationDescription }}

hierarchy=hierarchy data=data required=hierarchy.required - prefix="to_" + prefix="destination_" %} {% endfor %} diff --git a/src/plugins/bullmq.ts b/src/plugins/bullmq.ts index dfe59646..34caa8fe 100644 --- a/src/plugins/bullmq.ts +++ b/src/plugins/bullmq.ts @@ -5,7 +5,7 @@ import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'; import { createBullBoard } from '@bull-board/api'; import { FastifyAdapter } from '@bull-board/fastify'; -import { getMoveContactQueue } from '../lib/queues'; +import { getChtConfQueue } from '../lib/queues'; async function bullMQBoardPlugin(fastify: FastifyInstance) { @@ -15,7 +15,7 @@ async function bullMQBoardPlugin(fastify: FastifyInstance) { createBullBoard({ queues: [ new BullMQAdapter( - getMoveContactQueue().bullQueue + getChtConfQueue().bullQueue ), ], serverAdapter, diff --git a/src/routes/manage-hierarchy.ts b/src/routes/manage-hierarchy.ts index 26425767..1471a470 100644 --- a/src/routes/manage-hierarchy.ts +++ b/src/routes/manage-hierarchy.ts @@ -33,7 +33,7 @@ export default async function sessionCache(fastify: FastifyInstance) { const chtApi = new ChtApi(req.chtSession); try { - const result = await ManageHierarchyLib.move(formData, contactType, sessionCache, chtApi); + const result = await ManageHierarchyLib.scheduleJob(formData, contactType, sessionCache, chtApi); const tmplData = { view: 'manage-hierarchy', diff --git a/src/services/hierarchy-view-model.ts b/src/services/hierarchy-view-model.ts index 8ffd9441..68af39c3 100644 --- a/src/services/hierarchy-view-model.ts +++ b/src/services/hierarchy-view-model.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; import { Config, ContactType } from '../config'; -import { HIERARCHY_ACTIONS, HierarchyAction } from '../lib/manage-hierarchy'; +import ManageHierarchyLib, { HIERARCHY_ACTIONS, HierarchyAction } from '../lib/manage-hierarchy'; export function hierarchyViewModel(action: string, contactType: ContactType) { const parentTypeName = contactType.hierarchy.find(h => h.level === 1)?.contact_type; @@ -10,7 +10,7 @@ export function hierarchyViewModel(action: string, contactType: ContactType) { const sourceHierarchy = Config.getHierarchyWithReplacement(contactType, 'desc'); sourceHierarchy[sourceHierarchy.length - 1].friendly_name = contactType.friendly; - const hierarchyAction = getAction(action); + const hierarchyAction = ManageHierarchyLib.parseHierarchyAction(action); const destinationHierarchy = getDestinationHierarchy(); const sourceDescription = hierarchyAction === 'move' ? 'Move This Contact' : 'Delete This Contact'; const destinationDescription = hierarchyAction === 'move' ? 'To Have This Parent' : 'After Moving Data Into'; @@ -34,12 +34,4 @@ export function hierarchyViewModel(action: string, contactType: ContactType) { return _.orderBy(contactType.hierarchy, 'level', 'desc'); } - - function getAction(action: string = ''): HierarchyAction { - if (!HIERARCHY_ACTIONS.includes(action)) { - throw Error(`invalid action: "${action}"`); - } - - return action as HierarchyAction; - } } diff --git a/src/worker/cht-conf-worker.ts b/src/worker/cht-conf-worker.ts index 119dd040..793db9c5 100644 --- a/src/worker/cht-conf-worker.ts +++ b/src/worker/cht-conf-worker.ts @@ -4,11 +4,12 @@ import { Worker, Job, DelayedError, ConnectionOptions, MinimalJob } from 'bullmq import { DateTime } from 'luxon'; import Auth from '../lib/authentication'; +import { HierarchyAction } from '../lib/manage-hierarchy'; export interface ChtConfJobData { sourceId: string; destinationId: string; - action: 'move' | 'merge' | 'delete'; + action: HierarchyAction; sessionToken: string; instanceUrl: string; } diff --git a/src/worker/main.ts b/src/worker/main.ts index 3d02c501..67ed28d8 100644 --- a/src/worker/main.ts +++ b/src/worker/main.ts @@ -6,10 +6,10 @@ import { ChtConfWorker } from './cht-conf-worker'; import { WorkerConfig, checkRedisConnection } from '../config/config-worker'; (async () => { - const { moveContactQueue, redisConnection} = WorkerConfig; + const { queueName, redisConnection} = WorkerConfig; await checkRedisConnection(); ChtConfWorker.processQueue( - moveContactQueue, + queueName, redisConnection ); console.log(`🚀 CHT Conf Worker is listening`); diff --git a/test/integration/manage-hierarchy.spec.ts b/test/integration/manage-hierarchy.spec.ts index 16d8d193..75d146c0 100644 --- a/test/integration/manage-hierarchy.spec.ts +++ b/test/integration/manage-hierarchy.spec.ts @@ -76,7 +76,7 @@ describe('integration/manage-hierarchy', function () { encodeTokenStub.returns('encoded-token'); decodeTokenStub.returns(session); - await MoveLib.move( + await MoveLib.scheduleJob( formData, contactType, sessionCache, chtApi(), moveContactQueue ); @@ -107,7 +107,7 @@ describe('integration/manage-hierarchy', function () { encodeTokenStub.returns('encoded-token'); decodeTokenStub.throws(new Error('Missing WORKER_PRIVATE_KEY')); - await MoveLib.move( + await MoveLib.scheduleJob( formData, contactType, sessionCache, chtApi(), moveContactQueue ); diff --git a/test/lib/manage-hierarchy.spec.ts b/test/lib/manage-hierarchy.spec.ts new file mode 100644 index 00000000..bb8e840f --- /dev/null +++ b/test/lib/manage-hierarchy.spec.ts @@ -0,0 +1,169 @@ +import Chai from 'chai'; +import sinon from 'sinon'; + +import ManageHierarchyLib from '../../src/lib/manage-hierarchy'; +import { Config } from '../../src/config'; +import SessionCache from '../../src/services/session-cache'; +import { mockChtApi } from '../mocks'; + +import chaiAsPromised from 'chai-as-promised'; +import Auth from '../../src/lib/authentication'; +import { BullQueue } from '../../src/lib/queues'; +Chai.use(chaiAsPromised); + +const { expect } = Chai; + +describe('lib/manage-hierarchy.ts', () => { + let chtConfQueue: any; + + beforeEach(() => { + chtConfQueue = sinon.createStubInstance(BullQueue); + sinon.stub(Auth, 'encodeTokenForWorker').returns('encoded-token'); + }); + + afterEach(() => { + sinon.restore(); + }); + + const chtApi = () => mockChtApi( + [ + { id: 'from-sub', name: 'From Sub', lineage: [], type: 'remote' }, + { id: 'to-sub', name: 'To Sub', lineage: [], type: 'remote' } + ], + [ + { id: 'from-chu-id', name: 'c-h-u', lineage: ['from-sub'], type: 'remote' }, + { id: 'to-chu-id', name: 'destination', lineage: ['to-sub'], type: 'remote' } + ], + ); + + describe('move', () => { + it('move CHU: success', async () => { + const formData = { + op: 'move', + source_replacement: 'c-h-u', + source_SUBCOUNTY: 'from sub', + destination_SUBCOUNTY: 'to sub', + }; + const contactType = Config.getContactType('c_community_health_unit'); + const sessionCache = new SessionCache(); + + const actual = await ManageHierarchyLib.scheduleJob(formData, contactType, sessionCache, chtApi(), chtConfQueue); + expect(actual.sourceLineage.map((l:any) => l.id)).to.deep.eq(['from-chu-id', 'from-sub']); + expect(actual.destinationLineage.map((l:any) => l.id)).to.deep.eq([undefined, 'to-sub']); + + // Verify the data passed to mockmoveContactQueue + expect(chtConfQueue.add.calledOnce).to.be.true; + const jobParams = chtConfQueue.add.getCall(0).args[0]; + + expect(jobParams).to.have.property('jobName').that.equals('move_[From Sub.c-h-u]_to_[To Sub]'); + expect(jobParams).to.have.property('jobData').that.deep.include({ + action: 'move', + sourceId: 'from-chu-id', + destinationId: 'to-sub', + instanceUrl: 'http://domain.com', + sessionToken: 'encoded-token', + }); + }); + + it('move CHU: subcounty required', async () => { + const formData = { + op: 'move', + source_replacement: 'c-h-u', + destination_SUBCOUNTY: 'to sub', + }; + const contactType = Config.getContactType('c_community_health_unit'); + const sessionCache = new SessionCache(); + + const actual = ManageHierarchyLib.scheduleJob(formData, contactType, sessionCache, chtApi(), chtConfQueue); + await expect(actual).to.eventually.be.rejectedWith('search string is empty'); + }); + + it('move CHU: cant move to same place', async () => { + const formData = { + op: 'move', + source_replacement: 'c-h-u', + source_SUBCOUNTY: 'from SUB', + destination_SUBCOUNTY: 'from sub', + }; + const contactType = Config.getContactType('c_community_health_unit'); + const sessionCache = new SessionCache(); + + const actual = ManageHierarchyLib.scheduleJob(formData, contactType, sessionCache, chtApi(), chtConfQueue); + await expect(actual).to.eventually.be.rejectedWith('Place "c-h-u" already has "From Sub" as parent'); + }); + + it('move CHU: fail to resolve parent', async () => { + const formData = { + op: 'move', + source_replacement: 'c-h-u', + source_SUBCOUNTY: 'from SUB', + destination_SUBCOUNTY: 'invalid sub', + }; + const contactType = Config.getContactType('c_community_health_unit'); + const sessionCache = new SessionCache(); + + const actual = ManageHierarchyLib.scheduleJob(formData, contactType, sessionCache, chtApi(), chtConfQueue); + await expect(actual).to.eventually.be.rejectedWith('Cannot find \'b_sub_county\' matching \'Invalid Sub\''); + }); + }); + + describe('merge', () => { + it('merge CHU: success', async () => { + const formData = { + op: 'merge', + source_replacement: 'c-h-u', + source_SUBCOUNTY: 'from sub', + destination_SUBCOUNTY: 'to sub', + destination_replacement: 'destination', + }; + const contactType = Config.getContactType('c_community_health_unit'); + const sessionCache = new SessionCache(); + + const actual = await ManageHierarchyLib.scheduleJob(formData, contactType, sessionCache, chtApi(), chtConfQueue); + expect(actual.sourceLineage.map((l:any) => l.id)).to.deep.eq(['from-chu-id', 'from-sub']); + expect(actual.destinationLineage.map((l:any) => l.id)).to.deep.eq(['to-chu-id', 'to-sub']); + + // Verify the data passed to mockmoveContactQueue + expect(chtConfQueue.add.calledOnce).to.be.true; + const jobParams = chtConfQueue.add.getCall(0).args[0]; + + expect(jobParams).to.have.property('jobName').that.equals('merge_[From Sub.c-h-u]_to_[To Sub.destination]'); + expect(jobParams).to.have.property('jobData').that.deep.include({ + action: 'merge', + sourceId: 'from-chu-id', + destinationId: 'to-chu-id', + instanceUrl: 'http://domain.com', + sessionToken: 'encoded-token', + }); + }); + }); + + describe('delete', () => { + it('delete CHU: success', async () => { + const formData = { + op: 'delete', + source_replacement: 'c-h-u', + source_SUBCOUNTY: 'from sub' + }; + const contactType = Config.getContactType('c_community_health_unit'); + const sessionCache = new SessionCache(); + + const actual = await ManageHierarchyLib.scheduleJob(formData, contactType, sessionCache, chtApi(), chtConfQueue); + expect(actual.sourceLineage.map((l:any) => l.id)).to.deep.eq(['from-chu-id', 'from-sub']); + expect(actual.destinationLineage.map((l:any) => l.id)).to.deep.eq([]); + + // Verify the data passed to mockmoveContactQueue + expect(chtConfQueue.add.calledOnce).to.be.true; + const jobParams = chtConfQueue.add.getCall(0).args[0]; + + expect(jobParams).to.have.property('jobName').that.equals('delete_[From Sub.c-h-u]'); + expect(jobParams).to.have.property('jobData').that.deep.include({ + action: 'delete', + sourceId: 'from-chu-id', + instanceUrl: 'http://domain.com', + sessionToken: 'encoded-token', + }); + }); + }); +}); + diff --git a/test/lib/move.spec.ts b/test/lib/move.spec.ts deleted file mode 100644 index cbe0fb02..00000000 --- a/test/lib/move.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -import Chai from 'chai'; -import sinon from 'sinon'; - -import ManageHierarchyLib from '../../src/lib/manage-hierarchy'; -import { Config } from '../../src/config'; -import SessionCache from '../../src/services/session-cache'; -import { mockChtApi } from '../mocks'; - -import chaiAsPromised from 'chai-as-promised'; -import Auth from '../../src/lib/authentication'; -import { BullQueue } from '../../src/lib/queues'; -Chai.use(chaiAsPromised); - -const { expect } = Chai; - -describe('lib/move.ts', () => { - let moveContactQueue: any; - - beforeEach(() => { - moveContactQueue = sinon.createStubInstance(BullQueue); - sinon.stub(Auth, 'encodeTokenForWorker').returns('encoded-token'); - }); - - afterEach(() => { - sinon.restore(); - }); - - const chtApi = () => mockChtApi( - [ - { id: 'from-sub', name: 'From Sub', lineage: [], type: 'remote' }, - { id: 'to-sub', name: 'To Sub', lineage: [], type: 'remote' } - ], - [{ id: 'chu-id', name: 'c-h-u', lineage: ['from-sub'], type: 'remote' }], - ); - - it('move CHU: success', async () => { - const formData = { - from_replacement: 'c-h-u', - from_SUBCOUNTY: 'from sub', - to_SUBCOUNTY: 'to sub', - }; - const contactType = Config.getContactType('c_community_health_unit'); - const sessionCache = new SessionCache(); - - const actual = await ManageHierarchyLib.move(formData, contactType, sessionCache, chtApi(), moveContactQueue); - expect(actual.fromLineage.map((l:any) => l.id)).to.deep.eq(['chu-id', 'from-sub']); - expect(actual.toLineage.map((l:any) => l.id)).to.deep.eq([undefined, 'to-sub']); - - // Verify the data passed to mockmoveContactQueue - expect(moveContactQueue.add.calledOnce).to.be.true; - const jobParams = moveContactQueue.add.getCall(0).args[0]; - - expect(jobParams).to.have.property('jobName').that.equals('move_[c-h-u]_from_[From Sub]_to_[To Sub]'); - expect(jobParams).to.have.property('jobData').that.deep.include({ - sourceId: 'chu-id', - destinationId: 'to-sub', - instanceUrl: 'http://domain.com', - sessionToken: 'encoded-token', - }); - }); - - it('move CHU: subcounty required', async () => { - const formData = { - from_replacement: 'c-h-u', - to_SUBCOUNTY: 'to sub', - }; - const contactType = Config.getContactType('c_community_health_unit'); - const sessionCache = new SessionCache(); - - const actual = ManageHierarchyLib.move(formData, contactType, sessionCache, chtApi(), moveContactQueue); - await expect(actual).to.eventually.be.rejectedWith('search string is empty'); - }); - - it('move CHU: cant move to same place', async () => { - const formData = { - from_replacement: 'c-h-u', - from_SUBCOUNTY: 'from SUB', - to_SUBCOUNTY: 'from sub', - }; - const contactType = Config.getContactType('c_community_health_unit'); - const sessionCache = new SessionCache(); - - const actual = ManageHierarchyLib.move(formData, contactType, sessionCache, chtApi(), moveContactQueue); - await expect(actual).to.eventually.be.rejectedWith('Place "c-h-u" already has "From Sub" as parent'); - }); - - it('move CHU: fail to resolve parent', async () => { - const formData = { - from_replacement: 'c-h-u', - from_SUBCOUNTY: 'from SUB', - to_SUBCOUNTY: 'invalid sub', - }; - const contactType = Config.getContactType('c_community_health_unit'); - const sessionCache = new SessionCache(); - - const actual = ManageHierarchyLib.move(formData, contactType, sessionCache, chtApi(), moveContactQueue); - await expect(actual).to.eventually.be.rejectedWith('Cannot find \'b_sub_county\' matching \'Invalid Sub\''); - }); -}); -