Skip to content

Commit

Permalink
Support for cht-core v3.12 and the cht-script-api (#151)
Browse files Browse the repository at this point in the history
1. Adds support for cht-core v3.12
2. New interface harness.userRoles and associated handles in the "defaults file" for setting the current user's assigned role. I think we might want to improve this later Improved functionality for setting userRoles #152, and if reviewer has any immediate suggestions please indicate.
3. Adds version-aware tests cht-core for cht-scripts-api (works >=v3.12 and doesn't work <v3.12)

Refactoring:

Currently tests for tasks/targets are written against a compiled app_settings.json file without the source. That's tough to work with and tough to understand, so this adds a new project with source for unit tests. For now, it keeps the old tests too.
  • Loading branch information
kennsippell authored Jan 3, 2022
1 parent 9478fcd commit 764ca93
Show file tree
Hide file tree
Showing 26 changed files with 30,007 additions and 44 deletions.
10 changes: 10 additions & 0 deletions all-chts-bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,14 @@ module.exports = {
nootils: require('cht-core-3-11/shared-libs/rules-engine/node_modules/medic-nootils'),
Lineage: require('cht-core-3-11/shared-libs/lineage'),
},
'3.12': {
ddocs: require('./build/cht-core-3-12-ddocs.json'),
RegistrationUtils: require('cht-core-3-12/shared-libs/registration-utils'),
CalendarInterval: require('cht-core-3-12/shared-libs/calendar-interval'),
RulesEngineCore: require('cht-core-3-12/shared-libs/rules-engine'),
RulesEmitter: require('cht-core-3-12/shared-libs/rules-engine/src/rules-emitter'),
nootils: require('cht-core-3-12/shared-libs/rules-engine/node_modules/medic-nootils'),
Lineage: require('cht-core-3-12/shared-libs/lineage'),
ChtScriptApi: require('cht-core-3-12/shared-libs/cht-script-api'),
},
};
23,821 changes: 23,818 additions & 3 deletions dist/all-chts-bundle.dev.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/all-chts-bundle.dev.js.map

Large diffs are not rendered by default.

49 changes: 40 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cht-conf-test-harness",
"version": "2.2.5",
"version": "2.3.0",
"description": "Test Framework for CHT Projects",
"main": "./src/harness.js",
"scripts": {
Expand Down Expand Up @@ -29,6 +29,7 @@
"chai-exclude": "^2.0.2",
"cht-core-3-10": "git+https://github.com/medic/cht-core.git#3.10.x",
"cht-core-3-11": "git+https://github.com/medic/cht-core.git#3.11.x",
"cht-core-3-12": "git+https://github.com/medic/cht-core.git#3.12.x",
"cht-core-3-9": "git+https://github.com/medic/cht-core.git#3.9.x",
"couchdb-compile": "^1.11.0",
"enketo-core": "4.41.6",
Expand All @@ -40,6 +41,7 @@
"openrosa-xpath-evaluator": "^1.5.1",
"raw-loader": "^1.0.0",
"rewire": "^4.0.1",
"semver": "^7.3.5",
"underscore": "^1.10.2",
"webpack": "^5.37.0",
"webpack-cli": "^3.3.0"
Expand Down
51 changes: 35 additions & 16 deletions src/core-adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,16 @@ class CoreAdapter {
this.lineageLib = core.Lineage(Promise, this.pouchdb);
}

async fetchTargets(user, state) {
this.pouchdbStateHash = await prepare(this.core, this.rulesEngine, this.appSettings, this.pouchdb, this.pouchdbStateHash, user, state);
async fetchTargets(user, userRoles, state) {
this.pouchdbStateHash = await prepare(this.core, this.rulesEngine, this.appSettings, this.pouchdb, this.pouchdbStateHash, user, userRoles, state);

const uhcMonthStartDate = getMonthStartDate(this.appSettings);
const relevantInterval = this.core.CalendarInterval.getCurrent(uhcMonthStartDate);
return this.rulesEngine.fetchTargets(relevantInterval);
}

async fetchTasksFor(user, state) {
this.pouchdbStateHash = await prepare(this.core, this.rulesEngine, this.appSettings, this.pouchdb, this.pouchdbStateHash, user, state);
async fetchTasksFor(user, userRoles, state) {
this.pouchdbStateHash = await prepare(this.core, this.rulesEngine, this.appSettings, this.pouchdb, this.pouchdbStateHash, user, userRoles, state);
return this.rulesEngine.fetchTasksFor();
}

Expand Down Expand Up @@ -70,15 +70,16 @@ class CoreAdapter {
}
}

const prepare = async (chtCore, rulesEngine, appSettings, pouchdb, pouchdbStateHash, user, state) => {
await prepareRulesEngine(chtCore, rulesEngine, appSettings, user, pouchdb.name);
const prepare = async (chtCore, rulesEngine, appSettings, pouchdb, pouchdbStateHash, user, userRoles, state) => {
await prepareRulesEngine(chtCore, rulesEngine, appSettings, user, userRoles, pouchdb.name);
const { updatedSubjectIds, newPouchdbState } = await syncPouchWithState(chtCore, pouchdb, pouchdbStateHash, state);
await rulesEngine.updateEmissionsFor(updatedSubjectIds);
return newPouchdbState;
};

const prepareRulesEngine = async (chtCore, rulesEngine, appSettings, user, sessionId) => {
const rulesSettings = getRulesSettings(appSettings, user, sessionId);
const prepareRulesEngine = async (chtCore, rulesEngine, appSettings, user, userRoles, sessionId) => {
const rulesSettings = getRulesSettings(appSettings, user, userRoles, sessionId, chtCore.ChtScriptApi);

if (!rulesEngine.isEnabled()) {
await rulesEngine.initialize(rulesSettings);
} else {
Expand All @@ -92,11 +93,7 @@ const prepareRulesEngine = async (chtCore, rulesEngine, appSettings, user, sessi
*/
if (chtCore.RulesEmitter.isEnabled()) {
chtCore.RulesEmitter.shutdown();
chtCore.RulesEmitter.initialize({
rules: appSettings.tasks.rules,
contact: user,
taskSchedules: rulesSettings.taskSchedules
});
chtCore.RulesEmitter.initialize(rulesSettings);
}
};

Expand Down Expand Up @@ -177,22 +174,44 @@ const getMonthStartDate = settings => {
);
};

const getRulesSettings = (settingsDoc, userContactDoc, sessionId) => {
// cht-core/src/ts/services/cht-script-api.service.ts
const chtScriptApiWithDefaults = (chtScriptApi, settingsDoc, defaultUserRoles) => {
if (!chtScriptApi) {
return;
}

const defaultChtPermissionSettings = settingsDoc.permissions;
return {
v1: {
hasPermissions: (permissions, userRoles = defaultUserRoles, chtPermissionsSettings = defaultChtPermissionSettings) => {
return chtScriptApi.v1.hasPermissions(permissions, userRoles, chtPermissionsSettings);
},
hasAnyPermission: (permissionsGroupList, userRoles = defaultUserRoles, chtPermissionsSettings = defaultChtPermissionSettings) => {
return chtScriptApi.v1.hasAnyPermission(permissionsGroupList, userRoles, chtPermissionsSettings);
}
}
};
};

const getRulesSettings = (settingsDoc, userContactDoc, userRoles, sessionId, chtScriptApi) => {
const settingsTasks = settingsDoc && settingsDoc.tasks || {};
// https://github.com/medic/cht-conf-test-harness/issues/106
// const filterTargetByContext = (target) => target.context ? !!this.parseProvider.parse(target.context)({ user: userContactDoc }) : true;
const targets = settingsTasks.targets && settingsTasks.targets.items || [];

return {
rules: settingsTasks.rules,
taskSchedules: settingsTasks.schedules,
targets: targets,
enableTasks: true,
enableTargets: true,
contact: userContactDoc, // <- this goes to rules emitter
user: { _id: `org.couchdb.user:${userContactDoc ? userContactDoc._id : 'default'}` },
user: {
_id: `org.couchdb.user:${userContactDoc ? userContactDoc._id : 'default'}`,
roles: userRoles,
},
monthStartDate: getMonthStartDate(settingsDoc),
sessionId,
chtScriptApi: chtScriptApiWithDefaults(chtScriptApi, settingsDoc, userRoles),
};
};

Expand Down
4 changes: 2 additions & 2 deletions src/dev-mode/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ module.exports = {
stubbedNoolsLib.pathToProject = pathToProject;
if (!core.RulesEmitter.isMock) {
console.warn('******************************************');
console.warn('**** cht-conf-test-harness dev mode ****');
console.warn('***** cht-conf-test-harness dev mode *****');
console.warn('******************************************');
Object.assign(core.RulesEmitter, devRulesEmitter(core));
}
},
}
};
4 changes: 3 additions & 1 deletion src/dev-mode/mock.cht-conf.nools-lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
* @module mock.cht-conf.nools-lib
*/
module.exports.pathToProject = undefined;
module.exports = function(c, user, Utils, Task, Target, emit) {
module.exports = function(c, user, Utils, chtScriptApi, Task, Target, emit) {
const cacheBefore = Object.keys(require.cache);
try {
global.Utils = Utils;
global.user = user;
global.cht = chtScriptApi;

const tasks = require(`${module.exports.pathToProject}/tasks.js`);
const targets = require(`${module.exports.pathToProject}/targets.js`);
Expand All @@ -25,6 +26,7 @@ module.exports = function(c, user, Utils, Task, Target, emit) {
} finally {
delete global.Utils;
delete global.user;
delete global.cht;

const cacheAfter = Object.keys(require.cache).filter(key => !cacheBefore.includes(key));
cacheAfter.forEach(key => { delete require.cache[key]; });
Expand Down
4 changes: 3 additions & 1 deletion src/dev-mode/mock.rules-engine.rules-emitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const mockNoolsLib = require('./mock.cht-conf.nools-lib');

let enabled = false;
let Utils;
let chtScriptApi;
let user;

module.exports = chtCore => {
Expand All @@ -36,6 +37,7 @@ module.exports = chtCore => {
const settingsDoc = { tasks: { schedules: settings.taskSchedules } };
Utils = chtCore.nootils(settingsDoc);
user = settings.contact;
chtScriptApi = settings.chtScriptApi;

return true;
},
Expand Down Expand Up @@ -81,7 +83,7 @@ module.exports = chtCore => {
};

for (const container of containers) {
mockNoolsLib(container, user, Utils, Task, Target, emitCallback);
mockNoolsLib(container, user, Utils, chtScriptApi, Task, Target, emitCallback);
}

return Promise.resolve(results);
Expand Down
19 changes: 17 additions & 2 deletions src/harness.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class Harness {
* @param {string} [options.harnessDataPath=path.join(options.directory, 'harness.defaults.json')] Path to harness configuration file
* @param {string} [options.coreVersion=harness configuration file] The version of cht-core to emulate @example "3.8.0"
* @param {string} [options.user=harness configuration file] The default {@link HarnessInputs} controlling the environment in which your application is running
* @param {string} [options.userRoles=harness configuration file] The default {@link HarnessInputs} controlling the environment in which your application is running
* @param {string} [options.subject=harness configuration file] The default {@link HarnessInputs} controlling the environment in which your application is running
* @param {Object} [options.content=harness configuration file] The default {@link HarnessInputs} controlling the environment in which your application is running
* @param {Object} [options.contactSummary=harness configuration file] The default {@link HarnessInputs} controlling the environment in which your application is running
Expand Down Expand Up @@ -84,6 +85,7 @@ class Harness {
{
subject: 'default_subject',
user: 'default_user',
userRoles: ['default_role'],
content: { source: 'action' },
docs: [
{ _id: 'default_user', type: 'contact' },
Expand Down Expand Up @@ -321,6 +323,7 @@ class Harness {
* @param {Object=} options Some options when checking for tasks
* @param {string} [options.title=undefined] Filter the returns tasks to those with attribute `title` equal to this value. Filter is skipped if undefined.
* @param {Object} [options.user=Default specified via constructor] The current logged-in user which is viewing the tasks.
* @param {Object} [options.userRoles=Default specified via constructor] The roles associated with the current logged-in user which is viewing the tasks.
* @param {string} [options.actionForm] Filter task documents to only those whose action opens the form equal to this parameter. Filter is skipped if undefined.
* @param {boolean} [options.ownedBySubject] Filter task documents to only those owned by the subject. Filter is skipped if false.
*
Expand All @@ -330,6 +333,7 @@ class Harness {
options = _.defaults(options, {
subject: this.options.subject,
user: this.options.user,
userRoles: this.options.userRoles,
actionForm: this.options.actionForm,
ownedBySubject: this.options.ownedBySubject,
title: undefined,
Expand All @@ -345,7 +349,7 @@ class Harness {

const user = await resolveMock(this.coreAdapter, this.state, options.user);
const subject = await resolveMock(this.coreAdapter, this.state, options.subject, { hydrate: false });
const tasks = await this.coreAdapter.fetchTasksFor(user, stateEnsuringPresenceOfMocks(this.state, user, subject));
const tasks = await this.coreAdapter.fetchTasksFor(user, options.userRoles, stateEnsuringPresenceOfMocks(this.state, user, subject));

tasks.forEach(task => task.emission.actions.forEach(action => {
action.forId = task.emission.forId; // required to hydrate contact in loadAction()
Expand All @@ -359,6 +363,8 @@ class Harness {
*
* @param {Object=} options Some options when summarizing the tasks
* @param {string} [options.title=undefined] Filter task documents counted to only those with emitted `title` equal to this parameter. Filter is skipped if undefined.
* @param {Object} [options.user=Default specified via constructor] The current logged-in user which is viewing the tasks.
* @param {Object} [options.userRoles=Default specified via constructor] The roles associated with the current logged-in user which is viewing the tasks.
* @param {string} [options.actionForm] Filter task documents counted to only those whose action opens the form equal to this parameter. Filter is skipped if undefined.
* @param {boolean} [options.ownedBySubject] Filter task documents counted to only those owned by the subject. Filter is skipped if false.
*
Expand Down Expand Up @@ -405,6 +411,8 @@ class Harness {
* Check the state of targets
* @param {Object=} options Some options for looking for checking for targets
* @param {string|string[]} [options.type=undefined] Filter the returns targets to those with an `id` which matches type (when string) or is included in type (when Array).
* @param {Object} [options.user=Default specified via constructor] The current logged-in user which is viewing the tasks.
* @param {Object} [options.userRoles=Default specified via constructor] The roles associated with the current logged-in user which is viewing the tasks.
*
* @returns {Target[]} An array of targets which would be visible to the user
*/
Expand All @@ -413,6 +421,7 @@ class Harness {
type: undefined,
subject: this.options.subject,
user: this.options.user,
userRoles: this.options.userRoles,
});

if (options.now) {
Expand All @@ -421,7 +430,7 @@ class Harness {

const user = await resolveMock(this.coreAdapter, this.state, options.user);
const subject = await resolveMock(this.coreAdapter, this.state, options.subject, { hydrate: false });
const targets = await this.coreAdapter.fetchTargets(user, stateEnsuringPresenceOfMocks(this.state, user, subject));
const targets = await this.coreAdapter.fetchTargets(user, options.userRoles, stateEnsuringPresenceOfMocks(this.state, user, subject));

return targets
.filter(target =>
Expand Down Expand Up @@ -516,6 +525,12 @@ class Harness {
}
set user(value) { this.options.user = value; }

/**
* `userRoles` from the {@link HarnessInputs} set through the constructor (defaulting to values from harness.defaults.json file)
*/
get userRoles() { return this.options.userRoles; }
set userRoles(value) { this.options.userRoles = value; }

/**
* `coreVersion` is the version of the cht-core that is being emulated in testing (eg. 3.9.0)
*/
Expand Down
4 changes: 4 additions & 0 deletions src/jsdocs.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@
* In harness.getTargets(), this is the global `user` object available in targets.js. (hydrated)
* In harness.getTasks(), this is the global `user` object available in tasks.js. (hydrated)
* In contact-summary code, this is the global `user` object in contact-summary.templated.js. (hydrated)
* @see userRoles For setting the user's role
*
* @property {Array<string>} userRoles This represents the 'roles' assigned to the current user that is logged in. Roles control the user's permissions ([roles documentation](https://docs.communityhealthtoolkit.org/apps/concepts/users/#roles))
* @example harness.userRoles = ['chw']
*
* @property {string|Object} subject This represents the contact that is being "acted on" or the "subject of the test".
* The harness.fillForm() function simulates "completing an action" on the subject's profile page.
* The harness.getTasks() function returns the tasks listed on the subject's profile page.
Expand Down
3 changes: 2 additions & 1 deletion test/app-forms.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ const path = require('path');
const Harness = require('../src/harness');

const harness = new Harness({
directory: path.join(__dirname, 'collateral'),
directory: path.join(__dirname, 'collateral', 'project-without-source'),
xformFolderPath: path.join(__dirname, 'collateral', 'forms'),
harnessDataPath: path.join(__dirname, 'collateral', 'harness.defaults.json'),
verbose: false,
reportFormErrors: false
});
Expand Down
Loading

0 comments on commit 764ca93

Please sign in to comment.