diff --git a/bin/test-backend-ci b/bin/test-backend-ci index f7ead1634d..34c5366312 100755 --- a/bin/test-backend-ci +++ b/bin/test-backend-ci @@ -41,7 +41,7 @@ main(){ check_exit "$?" # then list through the folders and run the tests - targets=("lib" "middleware" "models" "policies" "routes" "scopes" "services" "tools" "widgets" "goalServices") + targets=("lib" "middleware" "models" "policies" "routes" "scopes" "services" "tools" "widgets" "goalServices" "hooks") for target in "${targets[@]}"; do # jest command to diff --git a/package.json b/package.json index 124e19f053..6c372e2bac 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "db:seed:prod": "node_modules/.bin/sequelize db:seed:all --options-path .production.sequelizerc", "db:seed:undo": "node_modules/.bin/sequelize db:seed:undo:all", "db:seed:undo:prod": "node_modules/.bin/sequelize db:seed:undo:all --options-path .production.sequelizerc", - "docker:deps": "docker-compose run --rm backend yarn install && docker-compose run --rm frontend yarn install", + "docker:deps": "docker-compose run --rm backend yarn install && docker-compose run --rm frontend yarn install && docker-compose run --rm testingonly yarn install", "docker:reset": "./bin/reset-all", "docker:start": "docker-compose up", "docker:start:debug": "docker-compose --compatibility -f docker-compose.yml -f docker-compose.debug.yml up", diff --git a/packages/common/README.md b/packages/common/README.md index 69dab3c1c5..953d4407d8 100644 --- a/packages/common/README.md +++ b/packages/common/README.md @@ -21,6 +21,9 @@ Note: On Windows you will need to use `yarn add @ttahub/common@1.x.0` to update ## Versions +## 2.0.19 +Add "escapeHTML" function + ## 1.4.0 Add SUPPORT_TYPE diff --git a/packages/common/package.json b/packages/common/package.json index 7bf6be5b96..777f567aea 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@ttahub/common", - "version": "2.0.18", + "version": "2.1.2", "description": "The purpose of this package is to reduce code duplication between the frontend and backend projects.", "main": "src/index.js", "author": "", diff --git a/packages/common/src/utils.js b/packages/common/src/utils.js index 24b27a71b2..5c5ca99e1f 100644 --- a/packages/common/src/utils.js +++ b/packages/common/src/utils.js @@ -31,5 +31,5 @@ function determineMergeGoalStatus(statuses) { } module.exports = { - determineMergeGoalStatus + determineMergeGoalStatus, } \ No newline at end of file diff --git a/src/models/hooks/activityReport.js b/src/hooks/activityReport.js similarity index 98% rename from src/models/hooks/activityReport.js rename to src/hooks/activityReport.js index 900e3a8da6..818bbf05ef 100644 --- a/src/models/hooks/activityReport.js +++ b/src/hooks/activityReport.js @@ -1,3 +1,5 @@ +import escapeFields from '../models/helpers/escapeFields'; + const { Op } = require('sequelize'); const { REPORT_STATUSES } = require('@ttahub/common'); const { @@ -5,27 +7,29 @@ const { AWS_ELASTIC_SEARCH_INDEXES, GOAL_COLLABORATORS, OBJECTIVE_COLLABORATORS, -} = require('../../constants'); -const { auditLogger } = require('../../logger'); +} = require('../constants'); +const { auditLogger } = require('../logger'); const { findOrCreateGoalTemplate } = require('./goal'); -const { GOAL_STATUS } = require('../../constants'); +const { GOAL_STATUS } = require('../constants'); const { findOrCreateObjectiveTemplate } = require('./objective'); const { scheduleUpdateIndexDocumentJob, scheduleDeleteIndexDocumentJob, -} = require('../../lib/awsElasticSearch/queueManager'); -const { collectModelData } = require('../../lib/awsElasticSearch/datacollector'); -const { formatModelForAwsElasticsearch } = require('../../lib/awsElasticSearch/modelMapper'); -const { addIndexDocument, deleteIndexDocument } = require('../../lib/awsElasticSearch/index'); +} = require('../lib/awsElasticSearch/queueManager'); +const { collectModelData } = require('../lib/awsElasticSearch/datacollector'); +const { formatModelForAwsElasticsearch } = require('../lib/awsElasticSearch/modelMapper'); +const { addIndexDocument, deleteIndexDocument } = require('../lib/awsElasticSearch/index'); const { findOrCreateCollaborator, removeCollaboratorsForType, -} = require('../helpers/genericCollaborator'); +} = require('../models/helpers/genericCollaborator'); const { destroyLinkedSimilarityGroups } = require('./activityReportGoal'); +const AR_FIELDS_TO_ESCAPE = ['additionalNotes', 'context']; + const processForEmbeddedResources = async (sequelize, instance, options) => { // eslint-disable-next-line global-require - const { calculateIsAutoDetectedForActivityReport, processActivityReportForResourcesById } = require('../../services/resource'); + const { calculateIsAutoDetectedForActivityReport, processActivityReportForResourcesById } = require('../services/resource'); const changed = instance.changed() || Object.keys(instance); if (calculateIsAutoDetectedForActivityReport(changed)) { await processActivityReportForResourcesById(instance.id); @@ -816,6 +820,7 @@ const automaticGoalObjectiveStatusCachingOnApproval = async (sequelize, instance const beforeCreate = async (instance) => { copyStatus(instance); + escapeFields(instance, AR_FIELDS_TO_ESCAPE); }; const getActivityReportDocument = async (sequelize, instance) => { @@ -1033,6 +1038,7 @@ const beforeValidate = async (sequelize, instance, options) => { }; const beforeUpdate = async (sequelize, instance, options) => { + escapeFields(instance, AR_FIELDS_TO_ESCAPE); copyStatus(instance); setSubmittedDate(sequelize, instance, options); clearAdditionalNotes(sequelize, instance, options); diff --git a/src/models/hooks/activityReport.test.js b/src/hooks/activityReport.test.js similarity index 98% rename from src/models/hooks/activityReport.test.js rename to src/hooks/activityReport.test.js index c0aaaa81a2..86701385ca 100644 --- a/src/models/hooks/activityReport.test.js +++ b/src/hooks/activityReport.test.js @@ -12,16 +12,16 @@ import db, { Recipient, Grant, User, -} from '..'; -import { unlockReport } from '../../routes/activityReports/handlers'; -import ActivityReportPolicy from '../../policies/activityReport'; +} from '../models'; +import { unlockReport } from '../routes/activityReports/handlers'; +import ActivityReportPolicy from '../policies/activityReport'; import { moveDraftGoalsToNotStartedOnSubmission, propagateSubmissionStatus, } from './activityReport'; -import { auditLogger } from '../../logger'; +import { auditLogger } from '../logger'; -jest.mock('../../policies/activityReport'); +jest.mock('../policies/activityReport'); describe('activity report model hooks', () => { describe('automatic goal status changes', () => { diff --git a/src/models/hooks/activityReportApprover.js b/src/hooks/activityReportApprover.js similarity index 92% rename from src/models/hooks/activityReportApprover.js rename to src/hooks/activityReportApprover.js index 6a70e7d77d..b7aafb3321 100644 --- a/src/models/hooks/activityReportApprover.js +++ b/src/hooks/activityReportApprover.js @@ -1,4 +1,7 @@ const { APPROVER_STATUSES, REPORT_STATUSES } = require('@ttahub/common'); +const { default: escapeFields } = require('../models/helpers/escapeFields'); + +const FIELDS_TO_ESCAPE = ['note']; /** * Helper function called by model hooks. @@ -124,9 +127,19 @@ const afterUpdate = async (sequelize, instance) => { await updateReportStatus(sequelize, instance); }; +const beforeUpdate = async (_sequelize, instance) => { + escapeFields(instance, FIELDS_TO_ESCAPE); +}; + +const beforeCreate = async (_sequelize, instance) => { + escapeFields(instance, FIELDS_TO_ESCAPE); +}; + export { calculateReportStatusFromApprovals, calculateReportStatus, + beforeCreate, + beforeUpdate, afterCreate, afterDestroy, afterRestore, diff --git a/src/models/hooks/activityReportApprover.test.js b/src/hooks/activityReportApprover.test.js similarity index 99% rename from src/models/hooks/activityReportApprover.test.js rename to src/hooks/activityReportApprover.test.js index 05e9ab826b..f67351e2b4 100644 --- a/src/models/hooks/activityReportApprover.test.js +++ b/src/hooks/activityReportApprover.test.js @@ -4,7 +4,7 @@ import { User, ActivityReport, ActivityReportApprover, -} from '..'; +} from '../models'; import { calculateReportStatusFromApprovals, afterDestroy, diff --git a/src/models/hooks/activityReportFile.js b/src/hooks/activityReportFile.js similarity index 91% rename from src/models/hooks/activityReportFile.js rename to src/hooks/activityReportFile.js index d85f03631b..322b11d9b0 100644 --- a/src/models/hooks/activityReportFile.js +++ b/src/hooks/activityReportFile.js @@ -1,7 +1,7 @@ import { REPORT_STATUSES } from '@ttahub/common'; import { propagateDestroyToFile } from './genericFile'; -const { cleanupOrphanFiles } = require('../helpers/orphanCleanupHelper'); +const { cleanupOrphanFiles } = require('../models/helpers/orphanCleanupHelper'); const checkForUseOnApprovedReport = async (sequelize, instance, options) => { const activityReport = await sequelize.models.ActivityReport.findOne({ diff --git a/src/models/hooks/activityReportFile.test.js b/src/hooks/activityReportFile.test.js similarity index 94% rename from src/models/hooks/activityReportFile.test.js rename to src/hooks/activityReportFile.test.js index ed1a594e1a..609518c012 100644 --- a/src/models/hooks/activityReportFile.test.js +++ b/src/hooks/activityReportFile.test.js @@ -7,16 +7,16 @@ import { sequelize, User, ActivityReport, -} from '..'; +} from '../models'; import { propagateDestroyToFile } from './genericFile'; -import { cleanupOrphanFiles } from '../helpers/orphanCleanupHelper'; +import { cleanupOrphanFiles } from '../models/helpers/orphanCleanupHelper'; import { draftObject, mockApprovers, approverUserIds } from './testHelpers'; jest.mock('./genericFile', () => ({ propagateDestroyToFile: jest.fn(), })); -jest.mock('../helpers/orphanCleanupHelper', () => ({ +jest.mock('../models/helpers/orphanCleanupHelper', () => ({ cleanupOrphanFiles: jest.fn(), })); diff --git a/src/models/hooks/activityReportGoal.js b/src/hooks/activityReportGoal.js similarity index 96% rename from src/models/hooks/activityReportGoal.js rename to src/hooks/activityReportGoal.js index f4afcddde6..e3dad95305 100644 --- a/src/models/hooks/activityReportGoal.js +++ b/src/hooks/activityReportGoal.js @@ -1,14 +1,14 @@ const { REPORT_STATUSES } = require('@ttahub/common'); -const { GOAL_COLLABORATORS } = require('../../constants'); +const { GOAL_COLLABORATORS } = require('../constants'); const { currentUserPopulateCollaboratorForType, removeCollaboratorsForType, -} = require('../helpers/genericCollaborator'); -const { onlyAllowTrGoalSourceForGoalsCreatedViaTr } = require('../helpers/goalSource'); +} = require('../models/helpers/genericCollaborator'); +const { onlyAllowTrGoalSourceForGoalsCreatedViaTr } = require('../models/helpers/goalSource'); const processForEmbeddedResources = async (sequelize, instance, options) => { // eslint-disable-next-line global-require - const { calculateIsAutoDetectedForActivityReportGoal, processActivityReportGoalForResourcesById } = require('../../services/resource'); + const { calculateIsAutoDetectedForActivityReportGoal, processActivityReportGoalForResourcesById } = require('../services/resource'); const changed = instance.changed() || Object.keys(instance); if (calculateIsAutoDetectedForActivityReportGoal(changed)) { await processActivityReportGoalForResourcesById(instance.id); diff --git a/src/models/hooks/activityReportGoal.test.js b/src/hooks/activityReportGoal.test.js similarity index 100% rename from src/models/hooks/activityReportGoal.test.js rename to src/hooks/activityReportGoal.test.js diff --git a/src/models/hooks/activityReportGoalFieldResponse.js b/src/hooks/activityReportGoalFieldResponse.js similarity index 100% rename from src/models/hooks/activityReportGoalFieldResponse.js rename to src/hooks/activityReportGoalFieldResponse.js diff --git a/src/models/hooks/activityReportGoalResource.js b/src/hooks/activityReportGoalResource.js similarity index 94% rename from src/models/hooks/activityReportGoalResource.js rename to src/hooks/activityReportGoalResource.js index 4801c8bbcd..605572a85b 100644 --- a/src/models/hooks/activityReportGoalResource.js +++ b/src/hooks/activityReportGoalResource.js @@ -1,5 +1,5 @@ -const { getSingularOrPluralData } = require('../helpers/hookMetadata'); -const { cleanupOrphanResources } = require('../helpers/orphanCleanupHelper'); +const { getSingularOrPluralData } = require('../models/helpers/hookMetadata'); +const { cleanupOrphanResources } = require('../models/helpers/orphanCleanupHelper'); const propagateOnAR = async (sequelize, instance, options) => sequelize.models.GoalResource .update( diff --git a/src/models/hooks/activityReportGoalResource.test.js b/src/hooks/activityReportGoalResource.test.js similarity index 99% rename from src/models/hooks/activityReportGoalResource.test.js rename to src/hooks/activityReportGoalResource.test.js index 51589f5881..56f37cef1e 100644 --- a/src/models/hooks/activityReportGoalResource.test.js +++ b/src/hooks/activityReportGoalResource.test.js @@ -8,7 +8,7 @@ import { ActivityReportGoalResource, User, Resource, -} from '..'; +} from '../models'; import { recalculateOnAR } from './activityReportGoalResource'; const draftObject = { diff --git a/src/models/hooks/activityReportObjective.js b/src/hooks/activityReportObjective.js similarity index 90% rename from src/models/hooks/activityReportObjective.js rename to src/hooks/activityReportObjective.js index eb7147592c..4d50abf77e 100644 --- a/src/models/hooks/activityReportObjective.js +++ b/src/hooks/activityReportObjective.js @@ -1,10 +1,10 @@ -import { Op } from 'sequelize'; -import { validateChangedOrSetEnums } from '../helpers/enum'; -import { OBJECTIVE_COLLABORATORS, OBJECTIVE_STATUS } from '../../constants'; -import { +const { Op } = require('sequelize'); +const { validateChangedOrSetEnums } = require('../models/helpers/enum'); +const { OBJECTIVE_COLLABORATORS, OBJECTIVE_STATUS } = require('../constants'); +const { currentUserPopulateCollaboratorForType, removeCollaboratorsForType, -} from '../helpers/genericCollaborator'; +} = require('../models/helpers/genericCollaborator'); const propagateDestroyToMetadata = async (sequelize, instance, options) => Promise.all( [ @@ -97,7 +97,7 @@ const afterCreate = async (sequelize, instance, options) => { await propagateSupportTypeToObjective(sequelize, instance, options); }; -const beforeValidate = async (sequelize, instance, options) => { +const beforeValidate = async (sequelize, instance) => { validateChangedOrSetEnums(sequelize, instance); }; @@ -110,7 +110,7 @@ const afterDestroy = async (sequelize, instance, options) => { await recalculateOnAR(sequelize, instance, options); }; -export { +module.exports = { propagateDestroyToMetadata, recalculateOnAR, afterCreate, diff --git a/src/models/hooks/activityReportObjective.test.js b/src/hooks/activityReportObjective.test.js similarity index 66% rename from src/models/hooks/activityReportObjective.test.js rename to src/hooks/activityReportObjective.test.js index 2c1d825213..2ea93799da 100644 --- a/src/models/hooks/activityReportObjective.test.js +++ b/src/hooks/activityReportObjective.test.js @@ -1,3 +1,4 @@ +import { SUPPORT_TYPES } from '@ttahub/common'; import { sequelize, ActivityReportObjective, @@ -9,17 +10,20 @@ import { File, ActivityReport, Topic, -} from '..'; +} from '../models'; import { draftObject } from './testHelpers'; -import { FILE_STATUSES, OBJECTIVE_STATUS } from '../../constants'; +import { FILE_STATUSES, OBJECTIVE_STATUS } from '../constants'; import { beforeDestroy } from './activityReportObjective'; -import { processObjectiveForResourcesById, processActivityReportObjectiveForResourcesById } from '../../services/resource'; +import { processObjectiveForResourcesById, processActivityReportObjectiveForResourcesById } from '../services/resource'; describe('activityReportObjective hooks', () => { let ar; let topic; let objective; + let secondObjective; + let thirdObjective; + let aro; let file; @@ -31,10 +35,32 @@ describe('activityReportObjective hooks', () => { status: OBJECTIVE_STATUS.IN_PROGRESS, }); + secondObjective = await Objective.create({ + title: 'second test objective', + status: OBJECTIVE_STATUS.IN_PROGRESS, + }); + + thirdObjective = await Objective.create({ + title: 'third test objective', + status: OBJECTIVE_STATUS.IN_PROGRESS, + supportType: SUPPORT_TYPES[0], + }); + aro = await ActivityReportObjective.create({ activityReportId: ar.id, objectiveId: objective.id, - }); + supportType: SUPPORT_TYPES[0], + }, { individualHooks: true }); + + await ActivityReportObjective.create({ + activityReportId: ar.id, + objectiveId: secondObjective.id, + }, { individualHooks: true }); + + await ActivityReportObjective.create({ + activityReportId: ar.id, + objectiveId: thirdObjective.id, + }, { individualHooks: true }); topic = await Topic.create({ name: 'Javascript Mastery', @@ -87,11 +113,23 @@ describe('activityReportObjective hooks', () => { }); await ActivityReportObjective.destroy({ - where: { id: aro.id }, + where: { + objectiveId: [ + objective.id, + secondObjective.id, + thirdObjective.id, + ], + }, }); await Objective.destroy({ - where: { id: objective.id }, + where: { + id: [ + objective.id, + secondObjective.id, + thirdObjective.id, + ], + }, force: true, }); @@ -102,6 +140,18 @@ describe('activityReportObjective hooks', () => { await sequelize.close(); }); + describe('supportType', () => { + it('should propogate supportType to objective', async () => { + const objective1 = await Objective.findByPk(objective.id); + const objective2 = await Objective.findByPk(secondObjective.id); + const objective3 = await Objective.findByPk(thirdObjective.id); + + expect(objective1.supportType).toBe(SUPPORT_TYPES[0]); + expect(objective2.supportType).toBe(null); + expect(objective3.supportType).toBe(SUPPORT_TYPES[0]); + }); + }); + describe('beforeDestroy', () => { it('should propagate destroy to metadata', async () => { const transaction = await sequelize.transaction(); diff --git a/src/models/hooks/activityReportObjectiveFile.js b/src/hooks/activityReportObjectiveFile.js similarity index 88% rename from src/models/hooks/activityReportObjectiveFile.js rename to src/hooks/activityReportObjectiveFile.js index d9b2eefd50..ec8c6a78a3 100644 --- a/src/models/hooks/activityReportObjectiveFile.js +++ b/src/hooks/activityReportObjectiveFile.js @@ -1,5 +1,5 @@ -const { getSingularOrPluralData } = require('../helpers/hookMetadata'); -const { cleanupOrphanFiles } = require('../helpers/orphanCleanupHelper'); +const { getSingularOrPluralData } = require('../models/helpers/hookMetadata'); +const { cleanupOrphanFiles } = require('../models/helpers/orphanCleanupHelper'); const recalculateOnAR = async (sequelize, instance, options) => { // check to see if objectiveId or objectiveIds is validly defined @@ -9,8 +9,7 @@ const recalculateOnAR = async (sequelize, instance, options) => { let fileOnReport; // by using the passed in objectives we can use a more performant version of the query if (objectiveIds !== undefined - && Array.isArray(objectiveIds) - && objectiveIds.map((i) => typeof i).every((i) => i === 'number')) { + && Array.isArray(objectiveIds)) { fileOnReport = ` SELECT f."id", diff --git a/src/models/hooks/activityReportObjectiveFile.test.js b/src/hooks/activityReportObjectiveFile.test.js similarity index 95% rename from src/models/hooks/activityReportObjectiveFile.test.js rename to src/hooks/activityReportObjectiveFile.test.js index 5657fd6134..e8a97bd288 100644 --- a/src/models/hooks/activityReportObjectiveFile.test.js +++ b/src/hooks/activityReportObjectiveFile.test.js @@ -9,11 +9,11 @@ import { File, ActivityReport, Topic, -} from '..'; +} from '../models'; import { draftObject } from './testHelpers'; -import { FILE_STATUSES, OBJECTIVE_STATUS } from '../../constants'; -import { processObjectiveForResourcesById, processActivityReportObjectiveForResourcesById } from '../../services/resource'; +import { FILE_STATUSES, OBJECTIVE_STATUS } from '../constants'; +import { processObjectiveForResourcesById, processActivityReportObjectiveForResourcesById } from '../services/resource'; describe('activityReportObjective hooks', () => { let ar; diff --git a/src/models/hooks/activityReportObjectiveResource.js b/src/hooks/activityReportObjectiveResource.js similarity index 91% rename from src/models/hooks/activityReportObjectiveResource.js rename to src/hooks/activityReportObjectiveResource.js index 875a5493ff..c332889b26 100644 --- a/src/models/hooks/activityReportObjectiveResource.js +++ b/src/hooks/activityReportObjectiveResource.js @@ -1,5 +1,5 @@ -const { getSingularOrPluralData } = require('../helpers/hookMetadata'); -const { cleanupOrphanResources } = require('../helpers/orphanCleanupHelper'); +const { getSingularOrPluralData } = require('../models/helpers/hookMetadata'); +const { cleanupOrphanResources } = require('../models/helpers/orphanCleanupHelper'); const propagateOnAR = async (sequelize, instance, options) => sequelize.models.ObjectiveResource .update( @@ -27,8 +27,7 @@ const recalculateOnAR = async (sequelize, instance, options) => { let resourceOnReport; // by using the passed in objectives we can use a more performant version of the query if (objectiveIds !== undefined - && Array.isArray(objectiveIds) - && objectiveIds.map((i) => typeof i).every((i) => i === 'number')) { + && Array.isArray(objectiveIds)) { resourceOnReport = ` SELECT r."id", @@ -81,7 +80,7 @@ const afterDestroy = async (sequelize, instance, options) => { await cleanupOrphanResources(sequelize, instance.resourceId); }; -export { +module.exports = { propagateOnAR, recalculateOnAR, afterCreate, diff --git a/src/models/hooks/activityReportObjectiveResource.test.js b/src/hooks/activityReportObjectiveResource.test.js similarity index 99% rename from src/models/hooks/activityReportObjectiveResource.test.js rename to src/hooks/activityReportObjectiveResource.test.js index 28626346ec..37957e6159 100644 --- a/src/models/hooks/activityReportObjectiveResource.test.js +++ b/src/hooks/activityReportObjectiveResource.test.js @@ -8,7 +8,7 @@ import { ActivityReportObjectiveResource, User, Resource, -} from '..'; +} from '../models'; const draftObject = { activityRecipientType: 'recipient', diff --git a/src/models/hooks/activityReportObjectiveTopic.js b/src/hooks/activityReportObjectiveTopic.js similarity index 91% rename from src/models/hooks/activityReportObjectiveTopic.js rename to src/hooks/activityReportObjectiveTopic.js index 9e37f8c72e..5eed5af358 100644 --- a/src/models/hooks/activityReportObjectiveTopic.js +++ b/src/hooks/activityReportObjectiveTopic.js @@ -1,4 +1,4 @@ -const { getSingularOrPluralData } = require('../helpers/hookMetadata'); +const { getSingularOrPluralData } = require('../models/helpers/hookMetadata'); const recalculateOnAR = async (sequelize, instance, options) => { // check to see if objectiveId or objectiveIds is validly defined @@ -8,8 +8,7 @@ const recalculateOnAR = async (sequelize, instance, options) => { let topicOnReport; // by using the passed in objectives we can use a more performant version of the query if (objectiveIds !== undefined - && Array.isArray(objectiveIds) - && objectiveIds.map((i) => typeof i).every((i) => i === 'number')) { + && Array.isArray(objectiveIds)) { topicOnReport = ` SELECT t."id", diff --git a/src/models/hooks/activityReportResource.js b/src/hooks/activityReportResource.js similarity index 71% rename from src/models/hooks/activityReportResource.js rename to src/hooks/activityReportResource.js index dff2bbc897..13e46e2382 100644 --- a/src/models/hooks/activityReportResource.js +++ b/src/hooks/activityReportResource.js @@ -1,5 +1,5 @@ /* eslint-disable import/prefer-default-export */ -const { cleanupOrphanResources } = require('../helpers/orphanCleanupHelper'); +const { cleanupOrphanResources } = require('../models/helpers/orphanCleanupHelper'); const afterDestroy = async (sequelize, instance, options) => { await cleanupOrphanResources(sequelize, instance.resourceId); diff --git a/src/models/hooks/activityReportResource.test.js b/src/hooks/activityReportResource.test.js similarity index 99% rename from src/models/hooks/activityReportResource.test.js rename to src/hooks/activityReportResource.test.js index 89b538ede9..c714d8d575 100644 --- a/src/models/hooks/activityReportResource.test.js +++ b/src/hooks/activityReportResource.test.js @@ -6,7 +6,7 @@ import { ActivityReportResource, Resource, User, -} from '..'; +} from '../models'; import { draftObject } from './testHelpers'; jest.mock('bull'); diff --git a/src/models/hooks/communicationLogFile.js b/src/hooks/communicationLogFile.js similarity index 100% rename from src/models/hooks/communicationLogFile.js rename to src/hooks/communicationLogFile.js diff --git a/src/models/hooks/eventReportPilot.js b/src/hooks/eventReportPilot.js similarity index 91% rename from src/models/hooks/eventReportPilot.js rename to src/hooks/eventReportPilot.js index 392c1c7af1..4f9584d178 100644 --- a/src/models/hooks/eventReportPilot.js +++ b/src/hooks/eventReportPilot.js @@ -1,28 +1,16 @@ /* eslint-disable max-len */ /* eslint-disable global-require */ + import { createGoalsForSessionRecipientsIfNecessary } from './sessionReportPilot'; /* eslint-disable import/prefer-default-export */ const { Op } = require('sequelize'); const { TRAINING_REPORT_STATUSES } = require('@ttahub/common'); -const { auditLogger } = require('../../logger'); - -const safeParse = (instance) => { - // Try to parse instance.data if it exists and has a 'val' property - if (instance?.data?.val) { - return JSON.parse(instance.data.val); - } - // Directly return instance.dataValues.data if it exists - if (instance?.dataValues?.data) { - return instance.dataValues.data; - } - // Directly return instance.data if it exists - if (instance?.data) { - return instance.data; - } +const { auditLogger } = require('../logger'); +const { escapeDataFields } = require('../models/helpers/escapeFields'); +const safeParse = require('../models/helpers/safeParse'); - return null; -}; +const fieldsToEscape = ['eventName']; const notifyNewCollaborators = async (_sequelize, instance) => { try { @@ -42,7 +30,7 @@ const notifyNewCollaborators = async (_sequelize, instance) => { } // imported inside function to prevent circular ref - const { trCollaboratorAdded } = require('../../lib/mailer'); + const { trCollaboratorAdded } = require('../lib/mailer'); // process notifications for new collaborators await Promise.all( @@ -69,7 +57,7 @@ const notifyNewPoc = async (_sequelize, instance) => { } // imported inside function to prevent circular ref - const { trPocAdded } = require('../../lib/mailer'); + const { trPocAdded } = require('../lib/mailer'); await Promise.all( newPocIds.map((id) => trPocAdded(instance, id)), @@ -91,7 +79,7 @@ const notifyPocEventComplete = async (_sequelize, instance) => { current.status === TRAINING_REPORT_STATUSES.COMPLETE && previous.status !== TRAINING_REPORT_STATUSES.COMPLETE) { // imported inside function to prevent circular ref - const { trPocEventComplete } = require('../../lib/mailer'); + const { trPocEventComplete } = require('../lib/mailer'); await trPocEventComplete(instance.dataValues); } } @@ -110,7 +98,7 @@ const notifyVisionAndGoalComplete = async (_sequelize, instance) => { if ( current.pocComplete && !previous.pocComplete) { // imported inside function to prevent circular ref - const { trVisionAndGoalComplete } = require('../../lib/mailer'); + const { trVisionAndGoalComplete } = require('../lib/mailer'); await trVisionAndGoalComplete(instance.dataValues); } } @@ -281,6 +269,11 @@ const createOrUpdateNationalCenterUserCacheTable = async (sequelize, instance, o const beforeUpdate = async (sequelize, instance, options) => { await updateGoalText(sequelize, instance, options); + escapeDataFields(instance, fieldsToEscape); +}; + +const beforeCreate = async (_sequelize, instance) => { + escapeDataFields(instance, fieldsToEscape); }; const afterUpdate = async (sequelize, instance, options) => { @@ -298,6 +291,7 @@ const afterCreate = async (sequelize, instance, options) => { export { afterUpdate, beforeUpdate, + beforeCreate, afterCreate, createOrUpdateNationalCenterUserCacheTable, }; diff --git a/src/models/hooks/eventReportPilot.test.js b/src/hooks/eventReportPilot.test.js similarity index 98% rename from src/models/hooks/eventReportPilot.test.js rename to src/hooks/eventReportPilot.test.js index ea8f0340dc..0777b53b2a 100644 --- a/src/models/hooks/eventReportPilot.test.js +++ b/src/hooks/eventReportPilot.test.js @@ -6,12 +6,12 @@ import { trPocAdded, trPocEventComplete, trVisionAndGoalComplete, -} from '../../lib/mailer'; -import { auditLogger } from '../../logger'; -import db from '..'; -import { createUser } from '../../testUtils'; +} from '../lib/mailer'; +import { auditLogger } from '../logger'; +import db from '../models'; +import { createUser } from '../testUtils'; -jest.mock('../../lib/mailer', () => ({ +jest.mock('../lib/mailer', () => ({ trCollaboratorAdded: jest.fn(), trPocAdded: jest.fn(), trPocEventComplete: jest.fn(), diff --git a/src/models/hooks/file.js b/src/hooks/file.js similarity index 76% rename from src/models/hooks/file.js rename to src/hooks/file.js index 189da73649..70f368ecec 100644 --- a/src/models/hooks/file.js +++ b/src/hooks/file.js @@ -1,5 +1,5 @@ /* eslint-disable import/prefer-default-export */ -const { addDeleteFileToQueue } = require('../../services/s3Queue'); +const { addDeleteFileToQueue } = require('../services/s3Queue'); const afterDestroy = async (_sequelize, instance) => { // Add delete job S3 queue. diff --git a/src/models/hooks/file.test.js b/src/hooks/file.test.js similarity index 81% rename from src/models/hooks/file.test.js rename to src/hooks/file.test.js index 648cce54a9..2880e23a03 100644 --- a/src/models/hooks/file.test.js +++ b/src/hooks/file.test.js @@ -1,13 +1,13 @@ import { sequelize, File, -} from '..'; -import { addDeleteFileToQueue } from '../../services/s3Queue'; -import { FILE_STATUSES } from '../../constants'; +} from '../models'; +import { addDeleteFileToQueue } from '../services/s3Queue'; +import { FILE_STATUSES } from '../constants'; jest.mock('bull'); -jest.mock('../../services/s3Queue', () => ({ +jest.mock('../services/s3Queue', () => ({ addDeleteFileToQueue: jest.fn(), })); diff --git a/src/models/hooks/genericFile.js b/src/hooks/genericFile.js similarity index 100% rename from src/models/hooks/genericFile.js rename to src/hooks/genericFile.js diff --git a/src/models/hooks/genericFile.test.js b/src/hooks/genericFile.test.js similarity index 98% rename from src/models/hooks/genericFile.test.js rename to src/hooks/genericFile.test.js index 2b671e7e21..173a61a2a3 100644 --- a/src/models/hooks/genericFile.test.js +++ b/src/hooks/genericFile.test.js @@ -9,8 +9,8 @@ import { File, ActivityReport, ActivityReportFile, -} from '..'; -import { FILE_STATUSES, OBJECTIVE_STATUS } from '../../constants'; +} from '../models'; +import { FILE_STATUSES, OBJECTIVE_STATUS } from '../constants'; import { propagateDestroyToFile } from './genericFile'; import { draftObject, objectiveTemplateGenerator } from './testHelpers'; diff --git a/src/models/hooks/genericLink.js b/src/hooks/genericLink.js similarity index 99% rename from src/models/hooks/genericLink.js rename to src/hooks/genericLink.js index 4d1d371ddd..5a906aa828 100644 --- a/src/models/hooks/genericLink.js +++ b/src/hooks/genericLink.js @@ -1,4 +1,4 @@ -import Semaphore from '../../lib/semaphore'; +import Semaphore from '../lib/semaphore'; const semaphore = new Semaphore(1); diff --git a/src/models/hooks/genericLink.test.js b/src/hooks/genericLink.test.js similarity index 99% rename from src/models/hooks/genericLink.test.js rename to src/hooks/genericLink.test.js index eb14777f93..976be7d2f0 100644 --- a/src/models/hooks/genericLink.test.js +++ b/src/hooks/genericLink.test.js @@ -1,4 +1,4 @@ -import Semaphore from '../../lib/semaphore'; +import Semaphore from '../lib/semaphore'; import { syncLink, diff --git a/src/models/hooks/goal.js b/src/hooks/goal.js similarity index 97% rename from src/models/hooks/goal.js rename to src/hooks/goal.js index f8926f3623..0df77840e9 100644 --- a/src/models/hooks/goal.js +++ b/src/hooks/goal.js @@ -1,14 +1,14 @@ const { Op } = require('sequelize'); -const { GOAL_STATUS, GOAL_COLLABORATORS } = require('../../constants'); +const { GOAL_STATUS, GOAL_COLLABORATORS } = require('../constants'); const { currentUserPopulateCollaboratorForType, -} = require('../helpers/genericCollaborator'); -const { skipIf } = require('../helpers/flowControl'); -const { onlyAllowTrGoalSourceForGoalsCreatedViaTr } = require('../helpers/goalSource'); +} = require('../models/helpers/genericCollaborator'); +const { skipIf } = require('../models/helpers/flowControl'); +const { onlyAllowTrGoalSourceForGoalsCreatedViaTr } = require('../models/helpers/goalSource'); const processForEmbeddedResources = async (_sequelize, instance) => { // eslint-disable-next-line global-require - const { calculateIsAutoDetectedForGoal, processGoalForResourcesById } = require('../../services/resource'); + const { calculateIsAutoDetectedForGoal, processGoalForResourcesById } = require('../services/resource'); const changed = instance.changed() || Object.keys(instance); if (calculateIsAutoDetectedForGoal(changed)) { await processGoalForResourcesById(instance.id); diff --git a/src/models/hooks/goal.test.js b/src/hooks/goal.test.js similarity index 96% rename from src/models/hooks/goal.test.js rename to src/hooks/goal.test.js index 0b6c838093..0b7a7ebf50 100644 --- a/src/models/hooks/goal.test.js +++ b/src/hooks/goal.test.js @@ -3,10 +3,9 @@ const { autoPopulateStatusChangeDates, processForEmbeddedResources, findOrCreateGoalTemplate, - onlyAllowTrGoalSourceForGoalsCreatedViaTr, } = require('./goal'); -const { GOAL_STATUS } = require('../../constants'); -const { createRecipient, createGrant, createGoal } = require('../../testUtils'); +const { GOAL_STATUS } = require('../constants'); +const { createRecipient, createGrant, createGoal } = require('../testUtils'); const { sequelize: db, Grant: GrantModel, @@ -14,9 +13,9 @@ const { Goal: GoalModel, GoalSimilarityGroup: GoalSimilarityGroupModel, GoalSimilarityGroupGoal: GoalSimilarityGroupGoalModel, -} = require('..'); +} = require('../models'); -jest.mock('../../services/resource'); +jest.mock('../services/resource'); describe('goal hooks', () => { describe('GoalSimilarityGroup hooks', () => { @@ -224,14 +223,14 @@ describe('goal hooks', () => { afterEach(() => jest.clearAllMocks()); it('should call processGoalForResourcesById if auto detection is true', async () => { - const { calculateIsAutoDetectedForGoal, processGoalForResourcesById } = require('../../services/resource'); + const { calculateIsAutoDetectedForGoal, processGoalForResourcesById } = require('../services/resource'); calculateIsAutoDetectedForGoal.mockReturnValueOnce(true); await processForEmbeddedResources(sequelize, instance); expect(processGoalForResourcesById).toHaveBeenCalledWith(instance.id); }); it('should not call processGoalForResourcesById if auto detection is false', async () => { - const { calculateIsAutoDetectedForGoal, processGoalForResourcesById } = require('../../services/resource'); + const { calculateIsAutoDetectedForGoal, processGoalForResourcesById } = require('../services/resource'); calculateIsAutoDetectedForGoal.mockReturnValueOnce(false); await processForEmbeddedResources(sequelize, instance); expect(processGoalForResourcesById).not.toHaveBeenCalled(); diff --git a/src/models/hooks/goalFieldResponse.js b/src/hooks/goalFieldResponse.js similarity index 100% rename from src/models/hooks/goalFieldResponse.js rename to src/hooks/goalFieldResponse.js diff --git a/src/models/hooks/goalFieldResponse.test.js b/src/hooks/goalFieldResponse.test.js similarity index 99% rename from src/models/hooks/goalFieldResponse.test.js rename to src/hooks/goalFieldResponse.test.js index 2772537f04..a2127c4abf 100644 --- a/src/models/hooks/goalFieldResponse.test.js +++ b/src/hooks/goalFieldResponse.test.js @@ -11,7 +11,7 @@ import { GoalTemplateFieldPrompt, GoalFieldResponse, ActivityReportGoalFieldResponse, -} from '..'; +} from '../models'; const mockReport = { activityRecipientType: 'recipient', diff --git a/src/models/hooks/goalResource.js b/src/hooks/goalResource.js similarity index 71% rename from src/models/hooks/goalResource.js rename to src/hooks/goalResource.js index dff2bbc897..13e46e2382 100644 --- a/src/models/hooks/goalResource.js +++ b/src/hooks/goalResource.js @@ -1,5 +1,5 @@ /* eslint-disable import/prefer-default-export */ -const { cleanupOrphanResources } = require('../helpers/orphanCleanupHelper'); +const { cleanupOrphanResources } = require('../models/helpers/orphanCleanupHelper'); const afterDestroy = async (sequelize, instance, options) => { await cleanupOrphanResources(sequelize, instance.resourceId); diff --git a/src/models/hooks/goalResource.test.js b/src/hooks/goalResource.test.js similarity index 99% rename from src/models/hooks/goalResource.test.js rename to src/hooks/goalResource.test.js index 666479231d..eca9e68a9b 100644 --- a/src/models/hooks/goalResource.test.js +++ b/src/hooks/goalResource.test.js @@ -3,7 +3,7 @@ import { Resource, Goal, GoalResource, -} from '..'; +} from '../models'; jest.mock('bull'); diff --git a/src/models/hooks/goalTemplate.js b/src/hooks/goalTemplate.js similarity index 96% rename from src/models/hooks/goalTemplate.js rename to src/hooks/goalTemplate.js index 4434daf09a..67a5596f09 100644 --- a/src/models/hooks/goalTemplate.js +++ b/src/hooks/goalTemplate.js @@ -1,9 +1,9 @@ const { Op } = require('sequelize'); -const { AUTOMATIC_CREATION } = require('../../constants'); +const { AUTOMATIC_CREATION } = require('../constants'); const processForEmbeddedResources = async (sequelize, instance, options) => { // eslint-disable-next-line global-require - const { calculateIsAutoDetectedForGoalTemplate, processGoalTemplateForResourcesById } = require('../../services/resource'); + const { calculateIsAutoDetectedForGoalTemplate, processGoalTemplateForResourcesById } = require('../services/resource'); const changed = instance.changed() || Object.keys(instance); if (calculateIsAutoDetectedForGoalTemplate(changed)) { await processGoalTemplateForResourcesById(instance.id); diff --git a/src/models/hooks/goalTemplate.test.js b/src/hooks/goalTemplate.test.js similarity index 98% rename from src/models/hooks/goalTemplate.test.js rename to src/hooks/goalTemplate.test.js index 578fee0de4..d41710b481 100644 --- a/src/models/hooks/goalTemplate.test.js +++ b/src/hooks/goalTemplate.test.js @@ -1,7 +1,7 @@ import { sequelize, -} from '..'; -import { AUTOMATIC_CREATION, CURATED_CREATION } from '../../constants'; +} from '../models'; +import { AUTOMATIC_CREATION, CURATED_CREATION } from '../constants'; import { beforeValidate, beforeUpdate } from './goalTemplate'; describe('GoalTemplate hooks', () => { diff --git a/src/models/hooks/goalTemplateResource.js b/src/hooks/goalTemplateResource.js similarity index 71% rename from src/models/hooks/goalTemplateResource.js rename to src/hooks/goalTemplateResource.js index dff2bbc897..13e46e2382 100644 --- a/src/models/hooks/goalTemplateResource.js +++ b/src/hooks/goalTemplateResource.js @@ -1,5 +1,5 @@ /* eslint-disable import/prefer-default-export */ -const { cleanupOrphanResources } = require('../helpers/orphanCleanupHelper'); +const { cleanupOrphanResources } = require('../models/helpers/orphanCleanupHelper'); const afterDestroy = async (sequelize, instance, options) => { await cleanupOrphanResources(sequelize, instance.resourceId); diff --git a/src/models/hooks/goalTemplateResource.test.js b/src/hooks/goalTemplateResource.test.js similarity index 99% rename from src/models/hooks/goalTemplateResource.test.js rename to src/hooks/goalTemplateResource.test.js index 0e320f0d39..f30c04e395 100644 --- a/src/models/hooks/goalTemplateResource.test.js +++ b/src/hooks/goalTemplateResource.test.js @@ -3,7 +3,7 @@ import { Resource, ObjectiveTemplate, ObjectiveTemplateResource, -} from '..'; +} from '../models'; jest.mock('bull'); diff --git a/src/models/hooks/grant.js b/src/hooks/grant.js similarity index 100% rename from src/models/hooks/grant.js rename to src/hooks/grant.js diff --git a/src/models/hooks/group.js b/src/hooks/group.js similarity index 82% rename from src/models/hooks/group.js rename to src/hooks/group.js index f095eb5fd9..a08b556260 100644 --- a/src/models/hooks/group.js +++ b/src/hooks/group.js @@ -1,8 +1,8 @@ -const { GROUP_COLLABORATORS } = require('../../constants'); +const { GROUP_COLLABORATORS } = require('../constants'); const { currentUserPopulateCollaboratorForType, -} = require('../helpers/genericCollaborator'); -const { skipIf } = require('../helpers/flowControl'); +} = require('../models/helpers/genericCollaborator'); +const { skipIf } = require('../models/helpers/flowControl'); const autoPopulateCreator = async (sequelize, instance, options) => { if (skipIf(options, 'autoPopulateCreator')) return Promise.resolve(); @@ -37,6 +37,8 @@ const afterUpdate = async (sequelize, instance, options) => { }; export { + autoPopulateCreator, + autoPopulateEditor, afterCreate, afterUpdate, }; diff --git a/src/hooks/group.test.js b/src/hooks/group.test.js new file mode 100644 index 0000000000..fa92a9d976 --- /dev/null +++ b/src/hooks/group.test.js @@ -0,0 +1,67 @@ +const { autoPopulateCreator, autoPopulateEditor } = require('./group'); +const { GROUP_COLLABORATORS } = require('../constants'); +const { currentUserPopulateCollaboratorForType } = require('../models/helpers/genericCollaborator'); + +jest.mock('../models/helpers/genericCollaborator', () => ({ + currentUserPopulateCollaboratorForType: jest.fn(), +})); + +describe('group hooks', () => { + describe('autoPopulateCreator', () => { + afterAll(() => { + jest.clearAllMocks(); + jest.resetModules(); + }); + + it('should skip auto-population if "autoPopulateCreator" option is set', async () => { + const sequelize = {}; // mock sequelize object + const instance = {}; // mock instance object + const options = { autoPopulateCreator: true }; // set "autoPopulateCreator" option to true + + const result = await autoPopulateCreator(sequelize, instance, options); + + expect(result).toBeUndefined(); + }); + + it('should populate the creator collaborator for the group', async () => { + const sequelize = {}; // mock sequelize object + const instance = { id: 1 }; // mock instance object with an id + const options = {}; // empty options object + + const result = await autoPopulateCreator(sequelize, instance, options); + + expect(currentUserPopulateCollaboratorForType).toHaveBeenCalledWith( + 'group', + sequelize, + undefined, // mock transaction + instance.id, + GROUP_COLLABORATORS.CREATOR, + ); + expect(result).toBeUndefined(); + }); + }); + + describe('autoPpopulateEditor', () => { + afterAll(() => { + jest.clearAllMocks(); + jest.resetModules(); + }); + + it('should populate the editor collaborator for the group', async () => { + const sequelize = {}; // mock sequelize object + const instance = { id: 1 }; // mock instance object with an id + const options = {}; // empty options object + + const result = await autoPopulateEditor(sequelize, instance, options); + + expect(currentUserPopulateCollaboratorForType).toHaveBeenCalledWith( + 'group', + sequelize, + undefined, // mock transaction + instance.id, + GROUP_COLLABORATORS.EDITOR, + ); + expect(result).toBeUndefined(); + }); + }); +}); diff --git a/src/models/hooks/importFile.js b/src/hooks/importFile.js similarity index 100% rename from src/models/hooks/importFile.js rename to src/hooks/importFile.js diff --git a/src/models/hooks/monitoringClassSummary.js b/src/hooks/monitoringClassSummary.js similarity index 100% rename from src/models/hooks/monitoringClassSummary.js rename to src/hooks/monitoringClassSummary.js diff --git a/src/models/hooks/monitoringFindingHistory.js b/src/hooks/monitoringFindingHistory.js similarity index 100% rename from src/models/hooks/monitoringFindingHistory.js rename to src/hooks/monitoringFindingHistory.js diff --git a/src/models/hooks/monitoringReview.js b/src/hooks/monitoringReview.js similarity index 100% rename from src/models/hooks/monitoringReview.js rename to src/hooks/monitoringReview.js diff --git a/src/models/hooks/monitoringReviewGrantee.js b/src/hooks/monitoringReviewGrantee.js similarity index 100% rename from src/models/hooks/monitoringReviewGrantee.js rename to src/hooks/monitoringReviewGrantee.js diff --git a/src/models/hooks/monitoringReviewStatus.js b/src/hooks/monitoringReviewStatus.js similarity index 100% rename from src/models/hooks/monitoringReviewStatus.js rename to src/hooks/monitoringReviewStatus.js diff --git a/src/models/hooks/nationalCenter.js b/src/hooks/nationalCenter.js similarity index 100% rename from src/models/hooks/nationalCenter.js rename to src/hooks/nationalCenter.js diff --git a/src/models/hooks/nationalCenter.test.js b/src/hooks/nationalCenter.test.js similarity index 97% rename from src/models/hooks/nationalCenter.test.js rename to src/hooks/nationalCenter.test.js index 8aa865ab2f..923e194ed3 100644 --- a/src/models/hooks/nationalCenter.test.js +++ b/src/hooks/nationalCenter.test.js @@ -1,7 +1,7 @@ import { EVENT_REPORT_STATUSES } from '@ttahub/common'; import faker from '@faker-js/faker'; -import db from '..'; -import { updateById } from '../../services/nationalCenters'; +import db from '../models'; +import { updateById } from '../services/nationalCenters'; jest.mock('./sessionReportPilot'); diff --git a/src/models/hooks/nextStep.js b/src/hooks/nextStep.js similarity index 92% rename from src/models/hooks/nextStep.js rename to src/hooks/nextStep.js index f7a770f732..f1b0f88357 100644 --- a/src/models/hooks/nextStep.js +++ b/src/hooks/nextStep.js @@ -1,6 +1,6 @@ const processForEmbeddedResources = async (sequelize, instance, options) => { // eslint-disable-next-line global-require - const { calculateIsAutoDetectedForNextStep, processNextStepForResourcesById } = require('../../services/resource'); + const { calculateIsAutoDetectedForNextStep, processNextStepForResourcesById } = require('../services/resource'); const changed = instance.changed() || Object.keys(instance); if (calculateIsAutoDetectedForNextStep(changed)) { await processNextStepForResourcesById(instance.id); diff --git a/src/models/hooks/nextStep.test.js b/src/hooks/nextStep.test.js similarity index 95% rename from src/models/hooks/nextStep.test.js rename to src/hooks/nextStep.test.js index 4fc9b95bf2..3e64b1c760 100644 --- a/src/models/hooks/nextStep.test.js +++ b/src/hooks/nextStep.test.js @@ -2,9 +2,9 @@ import { afterCreate, afterUpdate, } from './nextStep'; -import { processNextStepForResourcesById, calculateIsAutoDetectedForNextStep } from '../../services/resource'; +import { processNextStepForResourcesById, calculateIsAutoDetectedForNextStep } from '../services/resource'; -jest.mock('../../services/resource', () => ({ +jest.mock('../services/resource', () => ({ calculateIsAutoDetectedForNextStep: jest.fn(), processNextStepForResourcesById: jest.fn(), })); diff --git a/src/models/hooks/nextStepResource.js b/src/hooks/nextStepResource.js similarity index 71% rename from src/models/hooks/nextStepResource.js rename to src/hooks/nextStepResource.js index dff2bbc897..13e46e2382 100644 --- a/src/models/hooks/nextStepResource.js +++ b/src/hooks/nextStepResource.js @@ -1,5 +1,5 @@ /* eslint-disable import/prefer-default-export */ -const { cleanupOrphanResources } = require('../helpers/orphanCleanupHelper'); +const { cleanupOrphanResources } = require('../models/helpers/orphanCleanupHelper'); const afterDestroy = async (sequelize, instance, options) => { await cleanupOrphanResources(sequelize, instance.resourceId); diff --git a/src/models/hooks/nextStepResource.test.js b/src/hooks/nextStepResource.test.js similarity index 99% rename from src/models/hooks/nextStepResource.test.js rename to src/hooks/nextStepResource.test.js index ccbee08c6d..f771af83c2 100644 --- a/src/models/hooks/nextStepResource.test.js +++ b/src/hooks/nextStepResource.test.js @@ -7,7 +7,7 @@ import { NextStep, Resource, User, -} from '..'; +} from '../models'; import { draftObject } from './testHelpers'; jest.mock('bull'); diff --git a/src/models/hooks/objective.js b/src/hooks/objective.js similarity index 98% rename from src/models/hooks/objective.js rename to src/hooks/objective.js index 2bc4294f31..2d20889862 100644 --- a/src/models/hooks/objective.js +++ b/src/hooks/objective.js @@ -1,11 +1,11 @@ import { Op } from 'sequelize'; import { REPORT_STATUSES } from '@ttahub/common'; -import { OBJECTIVE_STATUS, OBJECTIVE_COLLABORATORS } from '../../constants'; -import { validateChangedOrSetEnums } from '../helpers/enum'; -import { skipIf } from '../helpers/flowControl'; +import { OBJECTIVE_STATUS, OBJECTIVE_COLLABORATORS } from '../constants'; +import { validateChangedOrSetEnums } from '../models/helpers/enum'; +import { skipIf } from '../models/helpers/flowControl'; import { currentUserPopulateCollaboratorForType, -} from '../helpers/genericCollaborator'; +} from '../models/helpers/genericCollaborator'; const findOrCreateObjectiveTemplate = async ( sequelize, diff --git a/src/models/hooks/objective.test.js b/src/hooks/objective.test.js similarity index 97% rename from src/models/hooks/objective.test.js rename to src/hooks/objective.test.js index 8363fa093c..c6e3287260 100644 --- a/src/models/hooks/objective.test.js +++ b/src/hooks/objective.test.js @@ -4,8 +4,8 @@ import db, { Objective, Recipient, Grant, -} from '..'; -import { OBJECTIVE_STATUS } from '../../constants'; +} from '../models'; +import { OBJECTIVE_STATUS } from '../constants'; jest.mock('bull'); diff --git a/src/models/hooks/objectiveFile.js b/src/hooks/objectiveFile.js similarity index 96% rename from src/models/hooks/objectiveFile.js rename to src/hooks/objectiveFile.js index cc9252e438..6de058edc3 100644 --- a/src/models/hooks/objectiveFile.js +++ b/src/hooks/objectiveFile.js @@ -1,9 +1,9 @@ import { Op } from 'sequelize'; -import { AUTOMATIC_CREATION } from '../../constants'; +import { AUTOMATIC_CREATION } from '../constants'; import { propagateDestroyToFile } from './genericFile'; -import { skipIf } from '../helpers/flowControl'; +import { skipIf } from '../models/helpers/flowControl'; -const { cleanupOrphanFiles } = require('../helpers/orphanCleanupHelper'); +const { cleanupOrphanFiles } = require('../models/helpers/orphanCleanupHelper'); const autoPopulateOnAR = (sequelize, instance, options) => { if (skipIf(options, 'autoPopulateOnAR')) return; diff --git a/src/models/hooks/objectiveFile.test.js b/src/hooks/objectiveFile.test.js similarity index 98% rename from src/models/hooks/objectiveFile.test.js rename to src/hooks/objectiveFile.test.js index ccd8d470a1..58ea7d2339 100644 --- a/src/models/hooks/objectiveFile.test.js +++ b/src/hooks/objectiveFile.test.js @@ -1,12 +1,12 @@ import { sequelize, -} from '..'; +} from '../models'; import { beforeValidate, checkForUseOnApprovedReport, } from './objectiveFile'; import { fileGenerator, objectiveTemplateGenerator } from './testHelpers'; -import { CURATED_CREATION } from '../../constants'; +import { CURATED_CREATION } from '../constants'; describe('objectiveFile hooks', () => { afterAll(async () => { diff --git a/src/models/hooks/objectiveResource.js b/src/hooks/objectiveResource.js similarity index 97% rename from src/models/hooks/objectiveResource.js rename to src/hooks/objectiveResource.js index c94657db2c..6dd9faf5b4 100644 --- a/src/models/hooks/objectiveResource.js +++ b/src/hooks/objectiveResource.js @@ -1,7 +1,7 @@ import { Op } from 'sequelize'; -import { AUTOMATIC_CREATION } from '../../constants'; +import { AUTOMATIC_CREATION } from '../constants'; -const { cleanupOrphanResources } = require('../helpers/orphanCleanupHelper'); +const { cleanupOrphanResources } = require('../models/helpers/orphanCleanupHelper'); const autoPopulateOnAR = (sequelize, instance, options) => { // eslint-disable-next-line no-prototype-builtins diff --git a/src/models/hooks/objectiveResource.test.js b/src/hooks/objectiveResource.test.js similarity index 96% rename from src/models/hooks/objectiveResource.test.js rename to src/hooks/objectiveResource.test.js index e489810fcb..87fc97bd11 100644 --- a/src/models/hooks/objectiveResource.test.js +++ b/src/hooks/objectiveResource.test.js @@ -6,11 +6,11 @@ import { ObjectiveResource, ObjectiveTemplateResource, Resource, -} from '..'; -import { OBJECTIVE_STATUS } from '../../constants'; +} from '../models'; +import { OBJECTIVE_STATUS } from '../constants'; import { objectiveTemplateGenerator } from './testHelpers'; import { beforeValidate, afterDestroy } from './objectiveResource'; -import { processObjectiveForResourcesById } from '../../services/resource'; +import { processObjectiveForResourcesById } from '../services/resource'; describe('objectiveResource hooks', () => { const url = faker.internet.url(); diff --git a/src/models/hooks/objectiveTemplate.js b/src/hooks/objectiveTemplate.js similarity index 98% rename from src/models/hooks/objectiveTemplate.js rename to src/hooks/objectiveTemplate.js index 13480bd190..176b3d712f 100644 --- a/src/models/hooks/objectiveTemplate.js +++ b/src/hooks/objectiveTemplate.js @@ -1,5 +1,5 @@ import { Op } from 'sequelize'; -import { AUTOMATIC_CREATION } from '../../constants'; +import { AUTOMATIC_CREATION } from '../constants'; const autoPopulateHash = (sequelize, instance, options) => { const changed = instance.changed(); diff --git a/src/models/hooks/objectiveTemplateFile.js b/src/hooks/objectiveTemplateFile.js similarity index 96% rename from src/models/hooks/objectiveTemplateFile.js rename to src/hooks/objectiveTemplateFile.js index 0ceb772000..bdec891cdb 100644 --- a/src/models/hooks/objectiveTemplateFile.js +++ b/src/hooks/objectiveTemplateFile.js @@ -1,7 +1,7 @@ -import { AUTOMATIC_CREATION } from '../../constants'; +import { AUTOMATIC_CREATION } from '../constants'; import { propagateDestroyToFile } from './genericFile'; -const { cleanupOrphanFiles } = require('../helpers/orphanCleanupHelper'); +const { cleanupOrphanFiles } = require('../models/helpers/orphanCleanupHelper'); // When a new file is added to an objective, add the file to the template or update the // updatedAt value. @@ -91,6 +91,7 @@ const propagateDestroyToTemplate = async (sequelize, instance, options) => { ], transaction: options.transaction, }); + if (otfs.objectiveTemplate.objectives.length > 0) { await sequelize.models.ObjectiveTemplateFile.update( { diff --git a/src/hooks/objectiveTemplateFile.propagateDestroyToTemplate.test.js b/src/hooks/objectiveTemplateFile.propagateDestroyToTemplate.test.js new file mode 100644 index 0000000000..52542967af --- /dev/null +++ b/src/hooks/objectiveTemplateFile.propagateDestroyToTemplate.test.js @@ -0,0 +1,212 @@ +const { AUTOMATIC_CREATION, CURATED_CREATION } = require('../constants'); +const { propagateDestroyToTemplate } = require('./objectiveTemplateFile'); + +describe('propagateDestroyToTemplate', () => { + it('should do nothing if not automatic creation', async () => { + const sequelize = {}; // Mock sequelize object + const instance = { + objectiveId: 1, + fileId: 1, + }; + const options = { + transaction: {}, + }; + + // Mock Objective.findOne method + sequelize.models = { + Objective: { + findOne: jest.fn().mockResolvedValue({ + objectiveTemplate: { + creationMethod: CURATED_CREATION, + }, + }), + }, + ObjectiveTemplateFile: { + findOne: jest.fn(), + update: jest.fn(), + destroy: jest.fn(), + }, + }; + + await propagateDestroyToTemplate(sequelize, instance, options); + + expect(sequelize.models.Objective.findOne).toHaveBeenCalledWith({ + where: { id: instance.objectiveId }, + include: [ + { + model: sequelize.models.ObjectiveTemplate, + as: 'objectiveTemplate', + required: true, + attributes: ['id', 'creationMethod'], + }, + ], + transaction: options.transaction, + }); + + expect(sequelize.models.ObjectiveTemplateFile.findOne).not.toHaveBeenCalled(); + expect(sequelize.models.ObjectiveTemplateFile.update).not.toHaveBeenCalled(); + expect(sequelize.models.ObjectiveTemplateFile.destroy).not.toHaveBeenCalled(); + }); + + it('should update the updatedAt field of ObjectiveTemplateFile if there are objectives on approved reports', async () => { + const sequelize = {}; // Mock sequelize object + const instance = { + objectiveId: 1, + fileId: 1, + }; + const options = { + transaction: {}, + }; + + // Mock Objective.findOne method + sequelize.models = { + Objective: { + findOne: jest.fn().mockResolvedValue({ + objectiveTemplate: { + creationMethod: AUTOMATIC_CREATION, + }, + }), + }, + ObjectiveTemplateFile: { + findOne: jest.fn().mockResolvedValue({ + id: 1, + objectiveTemplate: { + objectives: [{ id: 1 }], + creationMethod: AUTOMATIC_CREATION, + }, + }), + update: jest.fn(), + }, + }; + + await propagateDestroyToTemplate(sequelize, instance, options); + + expect(sequelize.models.Objective.findOne).toHaveBeenCalledWith({ + where: { id: instance.objectiveId }, + include: [ + { + model: sequelize.models.ObjectiveTemplate, + as: 'objectiveTemplate', + required: true, + attributes: ['id', 'creationMethod'], + }, + ], + transaction: options.transaction, + }); + + expect(sequelize.models.ObjectiveTemplateFile.findOne).toHaveBeenCalledWith({ + attributes: ['id'], + where: { + objectiveTemplateId: instance.objectiveTemplateId, + fileId: instance.fileId, + }, + include: [ + { + model: sequelize.models.ObjectiveTemplate, + as: 'objectiveTemplate', + required: true, + include: [ + { + model: sequelize.models.Objective, + as: 'objectives', + required: true, + attributes: ['id'], + where: { onApprovedAR: true }, + }, + ], + }, + ], + transaction: options.transaction, + }); + + expect(sequelize.models.ObjectiveTemplateFile.update).toHaveBeenCalledWith( + { + updatedAt: expect.any(Date), + }, + { + where: { id: expect.any(Number) }, + transaction: options.transaction, + individualHooks: true, + }, + ); + }); + + it('should destroy ObjectiveTemplateFile if there are no objectives on approved reports', async () => { + const sequelize = {}; // Mock sequelize object + const instance = { + objectiveId: 1, + fileId: 1, + }; + const options = { + transaction: {}, + }; + + // Mock Objective.findOne method + sequelize.models = { + Objective: { + findOne: jest.fn().mockResolvedValue({ + objectiveTemplate: { + creationMethod: AUTOMATIC_CREATION, + }, + }), + }, + ObjectiveTemplateFile: { + findOne: jest.fn().mockResolvedValue({ + id: 1, + objectiveTemplate: { + objectives: [], + }, + }), + destroy: jest.fn(), + }, + }; + + await propagateDestroyToTemplate(sequelize, instance, options); + + expect(sequelize.models.Objective.findOne).toHaveBeenCalledWith({ + where: { id: instance.objectiveId }, + include: [ + { + model: sequelize.models.ObjectiveTemplate, + as: 'objectiveTemplate', + required: true, + attributes: ['id', 'creationMethod'], + }, + ], + transaction: options.transaction, + }); + + expect(sequelize.models.ObjectiveTemplateFile.findOne).toHaveBeenCalledWith({ + attributes: ['id'], + where: { + objectiveTemplateId: instance.objectiveTemplateId, + fileId: instance.fileId, + }, + include: [ + { + model: sequelize.models.ObjectiveTemplate, + as: 'objectiveTemplate', + required: true, + include: [ + { + model: sequelize.models.Objective, + as: 'objectives', + required: true, + attributes: ['id'], + where: { onApprovedAR: true }, + }, + ], + }, + ], + transaction: options.transaction, + }); + + expect(sequelize.models.ObjectiveTemplateFile.destroy).toHaveBeenCalledWith( + { + where: { id: expect.any(Number) }, + individualHooks: true, + transaction: options.transaction, + }, + ); + }); +}); diff --git a/src/models/hooks/objectiveTemplateFile.test.js b/src/hooks/objectiveTemplateFile.test.js similarity index 99% rename from src/models/hooks/objectiveTemplateFile.test.js rename to src/hooks/objectiveTemplateFile.test.js index 697ca19367..9690fd6aae 100644 --- a/src/models/hooks/objectiveTemplateFile.test.js +++ b/src/hooks/objectiveTemplateFile.test.js @@ -3,9 +3,9 @@ import { Objective, ObjectiveTemplate, File, -} from '..'; +} from '../models'; -import { OBJECTIVE_STATUS, CURATED_CREATION, FILE_STATUSES } from '../../constants'; +import { OBJECTIVE_STATUS, CURATED_CREATION, FILE_STATUSES } from '../constants'; import { afterCreate, checkForUseOnApprovedReport, diff --git a/src/models/hooks/objectiveTemplateResource.js b/src/hooks/objectiveTemplateResource.js similarity index 71% rename from src/models/hooks/objectiveTemplateResource.js rename to src/hooks/objectiveTemplateResource.js index dff2bbc897..13e46e2382 100644 --- a/src/models/hooks/objectiveTemplateResource.js +++ b/src/hooks/objectiveTemplateResource.js @@ -1,5 +1,5 @@ /* eslint-disable import/prefer-default-export */ -const { cleanupOrphanResources } = require('../helpers/orphanCleanupHelper'); +const { cleanupOrphanResources } = require('../models/helpers/orphanCleanupHelper'); const afterDestroy = async (sequelize, instance, options) => { await cleanupOrphanResources(sequelize, instance.resourceId); diff --git a/src/models/hooks/objectiveTemplateResource.test.js b/src/hooks/objectiveTemplateResource.test.js similarity index 96% rename from src/models/hooks/objectiveTemplateResource.test.js rename to src/hooks/objectiveTemplateResource.test.js index b6cd921f69..2888a2f111 100644 --- a/src/models/hooks/objectiveTemplateResource.test.js +++ b/src/hooks/objectiveTemplateResource.test.js @@ -1,9 +1,9 @@ import { faker } from '@faker-js/faker'; import { sequelize, -} from '..'; +} from '../models'; -import { CURATED_CREATION } from '../../constants'; +import { CURATED_CREATION } from '../constants'; describe('objectiveTemplateResource hooks', () => { describe('afterDestroy', () => { diff --git a/src/models/hooks/objectiveTopic.js b/src/hooks/objectiveTopic.js similarity index 97% rename from src/models/hooks/objectiveTopic.js rename to src/hooks/objectiveTopic.js index 03835ba8a3..6b251650e4 100644 --- a/src/models/hooks/objectiveTopic.js +++ b/src/hooks/objectiveTopic.js @@ -1,6 +1,6 @@ import { Op } from 'sequelize'; -import { AUTOMATIC_CREATION } from '../../constants'; -import { skipIf } from '../helpers/flowControl'; +import { AUTOMATIC_CREATION } from '../constants'; +import { skipIf } from '../models/helpers/flowControl'; const autoPopulateOnAR = (sequelize, instance, options) => { if (skipIf(options, 'autoPopulateOnAR')) return; diff --git a/src/models/hooks/objectiveTopic.test.js b/src/hooks/objectiveTopic.test.js similarity index 97% rename from src/models/hooks/objectiveTopic.test.js rename to src/hooks/objectiveTopic.test.js index c8a19b02bc..d9b821883c 100644 --- a/src/models/hooks/objectiveTopic.test.js +++ b/src/hooks/objectiveTopic.test.js @@ -6,8 +6,8 @@ import { ObjectiveTopic, ObjectiveTemplateTopic, Topic, -} from '..'; -import { OBJECTIVE_STATUS } from '../../constants'; +} from '../models'; +import { OBJECTIVE_STATUS } from '../constants'; import { objectiveTemplateGenerator } from './testHelpers'; import { beforeValidate } from './objectiveTopic'; diff --git a/src/models/hooks/programPersonnel.js b/src/hooks/programPersonnel.js similarity index 100% rename from src/models/hooks/programPersonnel.js rename to src/hooks/programPersonnel.js diff --git a/src/models/hooks/programPersonnel.test.js b/src/hooks/programPersonnel.test.js similarity index 83% rename from src/models/hooks/programPersonnel.test.js rename to src/hooks/programPersonnel.test.js index b14cc3db45..115eacea42 100644 --- a/src/models/hooks/programPersonnel.test.js +++ b/src/hooks/programPersonnel.test.js @@ -1,5 +1,5 @@ import faker from '@faker-js/faker'; -import db from '..'; +import db from '../models'; const { ProgramPersonnel, Grant, Program } = db; @@ -129,6 +129,19 @@ describe('ProgramPersonnel hooks', () => { effectiveDate: new Date(), mapsTo: null, email: faker.internet.email(), + }, { + grantId: grant.id, + programId: program.id, + role: 'director', + title: '', + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + suffix: faker.name.suffix(), + prefix: faker.name.prefix(), + active: false, + effectiveDate: new Date(), + mapsTo: null, + email: faker.internet.email(), }], { individualHooks: false, returning: true, @@ -142,13 +155,15 @@ describe('ProgramPersonnel hooks', () => { }, }); - expect(personnelForGrant.length).toBe(3); // all three, including the two directors + expect(personnelForGrant.length).toBe(4); // all three, including the two directors const activeDirector = personnelForGrant.find((p) => p.role === 'director' && p.active); expect(activeDirector.id).toBe(newDirector.id); expect(activeDirector.mapsTo).toBe(null); - const inactiveDirector = personnelForGrant.find((p) => p.role === 'director' && !p.active); - expect(inactiveDirector.mapsTo).toBe(activeDirector.id); + const inactiveDirectors = personnelForGrant.filter((p) => p.role === 'director' && !p.active); + expect(inactiveDirectors.length).toBe(2); + const inactiveDirector = inactiveDirectors.find((p) => p.mapsTo === activeDirector.id); + expect(inactiveDirector).toBeTruthy(); const cfo = personnelForGrant.find((p) => p.role === 'cfo' && p.active); expect(cfo).toBeTruthy(); }); diff --git a/src/models/hooks/resource.js b/src/hooks/resource.js similarity index 90% rename from src/models/hooks/resource.js rename to src/hooks/resource.js index 12b2cf4d4e..53d7079a88 100644 --- a/src/models/hooks/resource.js +++ b/src/hooks/resource.js @@ -1,4 +1,4 @@ -import { VALID_URL_REGEX } from '../../lib/urlUtils'; +import { VALID_URL_REGEX } from '../lib/urlUtils'; const autoPopulateDomain = (sequelize, instance, options) => { // eslint-disable-next-line no-prototype-builtins @@ -33,7 +33,7 @@ const afterCreate = async (sequelize, instance, options) => { // Model: /models/{Resource} Imports: /models/hooks/resource // Hook: /models/hooks/resource Imports: /services/resourceQueue // eslint-disable-next-line global-require - const { addGetResourceMetadataToQueue } = require('../../services/resourceQueue'); + const { addGetResourceMetadataToQueue } = require('../services/resourceQueue'); addGetResourceMetadataToQueue(instance.id, instance.url); } }; diff --git a/src/models/hooks/resource.test.js b/src/hooks/resource.test.js similarity index 87% rename from src/models/hooks/resource.test.js rename to src/hooks/resource.test.js index 6aee94dd7c..9dac44a169 100644 --- a/src/models/hooks/resource.test.js +++ b/src/hooks/resource.test.js @@ -1,13 +1,13 @@ import { sequelize, Resource, -} from '..'; -import { addGetResourceMetadataToQueue } from '../../services/resourceQueue'; +} from '../models'; +import { addGetResourceMetadataToQueue } from '../services/resourceQueue'; jest.mock('bull'); // Mock addGetResourceMetadataToQueue. -jest.mock('../../services/resourceQueue', () => ({ +jest.mock('../services/resourceQueue', () => ({ addGetResourceMetadataToQueue: jest.fn(), })); diff --git a/src/models/hooks/sessionReportPilot.js b/src/hooks/sessionReportPilot.js similarity index 98% rename from src/models/hooks/sessionReportPilot.js rename to src/hooks/sessionReportPilot.js index e90a875235..586412ce28 100644 --- a/src/models/hooks/sessionReportPilot.js +++ b/src/hooks/sessionReportPilot.js @@ -3,7 +3,7 @@ const { Op } = require('sequelize'); const httpContext = require('express-http-context'); const { TRAINING_REPORT_STATUSES, GOAL_SOURCES } = require('@ttahub/common'); -const { auditLogger } = require('../../logger'); +const { auditLogger } = require('../logger'); const preventChangesIfEventComplete = async (sequelize, instance, options) => { let event; @@ -48,7 +48,7 @@ const notifyPocIfSessionComplete = async (sequelize, instance, options) => { }); if (event) { - const { trSessionCompleted } = require('../../lib/mailer'); + const { trSessionCompleted } = require('../lib/mailer'); await trSessionCompleted(event.dataValues); } } @@ -99,7 +99,7 @@ const notifySessionCreated = async (sequelize, instance, options) => { }); if (event) { - const { trSessionCreated } = require('../../lib/mailer'); + const { trSessionCreated } = require('../lib/mailer'); await trSessionCreated(event.dataValues); } } catch (err) { @@ -123,7 +123,7 @@ const participantsAndNextStepsComplete = async (sequelize, instance, options) => transaction: options.transaction, }); - const { trPocSessionComplete } = require('../../lib/mailer'); + const { trPocSessionComplete } = require('../lib/mailer'); await trPocSessionComplete(event); } } diff --git a/src/models/hooks/sessionReportPilot.test.js b/src/hooks/sessionReportPilot.test.js similarity index 96% rename from src/models/hooks/sessionReportPilot.test.js rename to src/hooks/sessionReportPilot.test.js index 836b0e3aeb..f1c7a291b3 100644 --- a/src/models/hooks/sessionReportPilot.test.js +++ b/src/hooks/sessionReportPilot.test.js @@ -10,10 +10,14 @@ import { removeGoalsForSessionRecipientsIfNecessary, syncGoalCollaborators, } from './sessionReportPilot'; -import { trSessionCreated, trSessionCompleted, trPocSessionComplete } from '../../lib/mailer'; -import db from '..'; +import { trSessionCreated, trSessionCompleted, trPocSessionComplete } from '../lib/mailer'; +import db from '../models'; +import { + createGoal, + createGrant, +} from '../testUtils'; -jest.mock('../../lib/mailer', () => ({ +jest.mock('../lib/mailer', () => ({ trSessionCreated: jest.fn(), trSessionCompleted: jest.fn(), trPocSessionComplete: jest.fn(), @@ -582,6 +586,7 @@ describe('syncGoalCollaborators', () => { let user; let newUser; let eventRecord; + let grant; let goalId; let sessionReport; let transaction; @@ -590,7 +595,9 @@ describe('syncGoalCollaborators', () => { transaction = await db.sequelize.transaction(); user = await db.User.create({ name: 'aa bb', email: 'aabb@cc.com', hsesUsername: 'aabbcc' }, { transaction }); newUser = await db.User.create({ name: 'cc dd', email: 'ccdd@ee.com', hsesUsername: 'ccdd' }, { transaction }); - goalId = 1; + grant = await createGrant({}, { transaction }); + const goal = await createGoal({ grantId: grant.id, status: 'Not Started' }, { transaction }); + goalId = goal.id; sessionReport = { id: 1 }; eventRecord = { pocIds: [user.id] }; @@ -603,6 +610,9 @@ describe('syncGoalCollaborators', () => { afterAll(async () => { await db.GoalCollaborator.destroy({ where: { goalId }, transaction }); await db.CollaboratorType.destroy({ where: { name: ['Creator', 'Linker'] }, transaction }); + await db.Goal.destroy({ where: { id: goalId }, force: true, transaction }); + await db.GrantNumberLink.destroy({ where: { grantId: grant.id }, transaction, force: true }); + await db.Grant.destroy({ where: { id: grant.id }, transaction }); await db.User.destroy({ where: { id: [user.id, newUser.id] }, transaction }); await transaction.rollback(); await db.sequelize.close(); diff --git a/src/models/hooks/testHelpers.js b/src/hooks/testHelpers.js similarity index 95% rename from src/models/hooks/testHelpers.js rename to src/hooks/testHelpers.js index ebc0be2527..b906439488 100644 --- a/src/models/hooks/testHelpers.js +++ b/src/hooks/testHelpers.js @@ -1,7 +1,7 @@ import crypto from 'crypto'; import faker from '@faker-js/faker'; import { REPORT_STATUSES } from '@ttahub/common'; -import { AUTOMATIC_CREATION, FILE_STATUSES } from '../../constants'; +import { AUTOMATIC_CREATION, FILE_STATUSES } from '../constants'; export const draftObject = { activityRecipientType: 'recipient', diff --git a/src/models/activityReport.js b/src/models/activityReport.js index ab71a89fad..63589b19d3 100644 --- a/src/models/activityReport.js +++ b/src/models/activityReport.js @@ -10,7 +10,7 @@ const { afterUpdate, beforeValidate, afterDestroy, -} = require('./hooks/activityReport'); +} = require('../hooks/activityReport'); const generateCreatorNameWithRole = (ar) => { const creatorName = ar.author ? ar.author.name : ''; diff --git a/src/models/activityReportApprover.js b/src/models/activityReportApprover.js index beb9c57d7f..ddb5bfe787 100644 --- a/src/models/activityReportApprover.js +++ b/src/models/activityReportApprover.js @@ -1,11 +1,13 @@ const { Model } = require('sequelize'); const { APPROVER_STATUSES } = require('@ttahub/common'); const { + beforeCreate, + beforeUpdate, afterCreate, afterDestroy, afterRestore, afterUpdate, -} = require('./hooks/activityReportApprover'); +} = require('../hooks/activityReportApprover'); export default (sequelize, DataTypes) => { class ActivityReportApprover extends Model { @@ -43,6 +45,8 @@ export default (sequelize, DataTypes) => { afterDestroy: async (instance) => afterDestroy(sequelize, instance), afterRestore: async (instance) => afterRestore(sequelize, instance), afterUpdate: async (instance) => afterUpdate(sequelize, instance), + beforeUpdate: async (instance) => beforeUpdate(sequelize, instance), + beforeCreate: async (instance) => beforeCreate(sequelize, instance), }, indexes: [{ unique: true, diff --git a/src/models/activityReportFile.js b/src/models/activityReportFile.js index c2cc2fe92b..1f5bc8b2cb 100644 --- a/src/models/activityReportFile.js +++ b/src/models/activityReportFile.js @@ -1,5 +1,5 @@ const { Model } = require('sequelize'); -const { beforeDestroy, afterDestroy } = require('./hooks/activityReportFile'); +const { beforeDestroy, afterDestroy } = require('../hooks/activityReportFile'); export default (sequelize, DataTypes) => { class ActivityReportFile extends Model { diff --git a/src/models/activityReportGoal.js b/src/models/activityReportGoal.js index 672c277f39..8f0740beed 100644 --- a/src/models/activityReportGoal.js +++ b/src/models/activityReportGoal.js @@ -8,7 +8,7 @@ const { afterUpdate, beforeValidate, beforeUpdate, -} = require('./hooks/activityReportGoal'); +} = require('../hooks/activityReportGoal'); export default (sequelize, DataTypes) => { class ActivityReportGoal extends Model { diff --git a/src/models/activityReportGoalFieldResponse.js b/src/models/activityReportGoalFieldResponse.js index cfecdc2d5a..df74afd407 100644 --- a/src/models/activityReportGoalFieldResponse.js +++ b/src/models/activityReportGoalFieldResponse.js @@ -2,7 +2,7 @@ const { Model } = require('sequelize'); const { afterCreate, afterDestroy, -} = require('./hooks/activityReportGoalFieldResponse'); +} = require('../hooks/activityReportGoalFieldResponse'); export default (sequelize, DataTypes) => { class ActivityReportGoalFieldResponse extends Model { diff --git a/src/models/activityReportGoalResource.js b/src/models/activityReportGoalResource.js index ee1be3f384..dcc05731c0 100644 --- a/src/models/activityReportGoalResource.js +++ b/src/models/activityReportGoalResource.js @@ -1,6 +1,6 @@ const { Model } = require('sequelize'); const { SOURCE_FIELD } = require('../constants'); -const { afterCreate, afterDestroy } = require('./hooks/activityReportGoalResource'); +const { afterCreate, afterDestroy } = require('../hooks/activityReportGoalResource'); export default (sequelize, DataTypes) => { class ActivityReportGoalResource extends Model { diff --git a/src/models/activityReportObjective.js b/src/models/activityReportObjective.js index 1fce379712..861a013b99 100644 --- a/src/models/activityReportObjective.js +++ b/src/models/activityReportObjective.js @@ -6,7 +6,7 @@ const { beforeDestroy, afterDestroy, afterUpdate, -} = require('./hooks/activityReportObjective'); +} = require('../hooks/activityReportObjective'); export default (sequelize, DataTypes) => { class ActivityReportObjective extends Model { diff --git a/src/models/activityReportObjectiveFile.js b/src/models/activityReportObjectiveFile.js index e3939ae65d..aea248869d 100644 --- a/src/models/activityReportObjectiveFile.js +++ b/src/models/activityReportObjectiveFile.js @@ -1,5 +1,5 @@ const { Model } = require('sequelize'); -const { afterDestroy } = require('./hooks/activityReportObjectiveFile'); +const { afterDestroy } = require('../hooks/activityReportObjectiveFile'); export default (sequelize, DataTypes) => { class ActivityReportObjectiveFile extends Model { diff --git a/src/models/activityReportObjectiveResource.js b/src/models/activityReportObjectiveResource.js index 141f196320..2a7d5d707c 100644 --- a/src/models/activityReportObjectiveResource.js +++ b/src/models/activityReportObjectiveResource.js @@ -1,6 +1,6 @@ const { Model } = require('sequelize'); const { SOURCE_FIELD } = require('../constants'); -const { afterCreate, afterDestroy } = require('./hooks/activityReportObjectiveResource'); +const { afterCreate, afterDestroy } = require('../hooks/activityReportObjectiveResource'); export default (sequelize, DataTypes) => { class ActivityReportObjectiveResource extends Model { diff --git a/src/models/activityReportObjectiveTopic.js b/src/models/activityReportObjectiveTopic.js index 6f1fbfb440..9f0da34173 100644 --- a/src/models/activityReportObjectiveTopic.js +++ b/src/models/activityReportObjectiveTopic.js @@ -1,5 +1,5 @@ const { Model } = require('sequelize'); -const { afterDestroy } = require('./hooks/activityReportObjectiveTopic'); +const { afterDestroy } = require('../hooks/activityReportObjectiveTopic'); /** * ObjectiveTopic table. Junction table diff --git a/src/models/activityReportResource.js b/src/models/activityReportResource.js index 73bd849558..7e813aab60 100644 --- a/src/models/activityReportResource.js +++ b/src/models/activityReportResource.js @@ -1,6 +1,6 @@ const { Model } = require('sequelize'); const { SOURCE_FIELD } = require('../constants'); -const { afterDestroy } = require('./hooks/activityReportResource'); +const { afterDestroy } = require('../hooks/activityReportResource'); export default (sequelize, DataTypes) => { class ActivityReportResource extends Model { diff --git a/src/models/communicationLogFile.js b/src/models/communicationLogFile.js index df1fcdf1e2..d34afd05b3 100644 --- a/src/models/communicationLogFile.js +++ b/src/models/communicationLogFile.js @@ -1,5 +1,5 @@ const { Model } = require('sequelize'); -const { afterDestroy } = require('./hooks/communicationLogFile'); +const { afterDestroy } = require('../hooks/communicationLogFile'); export default (sequelize, DataTypes) => { class CommunicationLogFile extends Model { diff --git a/src/models/eventReportPilot.js b/src/models/eventReportPilot.js index c760585f13..ed6eafec65 100644 --- a/src/models/eventReportPilot.js +++ b/src/models/eventReportPilot.js @@ -1,5 +1,10 @@ const { Model } = require('sequelize'); -const { afterUpdate, beforeUpdate, afterCreate } = require('./hooks/eventReportPilot'); +const { + afterUpdate, + beforeUpdate, + afterCreate, + beforeCreate, +} = require('../hooks/eventReportPilot'); export default (sequelize, DataTypes) => { class EventReportPilot extends Model { @@ -45,7 +50,8 @@ export default (sequelize, DataTypes) => { hooks: { afterCreate: async (instance, options) => afterCreate(sequelize, instance, options), afterUpdate: async (instance, options) => afterUpdate(sequelize, instance, options), - beforeUpdate: async (instance, options) => beforeUpdate(sequelize, instance, options), + beforeCreate: async (instance, options) => beforeUpdate(sequelize, instance, options), + beforeUpdate: async (instance, options) => beforeCreate(sequelize, instance, options), }, }); diff --git a/src/models/file.js b/src/models/file.js index d6129dd114..3515f63171 100644 --- a/src/models/file.js +++ b/src/models/file.js @@ -1,6 +1,6 @@ const { Model } = require('sequelize'); const { getPresignedURL } = require('../lib/s3'); -const { afterDestroy } = require('./hooks/file'); +const { afterDestroy } = require('../hooks/file'); export default (sequelize, DataTypes) => { class File extends Model { diff --git a/src/models/goal.js b/src/models/goal.js index 88445db421..04f14f0cd7 100644 --- a/src/models/goal.js +++ b/src/models/goal.js @@ -7,7 +7,7 @@ const { afterCreate, afterUpdate, afterDestroy, -} = require('./hooks/goal'); +} = require('../hooks/goal'); const { GOAL_CREATED_VIA } = require('../constants'); export const RTTAPA_ENUM = ['Yes', 'No']; diff --git a/src/models/goalFieldResponse.js b/src/models/goalFieldResponse.js index 7a0f72baae..d5f266f204 100644 --- a/src/models/goalFieldResponse.js +++ b/src/models/goalFieldResponse.js @@ -3,7 +3,7 @@ const { beforeValidate, afterUpdate, afterCreate, -} = require('./hooks/goalFieldResponse'); +} = require('../hooks/goalFieldResponse'); export default (sequelize, DataTypes) => { class GoalFieldResponse extends Model { diff --git a/src/models/goalResource.js b/src/models/goalResource.js index db85790d54..c7a1970197 100644 --- a/src/models/goalResource.js +++ b/src/models/goalResource.js @@ -1,6 +1,6 @@ const { Model } = require('sequelize'); const { SOURCE_FIELD } = require('../constants'); -const { afterDestroy } = require('./hooks/goalResource'); +const { afterDestroy } = require('../hooks/goalResource'); export default (sequelize, DataTypes) => { class GoalResource extends Model { diff --git a/src/models/goalTemplate.js b/src/models/goalTemplate.js index d85c234936..a8ce513876 100644 --- a/src/models/goalTemplate.js +++ b/src/models/goalTemplate.js @@ -5,7 +5,7 @@ const { beforeUpdate, afterCreate, afterUpdate, -} = require('./hooks/goalTemplate'); +} = require('../hooks/goalTemplate'); // const { auditLogger } = require('../logger'); export default (sequelize, DataTypes) => { diff --git a/src/models/goalTemplateResource.js b/src/models/goalTemplateResource.js index 76a6ab0e01..e8a18864cb 100644 --- a/src/models/goalTemplateResource.js +++ b/src/models/goalTemplateResource.js @@ -1,6 +1,6 @@ const { Model } = require('sequelize'); const { SOURCE_FIELD } = require('../constants'); -const { afterDestroy } = require('./hooks/goalTemplateResource'); +const { afterDestroy } = require('../hooks/goalTemplateResource'); export default (sequelize, DataTypes) => { class GoalTemplateResource extends Model { diff --git a/src/models/grant.js b/src/models/grant.js index 888f7c3084..396a40ec20 100644 --- a/src/models/grant.js +++ b/src/models/grant.js @@ -5,7 +5,7 @@ const { afterCreate, afterUpdate, beforeDestroy, -} = require('./hooks/grant'); +} = require('../hooks/grant'); const { GRANT_INACTIVATION_REASONS } = require('../constants'); diff --git a/src/models/group.js b/src/models/group.js index e6a075c863..92f17cbc19 100644 --- a/src/models/group.js +++ b/src/models/group.js @@ -5,7 +5,7 @@ const { GROUP_SHARED_WITH } = require('@ttahub/common'); const { afterCreate, afterUpdate, -} = require('./hooks/group'); +} = require('../hooks/group'); export default (sequelize, DataTypes) => { class Group extends Model { diff --git a/src/models/helpers/escapeFields.js b/src/models/helpers/escapeFields.js new file mode 100644 index 0000000000..99daa6fb05 --- /dev/null +++ b/src/models/helpers/escapeFields.js @@ -0,0 +1,52 @@ +import { escape } from 'lodash'; +import { auditLogger } from '../../logger'; +import safeParse from './safeParse'; + +/** + * Intended to be used in sequelize hooks "beforeCreate" and "beforeUpdate" + * Escapes fields in the data object of a sequelize model instance + * + * @param {Object} instance - Sequelize model instance + * @param {String[]} fields - Array of fields to escape + * @returns void + */ +export function escapeDataFields(instance, fields) { + const data = safeParse(instance); + if (!data) return; + + const copy = { ...data }; + + try { + fields.forEach((field) => { + if (field in copy && copy[field] !== null) { + copy[field] = escape(copy[field]); + } + }); + + instance.set('data', copy); + } catch (err) { + auditLogger.error(JSON.stringify({ 'Error escaping fields': err, instance })); + } +} + +/** + * Escape fields in sequelize instance + * Intended to be user in sequelize hooks "beforeCreate" and "beforeUpdate" + * + * @param {Object} instance - sequelize model instance + * @param {String[]} fields - array of fields to escape + * @returns void + */ +export default function escapeFields(instance, fields) { + const changed = instance.changed(); + + try { + fields.forEach((field) => { + if (changed.includes(field) && instance[field] !== null) { + instance.set(field, escape(instance[field])); + } + }); + } catch (err) { + auditLogger.error(JSON.stringify({ 'Error escaping fields': err, instance })); + } +} diff --git a/src/models/helpers/safeParse.js b/src/models/helpers/safeParse.js new file mode 100644 index 0000000000..def39b5755 --- /dev/null +++ b/src/models/helpers/safeParse.js @@ -0,0 +1,18 @@ +function safeParse(instance) { + // Try to parse instance.data if it exists and has a 'val' property + if (instance?.data?.val) { + return JSON.parse(instance.data.val); + } + // Directly return instance.dataValues.data if it exists + if (instance?.dataValues?.data) { + return instance.dataValues.data; + } + // Directly return instance.data if it exists + if (instance?.data) { + return instance.data; + } + + return null; +} + +module.exports = safeParse; diff --git a/src/models/helpers/tests/escapeFields.test.js b/src/models/helpers/tests/escapeFields.test.js new file mode 100644 index 0000000000..6475af9799 --- /dev/null +++ b/src/models/helpers/tests/escapeFields.test.js @@ -0,0 +1,199 @@ +import faker from '@faker-js/faker'; +import { APPROVER_STATUSES } from '@ttahub/common'; +import db from '../..'; +import { + createReport, + createTrainingReport, + createRegion, + createUser, + destroyReport, +} from '../../../testUtils'; +import escapeFields, { escapeDataFields } from '../escapeFields'; + +const { + ActivityReportApprover, + ActivityReport, + EventReportPilot, +} = db; + +const xss = ''; +const safe = '<script>alert("XSS")</script>'; + +describe('escapeFields', () => { + test('should escape specified fields in the instance', () => { + const instance = { + field1: xss, + field2: 'Hello World', + field3: null, + set: jest.fn(), + changed: jest.fn().mockReturnValue(['field1', 'field2']), + }; + + const fieldsToEscape = ['field1', 'field2']; + + escapeFields(instance, fieldsToEscape); + + expect(instance.set).toHaveBeenCalledTimes(2); + expect(instance.set).toHaveBeenCalledWith('field1', safe); + expect(instance.set).toHaveBeenCalledWith('field2', 'Hello World'); + }); + + test('should not escape fields that are null', () => { + const instance = { + field1: null, + field2: 'Hello World', + set: jest.fn(), + changed: jest.fn().mockReturnValue(['field1', 'field2']), + }; + + const fieldsToEscape = ['field1', 'field2']; + + escapeFields(instance, fieldsToEscape); + + expect(instance.set).toHaveBeenCalledTimes(1); + expect(instance.set).toHaveBeenCalledWith('field2', 'Hello World'); + }); + + test('should not modify the instance if it does not have the specified fields', () => { + const instance = { + set: jest.fn(), + field1: null, + field2: 'Hello World', + changed: jest.fn().mockReturnValue(['field1']), + }; + + const fieldsToEscape = ['field1', 'field2']; + + escapeFields(instance, fieldsToEscape); + + expect(instance.set).not.toHaveBeenCalled(); + }); + + describe('escapeDataFields', () => { + test('should escape specified fields in the instance data', () => { + const instance = { + data: { + field1: xss, + field2: 'Hello World', + field3: null, + }, + set: jest.fn(), + }; + + const fieldsToEscape = ['field1', 'field2']; + + escapeDataFields(instance, fieldsToEscape); + + expect(instance.set).toHaveBeenCalledTimes(1); + expect(instance.set).toHaveBeenCalledWith('data', { + field1: safe, + field2: 'Hello World', + field3: null, + }); + }); + + test('should not escape fields that are null', () => { + const instance = { + data: { + field1: null, + field2: 'Hello World', + }, + set: jest.fn(), + }; + + const fieldsToEscape = ['field1', 'field2']; + + escapeDataFields(instance, fieldsToEscape); + + expect(instance.set).toHaveBeenCalledTimes(1); + expect(instance.set).toHaveBeenCalledWith('data', { + field1: null, + field2: 'Hello World', + }); + }); + + test('should not modify the instance data if it is not present', () => { + const instance = { + set: jest.fn(), + }; + + const fieldsToEscape = ['field1', 'field2']; + + escapeDataFields(instance, fieldsToEscape); + + expect(instance.set).not.toHaveBeenCalled(); + }); + }); + + describe('live updates', () => { + let region; + let user; + let approver; + let report; + let event; + + beforeAll(async () => { + region = await createRegion(); + user = await createUser({ homeRegionId: region.id }); + report = await createReport({ + context: xss, + activityRecipients: [{ grantId: faker.datatype.number({ min: 99_000 }) }], + userId: user.id, + regionId: region.id, + }); + + approver = await ActivityReportApprover.create({ + activityReportId: report.id, + userId: user.id, + status: APPROVER_STATUSES.PENDING, + note: xss, + }); + + event = await createTrainingReport({ + regionId: region.id, + collaboratorIds: [user.id], + pocIds: [user.id], + ownerId: user.id, + data: { + eventName: xss, + }, + }); + }); + + it('properly escaped fields', async () => { + expect(report.context).toBe(safe); + expect(approver.note).toBe(safe); + expect(event.data.eventName).toBe(safe); + + await ActivityReport.update( + { context: xss }, + { where: { id: report.id }, individualHooks: true }, + ); + await ActivityReportApprover.update( + { note: xss }, + { where: { id: approver.id }, individualHooks: true }, + ); + await EventReportPilot.update( + { data: { ...event.data, eventName: xss } }, + { where: { id: event.id }, individualHooks: true }, + ); + + await report.reload(); + await approver.reload(); + await event.reload(); + + expect(report.context).toBe(safe); + expect(approver.note).toBe(safe); + expect(event.data.eventName).toBe(safe); + }); + + afterAll(async () => { + await approver.destroy({ force: true }); + await event.destroy(); + await destroyReport(report); + await user.destroy(); + await region.destroy(); + await db.sequelize.close(); + }); + }); +}); diff --git a/src/models/helpers/tests/orphanCleanupHelper.test.js b/src/models/helpers/tests/orphanCleanupHelper.test.js new file mode 100644 index 0000000000..b112f03100 --- /dev/null +++ b/src/models/helpers/tests/orphanCleanupHelper.test.js @@ -0,0 +1,41 @@ +const { cleanupOrphanResources, cleanupOrphanFiles } = require('../orphanCleanupHelper'); + +describe('orphanCleanupHelper', () => { + describe('cleanupOrphanFiles', () => { + it('should delete orphan files', async () => { + const sequelize = { + models: { + File: { + destroy: jest.fn(), + }, + }, + literal: jest.fn(), + }; // Replace with your sequelize instance + const fileId = 123; // Replace with the file ID you want to delete + + await cleanupOrphanFiles(sequelize, fileId); + + expect(sequelize.models.File.destroy).toHaveBeenCalledTimes(1); + expect(sequelize.literal).toHaveBeenCalledWith(expect.any(String)); + }); + }); + + describe('cleanupOrphanResources', () => { + it('should delete orphan resources', async () => { + const sequelize = { + models: { + Resource: { + destroy: jest.fn(), + }, + }, + literal: jest.fn(), + }; // Replace with your sequelize instance + const resourceId = 123; // Replace with the resource ID you want to delete + + await cleanupOrphanResources(sequelize, resourceId); + + expect(sequelize.models.Resource.destroy).toHaveBeenCalledTimes(1); + expect(sequelize.literal).toHaveBeenCalledWith(expect.any(String)); + }); + }); +}); diff --git a/src/models/helpers/tests/safeParse.test.js b/src/models/helpers/tests/safeParse.test.js new file mode 100644 index 0000000000..4789f7e3ef --- /dev/null +++ b/src/models/helpers/tests/safeParse.test.js @@ -0,0 +1,45 @@ +import safeParse from '../safeParse'; + +describe('safeParse', () => { + test('should parse instance.data if it exists and has a "val" property', () => { + const instance = { + data: { + val: '{"key": "value"}', + }, + }; + + const result = safeParse(instance); + + expect(result).toEqual({ key: 'value' }); + }); + + test('should return instance.dataValues.data if it exists', () => { + const instance = { + dataValues: { + data: { key: 'value' }, + }, + }; + + const result = safeParse(instance); + + expect(result).toEqual({ key: 'value' }); + }); + + test('should return instance.data if it exists', () => { + const instance = { + data: { key: 'value' }, + }; + + const result = safeParse(instance); + + expect(result).toEqual({ key: 'value' }); + }); + + test('should return null if none of the conditions are met', () => { + const instance = {}; + + const result = safeParse(instance); + + expect(result).toBeNull(); + }); +}); diff --git a/src/models/importFile.js b/src/models/importFile.js index e9b10f9811..9544175e5a 100644 --- a/src/models/importFile.js +++ b/src/models/importFile.js @@ -1,7 +1,7 @@ const { Model, } = require('sequelize'); -const { afterDestroy } = require('./hooks/importFile'); +const { afterDestroy } = require('../hooks/importFile'); const { IMPORT_STATUSES } = require('../constants'); export default (sequelize, DataTypes) => { diff --git a/src/models/monitoringClassSummary.js b/src/models/monitoringClassSummary.js index 84ee1c71fb..4ae529991f 100644 --- a/src/models/monitoringClassSummary.js +++ b/src/models/monitoringClassSummary.js @@ -2,7 +2,7 @@ import { Model } from 'sequelize'; import { beforeCreate, beforeUpdate, -} from './hooks/monitoringClassSummary'; +} from '../hooks/monitoringClassSummary'; export default (sequelize, DataTypes) => { class MonitoringClassSummary extends Model { diff --git a/src/models/monitoringFindingHistory.js b/src/models/monitoringFindingHistory.js index fece0b24ea..735ee45032 100644 --- a/src/models/monitoringFindingHistory.js +++ b/src/models/monitoringFindingHistory.js @@ -2,7 +2,7 @@ import { Model } from 'sequelize'; import { beforeCreate, beforeUpdate, -} from './hooks/monitoringFindingHistory'; +} from '../hooks/monitoringFindingHistory'; export default (sequelize, DataTypes) => { class MonitoringFindingHistory extends Model { diff --git a/src/models/monitoringReview.js b/src/models/monitoringReview.js index 2a5b68052d..cd5a618d71 100644 --- a/src/models/monitoringReview.js +++ b/src/models/monitoringReview.js @@ -2,7 +2,7 @@ import { Model } from 'sequelize'; import { beforeCreate, beforeUpdate, -} from './hooks/monitoringReview'; +} from '../hooks/monitoringReview'; export default (sequelize, DataTypes) => { class MonitoringReview extends Model { diff --git a/src/models/monitoringReviewGrantee.js b/src/models/monitoringReviewGrantee.js index a76f71ec4b..7a45524b73 100644 --- a/src/models/monitoringReviewGrantee.js +++ b/src/models/monitoringReviewGrantee.js @@ -2,7 +2,7 @@ import { Model } from 'sequelize'; import { beforeCreate, beforeUpdate, -} from './hooks/monitoringReviewGrantee'; +} from '../hooks/monitoringReviewGrantee'; export default (sequelize, DataTypes) => { class MonitoringReviewGrantee extends Model { diff --git a/src/models/monitoringReviewStatus.js b/src/models/monitoringReviewStatus.js index 4db6981350..298561b9d8 100644 --- a/src/models/monitoringReviewStatus.js +++ b/src/models/monitoringReviewStatus.js @@ -2,7 +2,7 @@ import { Model } from 'sequelize'; import { beforeCreate, beforeUpdate, -} from './hooks/monitoringReviewStatus'; +} from '../hooks/monitoringReviewStatus'; export default (sequelize, DataTypes) => { class MonitoringReviewStatus extends Model { diff --git a/src/models/nationalCenter.js b/src/models/nationalCenter.js index dd39169698..8cc0029104 100644 --- a/src/models/nationalCenter.js +++ b/src/models/nationalCenter.js @@ -1,5 +1,5 @@ const { Model } = require('sequelize'); -const { afterDestroy, afterUpdate } = require('./hooks/nationalCenter'); +const { afterDestroy, afterUpdate } = require('../hooks/nationalCenter'); export default (sequelize, DataTypes) => { class NationalCenter extends Model { diff --git a/src/models/nextStep.js b/src/models/nextStep.js index a1f5ef7a16..8609fdf15e 100644 --- a/src/models/nextStep.js +++ b/src/models/nextStep.js @@ -4,7 +4,7 @@ const { formatDate } = require('../lib/modelHelpers'); const { afterCreate, afterUpdate, -} = require('./hooks/goal'); +} = require('../hooks/goal'); export default (sequelize, DataTypes) => { class NextStep extends Model { diff --git a/src/models/nextStepResource.js b/src/models/nextStepResource.js index fcd784a18e..7ba5c301aa 100644 --- a/src/models/nextStepResource.js +++ b/src/models/nextStepResource.js @@ -1,6 +1,6 @@ const { Model } = require('sequelize'); const { SOURCE_FIELD } = require('../constants'); -const { afterDestroy } = require('./hooks/nextStepResource'); +const { afterDestroy } = require('../hooks/nextStepResource'); export default (sequelize, DataTypes) => { class NextStepResource extends Model { diff --git a/src/models/objective.js b/src/models/objective.js index ec317a7a98..e683ceebb9 100644 --- a/src/models/objective.js +++ b/src/models/objective.js @@ -7,7 +7,7 @@ const { beforeUpdate, afterUpdate, afterCreate, -} = require('./hooks/objective'); +} = require('../hooks/objective'); /** * Objective table. Stores objectives for goals. diff --git a/src/models/objectiveFile.js b/src/models/objectiveFile.js index 73e537ce49..5e2aea52e2 100644 --- a/src/models/objectiveFile.js +++ b/src/models/objectiveFile.js @@ -4,7 +4,7 @@ const { afterCreate, beforeDestroy, afterDestroy, -} = require('./hooks/objectiveFile'); +} = require('../hooks/objectiveFile'); export default (sequelize, DataTypes) => { class ObjectiveFile extends Model { diff --git a/src/models/objectiveResource.js b/src/models/objectiveResource.js index 099c2dcf9e..3ec7a8df28 100644 --- a/src/models/objectiveResource.js +++ b/src/models/objectiveResource.js @@ -1,6 +1,6 @@ const { Model } = require('sequelize'); const { SOURCE_FIELD } = require('../constants'); -const { beforeValidate, afterCreate, afterDestroy } = require('./hooks/objectiveResource'); +const { beforeValidate, afterCreate, afterDestroy } = require('../hooks/objectiveResource'); export default (sequelize, DataTypes) => { class ObjectiveResource extends Model { diff --git a/src/models/objectiveTemplate.js b/src/models/objectiveTemplate.js index 80f485e691..14301ebd22 100644 --- a/src/models/objectiveTemplate.js +++ b/src/models/objectiveTemplate.js @@ -1,6 +1,6 @@ const { Model } = require('sequelize'); const { CREATION_METHOD } = require('../constants'); -const { beforeValidate, beforeUpdate, afterUpdate } = require('./hooks/objectiveTemplate'); +const { beforeValidate, beforeUpdate, afterUpdate } = require('../hooks/objectiveTemplate'); // const { auditLogger } = require('../logger'); export default (sequelize, DataTypes) => { diff --git a/src/models/objectiveTemplateFile.js b/src/models/objectiveTemplateFile.js index b7fc3071d2..11b44157bf 100644 --- a/src/models/objectiveTemplateFile.js +++ b/src/models/objectiveTemplateFile.js @@ -1,5 +1,5 @@ const { Model } = require('sequelize'); -const { afterDestroy } = require('./hooks/objectiveTemplateFile'); +const { afterDestroy } = require('../hooks/objectiveTemplateFile'); export default (sequelize, DataTypes) => { class ObjectiveTemplateFile extends Model { diff --git a/src/models/objectiveTemplateResource.js b/src/models/objectiveTemplateResource.js index 093f7bc09d..c395d61aa1 100644 --- a/src/models/objectiveTemplateResource.js +++ b/src/models/objectiveTemplateResource.js @@ -1,6 +1,6 @@ const { Model } = require('sequelize'); const { SOURCE_FIELD } = require('../constants'); -const { afterDestroy } = require('./hooks/nextStepResource'); +const { afterDestroy } = require('../hooks/nextStepResource'); export default (sequelize, DataTypes) => { class ObjectiveTemplateResource extends Model { diff --git a/src/models/objectiveTopic.js b/src/models/objectiveTopic.js index c42acb91da..19240ef9d4 100644 --- a/src/models/objectiveTopic.js +++ b/src/models/objectiveTopic.js @@ -1,5 +1,5 @@ const { Model } = require('sequelize'); -const { beforeValidate, afterCreate, afterDestroy } = require('./hooks/objectiveTopic'); +const { beforeValidate, afterCreate, afterDestroy } = require('../hooks/objectiveTopic'); /** * ObjectiveTopic table. Junction table diff --git a/src/models/programPersonnel.js b/src/models/programPersonnel.js index b935cf1e6e..54011c2512 100644 --- a/src/models/programPersonnel.js +++ b/src/models/programPersonnel.js @@ -3,7 +3,7 @@ const { } = require('sequelize'); const { afterBulkCreate, -} = require('./hooks/programPersonnel'); +} = require('../hooks/programPersonnel'); export default (sequelize, DataTypes) => { class ProgramPersonnel extends Model { diff --git a/src/models/resource.js b/src/models/resource.js index 2f4611f4de..1c36350ca5 100644 --- a/src/models/resource.js +++ b/src/models/resource.js @@ -1,5 +1,5 @@ const { Model } = require('sequelize'); -const { beforeValidate, afterCreate } = require('./hooks/resource'); +const { beforeValidate, afterCreate } = require('../hooks/resource'); export default (sequelize, DataTypes) => { class Resource extends Model { diff --git a/src/models/sessionReportPilot.js b/src/models/sessionReportPilot.js index 80bc41ed8c..65f1d47e3b 100644 --- a/src/models/sessionReportPilot.js +++ b/src/models/sessionReportPilot.js @@ -5,7 +5,7 @@ const { beforeCreate, beforeUpdate, beforeDestroy, -} = require('./hooks/sessionReportPilot'); +} = require('../hooks/sessionReportPilot'); export default (sequelize, DataTypes) => { class SessionReportPilot extends Model { diff --git a/src/models/sessionReportPilotFile.js b/src/models/sessionReportPilotFile.js index 2897ac5b77..d6ab3357c8 100644 --- a/src/models/sessionReportPilotFile.js +++ b/src/models/sessionReportPilotFile.js @@ -1,5 +1,5 @@ const { Model } = require('sequelize'); -const { afterDestroy } = require('./hooks/objectiveTemplateFile'); +const { afterDestroy } = require('../hooks/objectiveTemplateFile'); export default (sequelize, DataTypes) => { class SessionReportPilotFile extends Model { diff --git a/src/models/tests/activityReport.test.js b/src/models/tests/activityReport.test.js index 2e69ac8aba..3571818b10 100644 --- a/src/models/tests/activityReport.test.js +++ b/src/models/tests/activityReport.test.js @@ -16,7 +16,7 @@ import db, { import { auditLogger } from '../../logger'; import { copyStatus, -} from '../hooks/activityReport'; +} from '../../hooks/activityReport'; import { scheduleUpdateIndexDocumentJob, scheduleDeleteIndexDocumentJob } from '../../lib/awsElasticSearch/queueManager'; jest.mock('../../lib/awsElasticSearch/queueManager'); diff --git a/src/models/tests/activityReportCollaborator.test.js b/src/models/tests/activityReportCollaborator.test.js new file mode 100644 index 0000000000..209f67ec50 --- /dev/null +++ b/src/models/tests/activityReportCollaborator.test.js @@ -0,0 +1,80 @@ +import faker from '@faker-js/faker'; +import db from '..'; +import { + createUser, + createReport, + destroyReport, +} from '../../testUtils'; + +const { + ActivityReportCollaborator, + Role, + CollaboratorRole, + User, +} = db; + +describe('activityReportCollaborator', () => { + let ar; + let ar2; + let user; + let role; + let arc1; + let arc2; + const roleName = faker.commerce.color() + + faker.commerce.department() + + faker.word.adjective() + + faker.datatype.number(10); + + beforeAll(async () => { + user = await createUser(); + ar = await createReport({ userId: user.id, activityRecipients: [] }); + ar2 = await createReport({ userId: user.id, activityRecipients: [] }); + await db.sequelize.query('ALTER SEQUENCE "Roles_id_seq" RESTART WITH 1000;'); + role = await Role.create({ name: roleName, fullName: roleName, isSpecialist: true }); + arc1 = await ActivityReportCollaborator.create({ userId: user.id, activityReportId: ar.id }); + arc2 = await ActivityReportCollaborator.create({ userId: user.id, activityReportId: ar2.id }); + await CollaboratorRole.create({ activityReportCollaboratorId: arc1.id, roleId: role.id }); + }); + + afterAll(async () => { + await CollaboratorRole.destroy({ where: { activityReportCollaboratorId: arc1.id } }); + await arc1.destroy(); + await arc2.destroy(); + await role.destroy(); + await destroyReport(ar2); + await destroyReport(ar); + await user.destroy(); + + await db.sequelize.close(); + }); + + it('should generate a full name based on the user and roles', async () => { + const arc = await ActivityReportCollaborator.findByPk(arc1.id, { + include: [{ + model: Role, + as: 'roles', + }, { + model: User, + as: 'user', + }], + }); + + const { fullName } = arc; + expect(fullName).toEqual(`${user.name}, ${roleName}`); + }); + + it('should generate a full name based on the user and roles when there are no roles', async () => { + const arc = await ActivityReportCollaborator.findByPk(arc2.id, { + include: [{ + model: Role, + as: 'roles', + }, { + model: User, + as: 'user', + }], + }); + + const { fullName } = arc; + expect(fullName).toEqual(`${user.name}`); + }); +}); diff --git a/src/models/tests/activityReportGoalResource.test.js b/src/models/tests/activityReportGoalResource.test.js new file mode 100644 index 0000000000..2923b96258 --- /dev/null +++ b/src/models/tests/activityReportGoalResource.test.js @@ -0,0 +1,60 @@ +import faker from '@faker-js/faker'; +import db from '..'; +import { + createGoal, createGrant, createReport, destroyReport, +} from '../../testUtils'; + +const { calculateIsAutoDetectedForActivityReportGoal } = require('../../services/resource'); + +jest.mock('../../services/resource', () => ({ + ...jest.requireActual('../../services/resource'), + calculateIsAutoDetectedForActivityReportGoal: jest.fn(), +})); + +const { ActivityReportGoalResource } = db; + +describe('ActivityReportGoalResource', () => { + let grant; + let goal; + let ar; + let arg; + let resource; + let argr; + + beforeAll(async () => { + jest.clearAllMocks(); + + grant = await createGrant(); + goal = await createGoal({ grantId: grant.id, status: 'In Progress' }); + ar = await createReport({ activityRecipients: [] }); + arg = await db.ActivityReportGoal.create({ activityReportId: ar.id, goalId: goal.id }); + resource = await db.Resource.create({ url: `${faker.internet.url()}/activity-report-goal-resource.aspx` }); + + argr = await ActivityReportGoalResource.create({ + resourceId: resource.id, + activityReportGoalId: arg.id, + sourceFields: ['name'], + }); + }); + + afterAll(async () => { + jest.clearAllMocks(); + await ActivityReportGoalResource.destroy({ where: { id: argr.id } }); + await db.Resource.destroy({ where: { id: resource.id } }); + await db.ActivityReportGoal.destroy({ where: { id: arg.id } }); + await destroyReport(ar); + await db.Goal.destroy({ where: { id: goal.id }, force: true }); + await db.GrantNumberLink.destroy({ where: { grantId: grant.id }, force: true }); + await db.Grant.destroy({ where: { id: grant.id } }); + await db.sequelize.close(); + }); + + it('calculates isAutoDetected', async () => { + calculateIsAutoDetectedForActivityReportGoal.mockReturnValue(true); + await argr.reload(); + + expect(argr.isAutoDetected).toBe(true); + + expect(calculateIsAutoDetectedForActivityReportGoal).toHaveBeenCalled(); + }); +}); diff --git a/src/models/tests/activityReportObjective.test.js b/src/models/tests/activityReportObjective.test.js new file mode 100644 index 0000000000..4f66792034 --- /dev/null +++ b/src/models/tests/activityReportObjective.test.js @@ -0,0 +1,64 @@ +import db from '..'; +import { beforeDestroy, afterUpdate, afterDestroy } from '../../hooks/activityReportObjective'; +import { createReport, destroyReport } from '../../testUtils'; + +jest.mock('../../hooks/activityReportObjective', () => ({ + beforeDestroy: jest.fn(), + beforeValidate: jest.fn(), + afterCreate: jest.fn(), + afterDestroy: jest.fn(), + afterUpdate: jest.fn(), +})); + +describe('activityReportObjective', () => { + let report; + let objective; + + beforeAll(async () => { + report = await createReport({ activityRecipients: [] }); + objective = await db.Objective.create({ + name: 'Test Objective', + status: 'In Progress', + }); + + await db.ActivityReportObjective.create({ + activityReportId: report.id, + objectiveId: objective.id, + }, { individualHooks: true }); + }); + + afterAll(async () => { + jest.clearAllMocks(); + await db.ActivityReportObjective.destroy({ + where: { + objectiveId: objective.id, + }, + }); + await db.Objective.destroy({ where: { id: objective.id }, force: true }); + await destroyReport(report); + await db.sequelize.close(); + }); + + it('calls hooks', async () => { + await db.ActivityReportObjective.update({ + status: 'Complete', + }, { + where: { + objectiveId: objective.id, + }, + individualHooks: true, + }); + + expect(afterUpdate).toHaveBeenCalledTimes(1); + + await db.ActivityReportObjective.destroy({ + where: { + objectiveId: objective.id, + }, + individualHooks: true, + }); + + expect(beforeDestroy).toHaveBeenCalledTimes(1); + expect(afterDestroy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/models/tests/activityReportObjectiveResource.test.js b/src/models/tests/activityReportObjectiveResource.test.js new file mode 100644 index 0000000000..7f30e64f38 --- /dev/null +++ b/src/models/tests/activityReportObjectiveResource.test.js @@ -0,0 +1,69 @@ +import faker from '@faker-js/faker'; +import db from '..'; +import { + createReport, destroyReport, +} from '../../testUtils'; +import { afterCreate, afterDestroy } from '../../hooks/activityReportObjectiveResource'; +import { calculateIsAutoDetectedForActivityReportObjective } from '../../services/resource'; + +jest.mock('../../hooks/activityReportObjectiveResource', () => ({ + afterCreate: jest.fn(), + afterDestroy: jest.fn(), +})); + +jest.mock('../../services/resource', () => ({ + ...jest.requireActual('../../services/resource'), + calculateIsAutoDetectedForActivityReportObjective: jest.fn(), +})); + +describe('ActivityReportObjectiveResource', () => { + let objective; + let ar; + let aro; + let resource; + let aror; + + beforeAll(async () => { + jest.clearAllMocks(); + objective = await db.Objective.create({ + title: faker.lorem.words(10), + status: 'In Progress', + }); + ar = await createReport({ activityRecipients: [] }); + aro = await db.ActivityReportObjective.create({ + activityReportId: ar.id, objectiveId: objective.id, + }); + resource = await db.Resource.create({ url: `${faker.internet.url()}/activity-report-objective-resource.aspx` }); + + aror = await db.ActivityReportObjectiveResource.create({ + resourceId: resource.id, + activityReportObjectiveId: aro.id, + sourceFields: ['title'], + }, { individualHooks: true }); + }); + + afterAll(async () => { + jest.clearAllMocks(); + await db.ActivityReportObjectiveResource.destroy({ where: { id: aror.id } }); + await db.Resource.destroy({ where: { id: resource.id } }); + await db.ActivityReportObjective.destroy({ where: { id: aro.id } }); + await destroyReport(ar); + await db.Objective.destroy({ where: { id: objective.id }, force: true }); + await db.sequelize.close(); + }); + + it('calculates isAutoDetected', async () => { + expect(afterCreate).toHaveBeenCalled(); + expect(afterDestroy).not.toHaveBeenCalled(); + calculateIsAutoDetectedForActivityReportObjective.mockReturnValue(true); + await aror.reload({ include: [{ model: db.Resource, as: 'resource' }] }); + + expect(aror.isAutoDetected).toBe(true); + expect(calculateIsAutoDetectedForActivityReportObjective).toHaveBeenCalled(); + expect(aror.userProvidedUrl).toBe(resource.url); + + await aror.destroy({ individualHooks: true }); + + expect(afterDestroy).toHaveBeenCalled(); + }); +}); diff --git a/src/models/tests/collaboratorType.test.js b/src/models/tests/collaboratorType.test.js new file mode 100644 index 0000000000..11ac5a00f0 --- /dev/null +++ b/src/models/tests/collaboratorType.test.js @@ -0,0 +1,60 @@ +const db = require('..'); + +describe('CollaboratorType', () => { + let vf; + let ct; + let ct2; + beforeAll((async () => { + vf = await db.ValidFor.create({ + name: 'test', + }); + + ct = await db.CollaboratorType.create({ + name: 'test', + validForId: vf.id, + }); + + ct2 = await db.CollaboratorType.create({ + name: 'test2', + validForId: vf.id, + mapsTo: ct.id, + }); + })); + + it('should create a collaboratorType', async () => { + await ct.reload({ + include: [ + { + model: db.CollaboratorType, + as: 'mapsToCollaboratorType', + required: false, + }, + ], + }); + expect(ct).toBeDefined(); + expect(ct.name).toBe('test'); + expect(ct.latestName).toBe('test'); + expect(ct.latestId).toBe(ct.id); + + await ct2.reload({ + include: [ + { + model: db.CollaboratorType, + as: 'mapsToCollaboratorType', + required: false, + }, + ], + }); + + expect(ct2).toBeDefined(); + expect(ct2.name).toBe('test2'); + expect(ct2.latestName).toBe('test'); + expect(ct2.latestId).toBe(ct.id); + }); + + afterAll((async () => { + await db.ValidFor.destroy({ where: { id: vf.id } }); + await db.CollaboratorType.destroy({ where: { id: ct.id } }); + await db.sequelize.close(); + })); +}); diff --git a/src/models/tests/goalTemplateResource.test.js b/src/models/tests/goalTemplateResource.test.js new file mode 100644 index 0000000000..aa476a5454 --- /dev/null +++ b/src/models/tests/goalTemplateResource.test.js @@ -0,0 +1,45 @@ +import faker from '@faker-js/faker'; +import db from '..'; +import { calculateIsAutoDetectedForGoalTemplate } from '../../services/resource'; + +jest.mock('../../services/resource', () => ({ + ...jest.requireActual('../../services/resource'), + calculateIsAutoDetectedForGoalTemplate: jest.fn(), +})); + +const { + sequelize, GoalTemplate, Resource, GoalTemplateResource, +} = db; + +describe('GoalTemplateResource', () => { + let goalTemp; + let resource; + let goalTempResource; + + beforeAll(async () => { + resource = await Resource.create({ url: `${faker.internet.url()}/objective-resource.aspx` }); + goalTemp = await GoalTemplate.create({ hash: 'HASHHASHHASHHASHASHHASHASHASHAHS', templateName: 'HASHHASHHASHHASHASHHASHASHASHAHS' }); + + goalTempResource = await GoalTemplateResource.create({ + resourceId: resource.id, + goalTemplateId: goalTemp.id, + sourceFields: ['name'], + }); + }); + + afterAll(async () => { + jest.clearAllMocks(); + await goalTempResource.destroy(); + await goalTemp.destroy(); + await resource.destroy(); + await sequelize.close(); + }); + + it('calculates isAutoDetected & userProvidedUrl', async () => { + calculateIsAutoDetectedForGoalTemplate.mockReturnValue(true); + await goalTempResource.reload({ include: [{ model: Resource, as: 'resource' }] }); + + expect(goalTempResource.isAutoDetected).toBe(true); + expect(calculateIsAutoDetectedForGoalTemplate).toHaveBeenCalled(); + }); +}); diff --git a/src/models/tests/goals.test.js b/src/models/tests/goals.test.js index ba4a47a110..79d7a486f1 100644 --- a/src/models/tests/goals.test.js +++ b/src/models/tests/goals.test.js @@ -9,7 +9,7 @@ import { preventNameChangeWhenOnApprovedAR, autoPopulateStatusChangeDates, // propagateName, -} from '../hooks/goal'; +} from '../../hooks/goal'; import { GOAL_STATUS } from '../../constants'; function sleep(milliseconds) { diff --git a/src/models/tests/objectiveFile.test.js b/src/models/tests/objectiveFile.test.js new file mode 100644 index 0000000000..2ccd39f02a --- /dev/null +++ b/src/models/tests/objectiveFile.test.js @@ -0,0 +1,67 @@ +import faker from '@faker-js/faker'; +import db from '..'; +import { + beforeValidate, afterCreate, beforeDestroy, afterDestroy, +} from '../../hooks/objectiveFile'; +import { FILE_STATUSES } from '../../constants'; + +jest.mock('../../hooks/objectiveFile', () => ({ + beforeValidate: jest.fn(), + afterCreate: jest.fn(), + beforeDestroy: jest.fn(), + afterDestroy: jest.fn(), +})); + +const { + sequelize, Objective, File, ObjectiveFile, +} = db; + +describe('objectiveFile', () => { + let file; + let objective; + + beforeAll(async () => { + file = await File.create({ + originalFileName: faker.system.fileName(), + key: faker.system.fileName(), + status: FILE_STATUSES.UPLOADED, + fileSize: 1234, + }); + objective = await Objective.create({ + name: `Test Objective ${faker.random.words(10)}`, + status: 'In Progress', + }); + + await ObjectiveFile.create({ + fileId: file.id, + objectiveId: objective.id, + }, { individualHooks: true }); + }); + + afterAll(async () => { + jest.clearAllMocks(); + await ObjectiveFile.destroy({ + where: { + objectiveId: objective.id, + }, + }); + await Objective.destroy({ where: { id: objective.id }, force: true }); + await File.destroy({ where: { id: file.id }, force: true }); + await sequelize.close(); + }); + + it('calls hooks', async () => { + expect(beforeValidate).toHaveBeenCalledTimes(1); + expect(afterCreate).toHaveBeenCalledTimes(1); + + await ObjectiveFile.destroy({ + where: { + objectiveId: objective.id, + }, + individualHooks: true, + }); + + expect(beforeDestroy).toHaveBeenCalledTimes(1); + expect(afterDestroy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/models/tests/objectiveResource.test.js b/src/models/tests/objectiveResource.test.js new file mode 100644 index 0000000000..43a9a5a45c --- /dev/null +++ b/src/models/tests/objectiveResource.test.js @@ -0,0 +1,42 @@ +import faker from '@faker-js/faker'; +import db from '..'; +import { calculateIsAutoDetectedForObjectiveTemplate } from '../../services/resource'; + +jest.mock('../../services/resource', () => ({ + ...jest.requireActual('../../services/resource'), + calculateIsAutoDetectedForObjectiveTemplate: jest.fn(), +})); + +describe('ActivityReportObjectiveResource', () => { + let objectiveTemp; + let resource; + let objectiveTempResource; + + beforeAll(async () => { + resource = await db.Resource.create({ url: `${faker.internet.url()}/objective-resource.aspx` }); + objectiveTemp = await db.ObjectiveTemplate.create({ hash: 'HASHHASHHASHHASHASHHASHASHASHAHS', templateTitle: 'HASHHASHHASHHASHASHHASHASHASHAHS' }); + + objectiveTempResource = await db.ObjectiveTemplateResource.create({ + resourceId: resource.id, + objectiveTemplateId: objectiveTemp.id, + sourceFields: ['title'], + }); + }); + + afterAll(async () => { + jest.clearAllMocks(); + await objectiveTempResource.destroy(); + await objectiveTemp.destroy(); + await resource.destroy(); + await db.sequelize.close(); + }); + + it('calculates isAutoDetected & userProvidedUrl', async () => { + calculateIsAutoDetectedForObjectiveTemplate.mockReturnValue(true); + await objectiveTempResource.reload({ include: [{ model: db.Resource, as: 'resource' }] }); + + expect(objectiveTempResource.isAutoDetected).toBe(true); + expect(calculateIsAutoDetectedForObjectiveTemplate).toHaveBeenCalled(); + expect(objectiveTempResource.userProvidedUrl).toBe(resource.url); + }); +}); diff --git a/src/models/tests/objectiveTopic.test.js b/src/models/tests/objectiveTopic.test.js new file mode 100644 index 0000000000..607fd675b0 --- /dev/null +++ b/src/models/tests/objectiveTopic.test.js @@ -0,0 +1,53 @@ +import faker from '@faker-js/faker'; +import db from '..'; +import { beforeValidate, afterCreate, afterDestroy } from '../../hooks/objectiveTopic'; + +jest.mock('../../hooks/objectiveTopic', () => ({ + beforeValidate: jest.fn(), + afterCreate: jest.fn(), + afterDestroy: jest.fn(), +})); + +describe('objectiveTopic', () => { + let topic; + let objective; + + beforeAll(async () => { + topic = await db.Topic.create({ name: `something gross ${faker.random.words(10)}` }); + objective = await db.Objective.create({ + name: `Test Objective ${faker.random.words(10)}`, + status: 'In Progress', + }); + + await db.ObjectiveTopic.create({ + topicId: topic.id, + objectiveId: objective.id, + }, { individualHooks: true }); + }); + + afterAll(async () => { + jest.clearAllMocks(); + await db.ObjectiveTopic.destroy({ + where: { + objectiveId: objective.id, + }, + }); + await db.Objective.destroy({ where: { id: objective.id }, force: true }); + await db.Topic.destroy({ where: { id: topic.id }, force: true }); + await db.sequelize.close(); + }); + + it('calls hooks', async () => { + expect(beforeValidate).toHaveBeenCalledTimes(1); + expect(afterCreate).toHaveBeenCalledTimes(1); + + await db.ObjectiveTopic.destroy({ + where: { + objectiveId: objective.id, + }, + individualHooks: true, + }); + + expect(afterDestroy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/models/tests/validFor.test.js b/src/models/tests/validFor.test.js new file mode 100644 index 0000000000..a31fb89df2 --- /dev/null +++ b/src/models/tests/validFor.test.js @@ -0,0 +1,33 @@ +const db = require('..'); + +describe('ValidFor', () => { + let vf; + let vf2; + beforeAll((async () => { + vf = await db.ValidFor.create({ + isReport: true, + name: 'test', + }); + vf2 = await db.ValidFor.create({ + isReport: false, + name: 'test2', + }); + })); + + it('should create a validFor', async () => { + expect(vf).toBeDefined(); + expect(vf.isReport).toBe(true); + expect(vf.latestName).toBe('test'); + expect(vf.latestId).toBe(vf.id); + + expect(vf2).toBeDefined(); + expect(vf2.isReport).toBe(false); + expect(vf2.latestName).toBe('test2'); + }); + + afterAll((async () => { + await db.ValidFor.destroy({ where: { id: vf.id } }); + await db.ValidFor.destroy({ where: { id: vf2.id } }); + await db.sequelize.close(); + })); +}); diff --git a/src/models/validFor.js b/src/models/validFor.js index 560e46a290..44e04fd57a 100644 --- a/src/models/validFor.js +++ b/src/models/validFor.js @@ -1,8 +1,4 @@ -const { - Model, - Op, -} = require('sequelize'); -const { ENTITY_TYPE } = require('../constants'); +const { Model } = require('sequelize'); /** * @param {} sequelize diff --git a/src/routes/admin/legacyReports.test.js b/src/routes/admin/legacyReports.test.js index b9a87b4a1c..9894db34e6 100644 --- a/src/routes/admin/legacyReports.test.js +++ b/src/routes/admin/legacyReports.test.js @@ -3,27 +3,35 @@ import db, { ActivityReportApprover, ActivityReportCollaborator, User, + Region, } from '../../models'; import { - createReport, createGrant, createUser, destroyReport, + createReport, + createGrant, + createUser, + destroyReport, + createRegion, } from '../../testUtils'; import { updateLegacyReportUsers } from './legacyReports'; describe('LegacyReports, admin routes', () => { describe('updateLegacyReportUsers', () => { + let region; let grant; let report; let user; let userTwo; beforeAll(async () => { - user = await createUser(); - userTwo = await createUser(); - grant = await createGrant(); + region = await createRegion(); + user = await createUser({ homeRegionId: region.id }); + userTwo = await createUser({ homeRegionId: region.id }); + grant = await createGrant({ regionId: region.id }); report = await createReport({ userId: user.id, activityRecipients: [{ grantId: grant.id }], imported: {}, + regionId: region.id, }); await report.update({ userId: null }); @@ -44,6 +52,7 @@ describe('LegacyReports, admin routes', () => { await destroyReport(report); await Grant.destroy({ where: { id: grant.id }, individualHooks: true }); await User.destroy({ where: { id: [user.id, userTwo.id] } }); + await Region.destroy({ where: { id: region.id } }); await db.sequelize.close(); }); diff --git a/src/services/event.test.js b/src/services/event.test.js index f97edf77c1..f2930cb073 100644 --- a/src/services/event.test.js +++ b/src/services/event.test.js @@ -19,9 +19,25 @@ import { } from './event'; describe('event service', () => { + let newOwner; + + beforeAll(async () => { + newOwner = await db.User.create({ + homeRegionId: 1, + name: 'New Owner', + hsesUsername: 'DF431423', + hsesUserId: 'DF431423', + email: 'newowner@test.com', + role: [], + lastLogin: new Date(), + }); + }); + afterAll(async () => { + await db.User.destroy({ where: { id: newOwner.id } }); await db.sequelize.close(); }); + const createAnEvent = async (num) => createEvent({ ownerId: num, regionId: num, @@ -83,15 +99,6 @@ describe('event service', () => { it('update owner json', async () => { const created = await createAnEvent(99_927); - const newOwner = await db.User.create({ - homeRegionId: 1, - name: 'New Owner', - hsesUsername: 'DF431423', - hsesUserId: 'DF431423', - email: 'newowner@test.com', - role: [], - lastLogin: new Date(), - }); const updated = await updateEvent(created.id, { ownerId: newOwner.id, @@ -106,7 +113,6 @@ describe('event service', () => { expect(updated.data.owner).toHaveProperty('email', 'newowner@test.com'); await destroyEvent(created.id); - await db.User.destroy({ where: { id: newOwner.id } }); }); it('creates a new event when the id cannot be found', async () => { const found = await findEventByDbId(99_999); diff --git a/src/services/nationalCenters.test.js b/src/services/nationalCenters.test.js index b595b9a35d..4cb6e16de9 100644 --- a/src/services/nationalCenters.test.js +++ b/src/services/nationalCenters.test.js @@ -10,7 +10,7 @@ import { import { auditLogger } from '../logger'; jest.spyOn(auditLogger, 'info'); -jest.mock('../models/hooks/sessionReportPilot'); +jest.mock('../hooks/sessionReportPilot'); describe('nationalCenters service', () => { afterAll(() => { diff --git a/src/testUtils.js b/src/testUtils.js index 79fd496c35..69862e0b82 100644 --- a/src/testUtils.js +++ b/src/testUtils.js @@ -239,7 +239,7 @@ export async function destroyReport(report) { } } -export async function createGoal(goal) { +export async function createGoal(goal, options = {}) { let grant = await Grant.findByPk(goal.grantId); if (!grant) { @@ -251,13 +251,14 @@ export async function createGoal(goal) { : (await GoalTemplate.findOrCreate({ where: { templateName: dg.name }, defaults: { templateName: dg.name }, + ...options, }))[0]; const dbGoal = await Goal.create({ ...dg, ...goal, grantId: grant.id, goalTemplateId: dbGoalTemplate.id, - }); + }, options); return dbGoal; } @@ -354,12 +355,23 @@ export async function createTrainingReport(report) { collaboratorIds, pocIds, ownerId, + regionId, data, } = report; + let region; + + if (regionId) { + region = await Region.findByPk(regionId); + } + + if (!region) { + region = await createRegion(); + } + let userCreator = await User.findByPk(ownerId); if (!userCreator) { - userCreator = await createUser(); + userCreator = await createUser({ homeRegionId: region.id }); } const userCollaborators = await Promise.all(collaboratorIds.map(async (id) => { @@ -373,7 +385,7 @@ export async function createTrainingReport(report) { const userPocs = await Promise.all(pocIds.map(async (id) => { let user = await User.findByPk(id); if (!user) { - user = await createUser(); + user = await createUser({ homeRegionId: region.id }); } return user.id; })); @@ -382,7 +394,7 @@ export async function createTrainingReport(report) { data: mockTrainingReportData(data || {}), collaboratorIds: userCollaborators, ownerId: userCreator.id, - regionId: userCreator.homeRegionId, + regionId: region.id, imported: {}, pocIds: userPocs, });