From b110678db9341e62d268d2b8cc67fca1ee02514b Mon Sep 17 00:00:00 2001 From: paulpascal Date: Tue, 19 Mar 2024 12:08:34 +0000 Subject: [PATCH] Support multiple roles on a contact type (#86) * feat(#69): add support for multiple user roles at a level * test(#69): add unit test * doc(#69): update doc * fix(#69): code review feedback 1 * fix(#69): update doc * fix(#69): address review feedback 2 * docs(#69): update readme * fix(#69): delegate validation to role validator * fix(#69): address feedback 3 * fix(#69): address final feedback * 1.1.6 --- README.md | 2 +- package-lock.json | 4 +- package.json | 2 +- src/config/chis-ke/config.json | 4 +- src/config/chis-ug/config.json | 2 +- src/config/index.ts | 23 +++++++++- src/lib/validation.ts | 12 +++++- src/lib/validator-role.ts | 33 ++++++++++++++ src/liquid/components/user_role_property.html | 17 ++++++++ src/liquid/place/bulk_create_form.html | 9 ++++ src/liquid/place/create_form.html | 8 ++++ src/liquid/place/list.html | 13 ++++++ src/routes/add-place.ts | 2 + src/routes/app.ts | 1 + src/routes/events.ts | 1 + src/routes/files.ts | 4 ++ src/routes/search.ts | 1 + src/services/place-factory.ts | 8 ++++ src/services/place.ts | 31 +++++++++++++ src/services/user-payload.ts | 4 +- test/lib/validation.spec.ts | 38 ++++++++++++++++ test/mocks.ts | 4 +- test/services/place.spec.ts | 28 ++++++++++++ test/services/upload-manager.spec.ts | 43 +++++++++++++++++-- 24 files changed, 275 insertions(+), 19 deletions(-) create mode 100644 src/lib/validator-role.ts create mode 100644 src/liquid/components/user_role_property.html diff --git a/README.md b/README.md index ea93962d..429c77ec 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Property | Type | Description `contact_types.name` | string | The name of the contact_type as it [appears in the app's base_settings.json](https://docs.communityhealthtoolkit.org/apps/reference/app-settings/hierarchy/) `contact_types.friendly` | string | Friendly name of the contact type `contact_types.contact_type` | string | The contact_type of the primary contact. [As defined in base_settings.json](https://docs.communityhealthtoolkit.org/apps/reference/app-settings/hierarchy/) -`contact_types.user_role` | string | The [role](https://docs.communityhealthtoolkit.org/apps/reference/app-settings/user-roles/) of the user which is created +`contact_types.user_role` | string[] | A list of allowed [user roles](https://docs.communityhealthtoolkit.org/apps/reference/app-settings/user-roles/). If only one is provided, it will be used by default. `contact_types.username_from_place` | boolean | When true, the username is generated from the place's name. When false, the username is generated from the primary contact's name. Default is false. `contact_types.hierarchy` | Array | Defines how this `contact_type` is connected into the hierarchy. An element with `level:1` (parent) is required and additional elements can be provided to support disambiguation. See [ConfigProperty](#ConfigProperty). `contact_types.hierarchy.level` | integer | The hierarchy element with `level:1` is the parent, `level:3` is the great grandparent. diff --git a/package-lock.json b/package-lock.json index 955b75f4..8ce36631 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cht-user-management", - "version": "1.1.5", + "version": "1.1.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cht-user-management", - "version": "1.1.5", + "version": "1.1.6", "license": "ISC", "dependencies": { "@fastify/autoload": "^5.8.0", diff --git a/package.json b/package.json index dc9eb50d..32bca52f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cht-user-management", - "version": "1.1.5", + "version": "1.1.6", "main": "dist/index.js", "dependencies": { "@fastify/autoload": "^5.8.0", diff --git a/src/config/chis-ke/config.json b/src/config/chis-ke/config.json index 80b5f2fd..a020b24c 100644 --- a/src/config/chis-ke/config.json +++ b/src/config/chis-ke/config.json @@ -211,7 +211,7 @@ "name": "c_community_health_unit", "friendly": "Community Health Unit", "contact_type": "person", - "user_role": "community_health_assistant", + "user_role": ["community_health_assistant"], "username_from_place": true, "deactivate_users_on_replace": false, "hierarchy": [ @@ -282,7 +282,7 @@ "name": "d_community_health_volunteer_area", "friendly": "Community Health Promoter", "contact_type": "person", - "user_role": "community_health_volunteer", + "user_role": ["community_health_volunteer"], "username_from_place": false, "deactivate_users_on_replace": false, "hierarchy": [ diff --git a/src/config/chis-ug/config.json b/src/config/chis-ug/config.json index 46fa7fb5..83b8cc9f 100644 --- a/src/config/chis-ug/config.json +++ b/src/config/chis-ug/config.json @@ -19,7 +19,7 @@ "name": "health_center", "friendly": "VHT Area", "contact_type": "person", - "user_role": "vht", + "user_role": ["vht"], "username_from_place": true, "deactivate_users_on_replace": true, "hierarchy": [ diff --git a/src/config/index.ts b/src/config/index.ts index 6be7a1ce..6fd3040f 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -17,7 +17,7 @@ export type ContactType = { name: string; friendly: string; contact_type: string; - user_role: string; + user_role: string[]; username_from_place: boolean; hierarchy: HierarchyConstraint[]; replacement_property: ContactProperty; @@ -102,6 +102,23 @@ export class Config { ], 'level', sortBy); } + public static getUserRoleConfig(contactType: ContactType): ContactProperty { + return { + friendly_name: 'Role(s)', + property_name: 'role', + type: 'select_role', + required: true, + parameter: contactType.user_role, + }; + } + + public static hasMultipleRoles(contactType: ContactType): boolean { + if (!contactType.user_role.length || contactType.user_role.some(role => !role.trim())) { + throw Error(`unvalidatable config: 'user_role' property is empty or contains empty strings`); + } + return contactType.user_role.length > 1; + } + public static async mutate(payload: PlacePayload, chtApi: ChtApi, isReplacement: boolean): Promise { return partnerConfig.mutate && partnerConfig.mutate(payload, chtApi, isReplacement); } @@ -131,11 +148,13 @@ export class Config { const requiredContactProps = contactType.contact_properties.filter(p => p.required); const requiredPlaceProps = isReplacement ? [] : contactType.place_properties.filter(p => p.required); const requiredHierarchy = contactType.hierarchy.filter(h => h.required); + const requiredUserRole = Config.hasMultipleRoles(contactType) ? [Config.getUserRoleConfig(contactType)] : []; return [ ...requiredHierarchy, ...requiredContactProps, - ...requiredPlaceProps + ...requiredPlaceProps, + ...requiredUserRole ]; } diff --git a/src/lib/validation.ts b/src/lib/validation.ts index 2b20c7ae..f29f7704 100644 --- a/src/lib/validation.ts +++ b/src/lib/validation.ts @@ -1,3 +1,4 @@ +import _ from 'lodash'; import { Config, ContactProperty } from '../config'; import Place from '../services/place'; import RemotePlaceResolver from './remote-place-resolver'; @@ -10,6 +11,7 @@ import ValidatorPhone from './validator-phone'; import ValidatorRegex from './validator-regex'; import ValidatorSkip from './validator-skip'; import ValidatorString from './validator-string'; +import ValidatorRole from './validator-role'; export type ValidationError = { property_name: string; @@ -34,6 +36,7 @@ const TypeValidatorMap: ValidatorMap = { none: new ValidatorSkip(), gender: new ValidatorGender(), dob: new ValidatorDateOfBirth(), + select_role: new ValidatorRole(), }; export class Validation { @@ -42,7 +45,8 @@ export class Validation { const result = [ ...Validation.validateHierarchy(place), ...Validation.validateProperties(place.properties, place.type.place_properties, requiredColumns, 'place_'), - ...Validation.validateProperties(place.contact.properties, place.type.contact_properties, requiredColumns, 'contact_') + ...Validation.validateProperties(place.contact.properties, place.type.contact_properties, requiredColumns, 'contact_'), + ...Validation.validateProperties(place.userRoleProperties, [Config.getUserRoleConfig(place.type)], requiredColumns, 'user_') ]; return result; @@ -114,7 +118,7 @@ export class Validation { for (const property of properties) { const value = obj[property.property_name]; - const isRequired = requiredProperties.includes(property); + const isRequired = this.isRequiredProperty(property, requiredProperties); if (value || isRequired) { const isValid = Validation.isValid(property, value); if (isValid === false || typeof isValid === 'string') { @@ -176,4 +180,8 @@ export class Validation { return `Cannot find '${friendlyType}' matching '${searchStr}'${requiredParentSuffix}`; } + + private static isRequiredProperty(property: ContactProperty, requiredColumns: ContactProperty[]): boolean { + return requiredColumns.some((prop) => _.isEqual(prop, property)); + } } diff --git a/src/lib/validator-role.ts b/src/lib/validator-role.ts new file mode 100644 index 00000000..de2e2a0c --- /dev/null +++ b/src/lib/validator-role.ts @@ -0,0 +1,33 @@ +import _ from 'lodash'; +import { ContactProperty } from '../config'; +import { IValidator } from './validation'; + +export default class ValidatorRole implements IValidator { + isValid(input: string, property: ContactProperty): boolean | string { + const allowedRoles = property.parameter as string[]; + + // Check if user roles are specified and not empty + const selectedRoles = input ? input.split(' ').map((role: string) => role.trim()).filter(Boolean) : []; + if (!selectedRoles.length) { + return `Should provide at least one role`; + } + + // Check if all provided roles are allowed + const invalidRoles = _.difference(selectedRoles, allowedRoles); + if (invalidRoles.length === 1) { + return `Role '${invalidRoles[0]}' is not allowed`; + } else if (invalidRoles.length > 1) { + return `Roles '${invalidRoles.join(', ')}' are not allowed`; + } + + return true; + } + + format(input: string): string { + return input; + } + + get defaultError(): string { + return 'Invalid user roles'; + } +} diff --git a/src/liquid/components/user_role_property.html b/src/liquid/components/user_role_property.html new file mode 100644 index 00000000..e05d7f13 --- /dev/null +++ b/src/liquid/components/user_role_property.html @@ -0,0 +1,17 @@ +{% capture prop_name %}{{ include.prefix }}{{include.prop.property_name}}{% endcapture %} +
+ +
+
+ +
+
+
\ No newline at end of file diff --git a/src/liquid/place/bulk_create_form.html b/src/liquid/place/bulk_create_form.html index ca2a790d..13a87d0b 100644 --- a/src/liquid/place/bulk_create_form.html +++ b/src/liquid/place/bulk_create_form.html @@ -79,6 +79,15 @@ Property on primary contact {% endfor %} + + {% if contactType.user_role.size > 1 %} + + {{ userRoleProperty.friendly_name }} + {{ userRoleProperty.required }} + Role of the user. Separate multiple roles with a space. Allowed roles are {{ contactType.user_role | join: ', ' }} + + + {% endif %} diff --git a/src/liquid/place/create_form.html b/src/liquid/place/create_form.html index 820df4db..1aa51770 100644 --- a/src/liquid/place/create_form.html +++ b/src/liquid/place/create_form.html @@ -30,6 +30,14 @@ {% endfor %} + {% if contactType.user_role.size > 1 %} +
+ {% + include "components/user_role_property.html" prefix="user_" prop=userRoleProperty + %} +
+ {% endif %} +
diff --git a/src/liquid/place/list.html b/src/liquid/place/list.html index 07ee2289..46d45acb 100644 --- a/src/liquid/place/list.html +++ b/src/liquid/place/list.html @@ -18,6 +18,9 @@

{{contactType.friendly}}

{% for contact_property in contactType.contact_properties %} {{contact_property.friendly_name}} {% endfor %} + {% if contactType.user_role.size > 1 %} + {{ contactType.userRoleProperty.friendly_name }} + {% endif %} @@ -57,6 +60,16 @@

{{contactType.friendly}}

%} {% endfor %} + {% if contactType.user_role.size > 1 %} + {% capture propertyName %}user_{{ contactType.userRoleProperty.property_name }}{% endcapture %} + {% + include "components/list_cell.html" + propertyName=propertyName + property=contactType.userRoleProperty + values=place.userRoleProperties + %} + {% endif %} + {% capture tag_text %}{% if place.validationErrors == empty %}{{ place.state }}{% else %}INVALID{% endif %}{% endcapture %} {% capture tag_class %} diff --git a/src/routes/add-place.ts b/src/routes/add-place.ts index 2e896a06..7621c197 100644 --- a/src/routes/add-place.ts +++ b/src/routes/add-place.ts @@ -25,6 +25,7 @@ export default async function addPlace(fastify: FastifyInstance) { hierarchy: Config.getHierarchyWithReplacement(contactType, 'desc'), contactType, contactTypes, + userRoleProperty: Config.getUserRoleConfig(contactType) }; return resp.view('src/liquid/app/view.html', tmplData); @@ -91,6 +92,7 @@ export default async function addPlace(fastify: FastifyInstance) { contactTypes: Config.contactTypes(), backend: `/place/edit/${id}`, data, + userRoleProperty: Config.getUserRoleConfig(place.type) }; resp.header('HX-Push-Url', `/place/edit/${id}`); diff --git a/src/routes/app.ts b/src/routes/app.ts index 464e31aa..678ec817 100644 --- a/src/routes/app.ts +++ b/src/routes/app.ts @@ -24,6 +24,7 @@ export default async function sessionCache(fastify: FastifyInstance) { ...item, places: sessionCache.getPlaces({ type: item.name }), hierarchy: Config.getHierarchyWithReplacement(item, 'desc'), + userRoleProperty: Config.getUserRoleConfig(item), }; }); diff --git a/src/routes/events.ts b/src/routes/events.ts index 9c913d1e..de47f30c 100644 --- a/src/routes/events.ts +++ b/src/routes/events.ts @@ -12,6 +12,7 @@ export default async function events(fastify: FastifyInstance) { ...item, places: sessionCache.getPlaces({ type: item.name }), hierarchy: Config.getHierarchyWithReplacement(item, 'desc'), + userRoleProperty: Config.getUserRoleConfig(item), })); return resp.view('src/liquid/place/list_event.html', { diff --git a/src/routes/files.ts b/src/routes/files.ts index 832ed473..8d3137e0 100644 --- a/src/routes/files.ts +++ b/src/routes/files.ts @@ -11,10 +11,12 @@ export default async function files(fastify: FastifyInstance) { const placeType = params.placeType; const placeTypeConfig = Config.getContactType(placeType); const hierarchy = Config.getHierarchyWithReplacement(placeTypeConfig); + const userRoleConfig = Config.getUserRoleConfig(placeTypeConfig); const columns = _.uniq([ ...hierarchy.map(p => p.friendly_name), ...placeTypeConfig.place_properties.map(p => p.friendly_name), ...placeTypeConfig.contact_properties.map(p => p.friendly_name), + ...(Config.hasMultipleRoles(placeTypeConfig) ? [userRoleConfig.friendly_name] : []), ]); return stringify([columns]); @@ -35,6 +37,7 @@ export default async function files(fastify: FastifyInstance) { place.contact.properties.phone, place.creationDetails.username, place.creationDetails.password, + place.userRoles.join(' ') ]); const constraints = Config.getHierarchyWithReplacement(contactType); const props = Object.keys(places[0].hierarchyProperties).map(prop => constraints.find(c => c.property_name === prop)!.friendly_name); @@ -45,6 +48,7 @@ export default async function files(fastify: FastifyInstance) { 'phone', 'username', 'password', + 'role' ]; zip.file( `${contactType.name}.csv`, diff --git a/src/routes/search.ts b/src/routes/search.ts index 5a0e652a..2ad7a57e 100644 --- a/src/routes/search.ts +++ b/src/routes/search.ts @@ -76,6 +76,7 @@ export default async function place(fastify: FastifyInstance) { place, contactType, hierarchy: Config.getHierarchyWithReplacement(contactType, 'desc'), + userRoleProperty: Config.getUserRoleConfig(contactType), ...moveModel, }; diff --git a/src/services/place-factory.ts b/src/services/place-factory.ts index acd4baf7..ab132df9 100644 --- a/src/services/place-factory.ts +++ b/src/services/place-factory.ts @@ -70,6 +70,14 @@ export default class PlaceFactory { const columnIndex = csvColumns.indexOf(hierarchyConstraint.friendly_name); place.hierarchyProperties[hierarchyConstraint.property_name] = row[columnIndex]; } + + if (Config.hasMultipleRoles(contactType)) { + const userRoleProperty = Config.getUserRoleConfig(contactType); + place.userRoleProperties[userRoleProperty.property_name] = row[ + csvColumns.indexOf(userRoleProperty.friendly_name) + ]; + } + places.push(place); } count++; diff --git a/src/services/place.ts b/src/services/place.ts index a65ec255..ea20cd8e 100644 --- a/src/services/place.ts +++ b/src/services/place.ts @@ -26,6 +26,8 @@ export enum PlaceUploadState { const PLACE_PREFIX = 'place_'; const CONTACT_PREFIX = 'contact_'; +const USER_PREFIX = 'user_'; + export default class Place { public readonly id: string; @@ -45,6 +47,10 @@ export default class Place { [key: string]: any; }; + public userRoleProperties: { + [key: string]: any; + }; + public state : PlaceUploadState; public validationErrors?: { [key: string]: string }; @@ -58,6 +64,7 @@ export default class Place { this.hierarchyProperties = {}; this.state = PlaceUploadState.STAGED; this.resolvedHierarchy = []; + this.userRoleProperties = {}; } /* @@ -87,6 +94,19 @@ export default class Place { const propertyName = hierarchyLevel.property_name; this.hierarchyProperties[propertyName] = formData[`${hierarchyPrefix}${propertyName}`] ?? ''; } + + if (Config.hasMultipleRoles(this.type)) { + const userRoleConfig = Config.getUserRoleConfig(this.type); + const propertyName = userRoleConfig.property_name; + const roleFormData = formData[`${USER_PREFIX}${propertyName}`]; + + // When multiple are selected, the form data is an array + if (Array.isArray(roleFormData)) { + this.userRoleProperties[propertyName] = roleFormData.join(' '); + } else { + this.userRoleProperties[propertyName] = roleFormData; + } + } } /** @@ -109,6 +129,7 @@ export default class Place { ...addPrefixToPropertySet(this.hierarchyProperties, hierarchyPrefix), ...addPrefixToPropertySet(this.properties, PLACE_PREFIX), ...addPrefixToPropertySet(this.contact.properties, CONTACT_PREFIX), + ...addPrefixToPropertySet(this.userRoleProperties, USER_PREFIX), }; } @@ -216,6 +237,16 @@ export default class Place { return username; } + public get userRoles(): string[] { + if (!Config.hasMultipleRoles(this.type)) { + return this.type.user_role; + } + + const userRoleConfig = Config.getUserRoleConfig(this.type); + const roles = this.userRoleProperties[userRoleConfig.property_name]; + return roles.split(' ').map((role: string) => role.trim()).filter(Boolean); + } + public get hasValidationErrors() : boolean { return Object.keys(this.validationErrors as any).length > 0; } diff --git a/src/services/user-payload.ts b/src/services/user-payload.ts index 2493b796..fb525cdf 100644 --- a/src/services/user-payload.ts +++ b/src/services/user-payload.ts @@ -4,16 +4,16 @@ import Place from './place'; export class UserPayload { public password: string; public username: string; - public type: string; public place: string; public contact: string; public fullname: string; public phone: string; + public roles: string[]; constructor(place: Place, placeId: string, contactId: string) { this.username = place.generateUsername(); this.password = this.generatePassword(); - this.type = place.type.user_role; + this.roles = place.userRoles; this.place = placeId; this.contact = contactId; this.fullname = place.contact.name; diff --git a/test/lib/validation.spec.ts b/test/lib/validation.spec.ts index 29d88337..7b9edf35 100644 --- a/test/lib/validation.spec.ts +++ b/test/lib/validation.spec.ts @@ -148,5 +148,43 @@ describe('lib/validation.ts', () => { description: `Cannot find 'contacttype-name' matching 'Sin Bad' under 'Parent'`, }]); }); + + it('user_role property empty throws', () => { + const contactType = mockSimpleContactType('string', undefined); + contactType.user_role = []; + + const place = mockPlace(contactType, 'prop'); + + expect(() => Validation.getValidationErrors(place)).to.throw('unvalidatable'); + }); + + it('user_role property contains empty string throws', () => { + const contactType = mockSimpleContactType('string', undefined); + contactType.user_role = ['']; + + const place = mockPlace(contactType, 'prop'); + + expect(() => Validation.getValidationErrors(place)).to.throw('unvalidatable'); + }); + + it('user role is invalid when not allowed', () => { + const contactType = mockSimpleContactType('string', undefined); + contactType.user_role = ['supervisor', 'stock_manager']; + + const place = mockPlace(contactType, 'prop'); + + const formData = { + place_prop: 'abc', + contact_prop: 'efg', + garbage: 'ghj', + user_role: 'supervisor stockmanager', + }; + place.setPropertiesFromFormData(formData); + + expect(Validation.getValidationErrors(place)).to.deep.eq([{ + property_name: 'user_role', + description: `Role 'stockmanager' is not allowed`, + }]); + }); }); diff --git a/test/mocks.ts b/test/mocks.ts index cf7fa069..913713bf 100644 --- a/test/mocks.ts +++ b/test/mocks.ts @@ -42,7 +42,7 @@ export const mockSimpleContactType = ( name: 'contacttype-name', friendly: 'friendly', contact_type: 'contact-type', - user_role: 'role', + user_role: ['role'], username_from_place: false, hierarchy: [ { @@ -64,7 +64,7 @@ export const mockValidContactType = (propertyType, propertyValidator: string | s name: 'contacttype-name', friendly: 'friendly', contact_type: 'contact-type', - user_role: 'role', + user_role: ['role'], username_from_place: false, hierarchy: [ { diff --git a/test/services/place.spec.ts b/test/services/place.spec.ts index fc540057..4d172447 100644 --- a/test/services/place.spec.ts +++ b/test/services/place.spec.ts @@ -135,4 +135,32 @@ describe('services/place.ts', () => { expect(actual.contact.type).to.eq(contactType.contact_type); expect(actual.contact.contact_type).to.be.undefined; }); + + it('setPropertiesFromFormData supports multiple roles', () => { + const contactType = mockSimpleContactType('string', undefined); + contactType.user_role = ['role1', 'role2']; + contactType.contact_properties = contactType.place_properties; + const place = new Place(contactType); + place.properties.existing = 'existing'; + + const formData = { + place_prop: 'abc', + contact_prop: 'efg', + garbage: 'ghj', + user_role: 'role1 role2', + }; + place.setPropertiesFromFormData(formData); + + expect(place.properties).to.deep.eq({ + existing: 'existing', + prop: 'abc', + }); + expect(place.contact.properties).to.deep.eq({ + prop: 'efg', + }); + expect(place.userRoles).to.deep.eq([ + 'role1', + 'role2', + ]); + }); }); diff --git a/test/services/upload-manager.spec.ts b/test/services/upload-manager.spec.ts index 09021396..e8502dfb 100644 --- a/test/services/upload-manager.spec.ts +++ b/test/services/upload-manager.spec.ts @@ -42,7 +42,7 @@ describe('services/upload-manager.ts', () => { expect(userPayload).to.deep.include({ contact: 'created-contact-id', place: 'created-place-id', - type: 'role', + roles: ['role'], username: 'contact', }); expect(chtApi.deleteDoc.called).to.be.false; @@ -218,8 +218,8 @@ describe('services/upload-manager.ts', () => { expect(chp.isCreated).to.be.true; // chu is created first - expect(chtApi.createUser.args[0][0].type).to.eq('community_health_assistant'); - expect(chtApi.createUser.args[1][0].type).to.eq('community_health_volunteer'); + expect(chtApi.createUser.args[0][0].roles).to.deep.eq(['community_health_assistant']); + expect(chtApi.createUser.args[1][0].roles).to.deep.eq(['community_health_volunteer']); const cachedChus = await RemotePlaceCache.getPlacesWithType(chtApi, chu.type.name); expect(cachedChus).to.have.property('length', 1); @@ -281,6 +281,41 @@ describe('services/upload-manager.ts', () => { expect(chtApi.createUser.callCount).to.be.gt(2); // retried expect(chu.uploadError).to.include('could not create user'); }); + + it('mock data is properly sent to chtApi (multiple roles)', async () => { + const { fakeFormData, contactType, chtApi, sessionCache, remotePlace } = await createMocks(); + + contactType.user_role = ['role1', 'role2']; + fakeFormData.user_role = 'role1 role2'; + + const place = await PlaceFactory.createOne(fakeFormData, contactType, sessionCache, chtApi); + + const uploadManager = new UploadManager(); + await uploadManager.doUpload([place], chtApi); + + expect(chtApi.createPlace.calledOnce).to.be.true; + const placePayload = chtApi.createPlace.args[0][0]; + expect(placePayload).to.nested.include({ + 'contact.contact_type': contactType.contact_type, + 'contact.name': 'contact', + prop: 'foo', + name: 'Place Community Health Unit', + parent: remotePlace.id, + contact_type: contactType.name, + }); + expect(chtApi.updateContactParent.calledOnce).to.be.true; + expect(chtApi.updateContactParent.args[0]).to.deep.eq(['created-place-id']); + + expect(chtApi.createUser.calledOnce).to.be.true; + const userPayload = chtApi.createUser.args[0][0]; + expect(userPayload).to.deep.include({ + contact: 'created-contact-id', + place: 'created-place-id', + roles: ['role1', 'role2'], + username: 'contact', + }); + expect(place.isCreated).to.be.true; + }); }); async function createChu(remotePlace: RemotePlace, chu_name: string, sessionCache: any, chtApi: ChtApi) { @@ -332,7 +367,7 @@ async function createMocks() { place_name: 'place', place_prop: 'foo', hierarchy_PARENT: remotePlace.name, - contact_name: 'contact', + contact_name: 'contact' }; return { fakeFormData, contactType, sessionCache, chtApi, remotePlace };