diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index 32ab42b20..a4edec5af 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -9,15 +9,14 @@ const CveIdRepository = require('./cveIdRepository') const uuid = require('uuid') const _ = require('lodash') const BaseOrg = require('../model/baseorg') -const ConversationRepository = require('./conversationRepository') const getConstants = require('../constants').getConstants - -const skipNulls = (objValue, srcValue) => { - if (_.isArray(objValue)) { - return srcValue - } - return undefined -} +const { + handleShortNameUpdate, + automateProgramDataDates, + processJointApprovalAndMerge, + createAuditLogEntry, + handleAuthorityModelChange +} = require('./baseOrgRepositoryHelpers') /** * @function setAggregateOrgObj @@ -822,7 +821,6 @@ class BaseOrgRepository extends BaseRepository { return changedFields } - /** /** * @async * @function updateOrgFull @@ -839,22 +837,25 @@ class BaseOrgRepository extends BaseRepository { * @returns {Promise} A promise that resolves to a plain JavaScript object representing the updated organization, stripped of internal properties and empty values. */ async updateOrgFull (shortName, incomingOrg, options = {}, isLegacyObject = false, requestingUserUUID = null, isAdmin = false, isSecretariat = false) { - // TODO: Fix these imports, remove the circular imports const { deepRemoveEmpty } = require('../utils/utils') const OrgRepository = require('./orgRepository') const ReviewObjectRepository = require('./reviewObjectRepository') const BaseUserRepository = require('./baseUserRepository') + const ConversationRepository = require('./conversationRepository') const legacyOrgRepo = new OrgRepository() const reviewObjectRepo = new ReviewObjectRepository() const userRepo = new BaseUserRepository() const conversationRepo = new ConversationRepository() + const legacyOrg = await legacyOrgRepo.findOneByShortName(shortName, options) const registryOrg = await this.findOneByShortName(shortName, options) const originalRegistryOrgObject = registryOrg.toObject() - // check to see if there is a PENDING review object: + const originalRoles = registryOrg.authority + const reviewObject = await reviewObjectRepo.getOrgReviewObjectByOrgShortname(shortName, isSecretariat, options) const { conversation, ...incomingOrgBody } = incomingOrg + let legacyObjectRaw let registryObjectRaw @@ -866,175 +867,27 @@ class BaseOrgRepository extends BaseRepository { legacyObjectRaw = this.convertRegistryToLegacy(incomingOrgBody) } - if (incomingOrg?.new_short_name) { - const newName = incomingOrg.new_short_name - - // 1. Update the Mongoose instances - registryOrg.short_name = newName - legacyOrg.short_name = newName - - // 2. Update the raw tracking objects so lodash.merge doesn't restore the old short_name - registryObjectRaw.short_name = newName - legacyObjectRaw.short_name = newName - - // 3. Remove new_short_name from the raw objects so it doesn't merge into the DB - delete registryObjectRaw.new_short_name - delete legacyObjectRaw.new_short_name - delete incomingOrg.new_short_name // Keeping for existing logic - } + handleShortNameUpdate(incomingOrg, registryOrg, legacyOrg, registryObjectRaw, legacyObjectRaw) + automateProgramDataDates(registryObjectRaw, registryOrg) - // Automate program_data dates based on status - if (registryObjectRaw.program_data && registryObjectRaw.program_data.status) { - const incomingStatus = registryObjectRaw.program_data.status - const currentStatus = registryOrg.program_data?.status || 'inactive' - if (incomingStatus !== currentStatus) { - if (incomingStatus === 'active') { - registryObjectRaw.program_data.partner_active_date = new Date().toISOString().split('T')[0] - if (registryObjectRaw.program_data.partner_inactive_date !== undefined) { - delete registryObjectRaw.program_data.partner_inactive_date - } - } else if (incomingStatus === 'inactive') { - registryObjectRaw.program_data.partner_inactive_date = new Date().toISOString().split('T')[0] - if (registryObjectRaw.program_data.partner_active_date !== undefined) { - delete registryObjectRaw.program_data.partner_active_date - } - } - } else { - // Keep existing dates if status didn't change - if (registryOrg.program_data?.partner_active_date) { - registryObjectRaw.program_data.partner_active_date = registryOrg.program_data.partner_active_date - } - if (registryOrg.program_data?.partner_inactive_date) { - registryObjectRaw.program_data.partner_inactive_date = registryOrg.program_data.partner_inactive_date - } - } - } + const requestingUser = requestingUserUUID ? await userRepo.findUserByUUID(requestingUserUUID, options) : null + const requestingUsername = requestingUser ? requestingUser.username : null - // Checking for joint approval fields const jointApprovalFieldsRegistry = this.getJointApprovalFields(registryOrg, registryObjectRaw) const jointApprovalFieldsLegacy = this.getJointApprovalFields(legacyOrg, legacyObjectRaw, true) - let updatedRegistryOrg = null - let updatedLegacyOrg = null - let jointApprovalRegistry = null - // If there are no joint approval fields, merge the original and updated objects. Otherwise, update the registry object and legacy object separately considering joint approval. - // Dealing with roles requires a bit of extra control. - const originalRoles = registryOrg.authority - - const requestingUser = requestingUserUUID ? await userRepo.findUserByUUID(requestingUserUUID, options) : null - const requestingUsername = requestingUser ? requestingUser.username : null - - const protectedFields = ['_id', 'UUID', '__v', '__t', 'created', 'last_updated', 'createdAt', 'updatedAt', 'users', 'admins'] - let registryProtectedFields = [...protectedFields] - if (!isSecretariat) { - registryProtectedFields = [...registryProtectedFields, ...getConstants().ORG_RESTRICTED_FIELDS] - } + let { updatedRegistryOrg, updatedLegacyOrg } = await processJointApprovalAndMerge( + registryOrg, legacyOrg, registryObjectRaw, legacyObjectRaw, reviewObject, isSecretariat, options, requestingUsername, jointApprovalFieldsRegistry, jointApprovalFieldsLegacy + ) - if (isSecretariat || _.isEmpty(jointApprovalFieldsRegistry)) { - updatedLegacyOrg = legacyOrg.overwrite(_.mergeWith(_.pick(legacyOrg.toObject(), protectedFields), _.omit(legacyObjectRaw, protectedFields), skipNulls)) - updatedRegistryOrg = registryOrg.overwrite(_.mergeWith(_.pick(registryOrg.toObject(), registryProtectedFields), _.omit(registryObjectRaw, registryProtectedFields), skipNulls)) - } else { - // Check if there are actual changes to joint approval fields compared to current org object (not current review) - // Only compare fields that are actually in the incoming data - const incomingJointApprovalKeys = Object.keys(_.pick(registryObjectRaw, jointApprovalFieldsRegistry)) - const currentJointApprovalData = JSON.parse(JSON.stringify(_.pick(registryOrg.toObject(), incomingJointApprovalKeys))) - const incomingJointApprovalData = JSON.parse(JSON.stringify(_.pick(registryObjectRaw, incomingJointApprovalKeys))) - const hasJointApprovalChanges = !_.isEqual(currentJointApprovalData, incomingJointApprovalData) - - if (hasJointApprovalChanges) { - // write the joint approval to the database - jointApprovalRegistry = _.mergeWith({}, registryOrg.toObject(), registryObjectRaw, skipNulls) - if (reviewObject) { - await reviewObjectRepo.updateReviewOrgObject(jointApprovalRegistry, reviewObject.uuid, options) - } else { - await reviewObjectRepo.createReviewOrgObject(jointApprovalRegistry, requestingUsername, options) - } - } else { - // If no changes between org and new object but a review object exists, remove it since joint approval is no longer needed - if (reviewObject) { - await reviewObjectRepo.rejectReviewOrgObject(reviewObject.uuid, requestingUsername, options) - } - } - updatedRegistryOrg = registryOrg.overwrite(_.mergeWith(_.pick(registryOrg.toObject(), [...registryProtectedFields, ...jointApprovalFieldsRegistry]), _.omit(registryObjectRaw, [...registryProtectedFields, ...jointApprovalFieldsRegistry]), skipNulls)) - updatedLegacyOrg = legacyOrg.overwrite(_.mergeWith(_.pick(legacyOrg.toObject(), [...protectedFields, ...jointApprovalFieldsLegacy]), _.omit(legacyObjectRaw, [...protectedFields, ...jointApprovalFieldsLegacy]), skipNulls)) - } - // handle conversation const conversationArray = [] if (conversation) { conversationArray.push(await conversationRepo.createConversation(registryOrg.UUID, conversation, requestingUser, isSecretariat, options)) } - // ADD AUDIT ENTRY AUTOMATICALLY for the registry object before it gets saved. - if (requestingUserUUID) { - try { - const AuditRepository = require('./auditRepository') - const auditRepo = new AuditRepository() - // Seed the audit history with the existing org data if an audit document doesn't already exist. - // This is necessary because older entities might not have an audit log yet, and we want - // the first entry to be their baseline state before this update. - await auditRepo.seedAuditHistoryForOrg( - registryOrg.UUID, - originalRegistryOrgObject, - requestingUserUUID, - { ...options, upsert: true } - ) - // Get the org state before save for comparison - const beforeUpdateObject = originalRegistryOrgObject - const afterUpdateObject = registryOrg.toObject() - - // Clean objects for comparison (remove Mongoose metadata) - const cleanBefore = _.omit(beforeUpdateObject, ['_id', '__v', '__t', 'createdAt', 'updatedAt']) - const cleanAfter = _.omit(afterUpdateObject, ['_id', '__v', '__t', 'createdAt', 'updatedAt']) - - // Only add audit entry if there are changes - if (!_.isEqual(cleanBefore, cleanAfter)) { - await auditRepo.appendToAuditHistoryForOrg( - registryOrg.UUID, - registryOrg.toObject(), - requestingUserUUID, - { ...options, upsert: true } - ) - } - console.log('Audit entry created for registry object') - } catch (auditError) { - console.error('Audit entry creation failed:', auditError) - } - } - - // Handle possible authority (discriminator) changes that require a different Mongoose model - let roleChange = false - if (!_.isEqual([...originalRoles].sort(), [...updatedRegistryOrg?.authority].sort())) { - roleChange = true - } - - // Determine the correct model based on the updated authority - let TargetModel = null - if (updatedRegistryOrg.authority?.includes('SECRETARIAT')) { - TargetModel = SecretariatOrgModel - } else if (updatedRegistryOrg.authority?.includes('CNA')) { - TargetModel = CNAOrgModel - } else if (updatedRegistryOrg.authority?.includes('ADP')) { - TargetModel = ADPOrgModel - } else if (updatedRegistryOrg.authority?.includes('BULK_DOWNLOAD')) { - TargetModel = BulkDownloadModel - } else if (updatedRegistryOrg.authority?.includes('ROOT')) { - TargetModel = RootOrgModel - } + await createAuditLogEntry(registryOrg, originalRegistryOrgObject, requestingUserUUID, options) - // If the model type has changed, replace the document with a new one of the correct type - if (TargetModel && roleChange) { - const oldId = updatedRegistryOrg._id - // Remove the old document - await BaseOrgModel.deleteOne({ _id: oldId }, options) - // Prepare data for the new document, preserving the UUID and _id - const newDocData = updatedRegistryOrg.toObject() - delete newDocData.__t - newDocData._id = oldId - const newDoc = new TargetModel(newDocData) - await newDoc.save(options) - // Update reference so subsequent code works with the newly saved document - updatedRegistryOrg = newDoc - } + updatedRegistryOrg = await handleAuthorityModelChange(updatedRegistryOrg, originalRoles, options) try { await updatedLegacyOrg.save(options) diff --git a/src/repositories/baseOrgRepositoryHelpers.js b/src/repositories/baseOrgRepositoryHelpers.js new file mode 100644 index 000000000..0bac9a580 --- /dev/null +++ b/src/repositories/baseOrgRepositoryHelpers.js @@ -0,0 +1,280 @@ +const _ = require('lodash') +const getConstants = require('../constants').getConstants +const BaseOrgModel = require('../model/baseorg') +const CNAOrgModel = require('../model/cnaorg') +const ADPOrgModel = require('../model/adporg') +const BulkDownloadModel = require('../model/bulkdownloadorg') +const SecretariatOrgModel = require('../model/secretariatorg') +const RootOrgModel = require('../model/rootorg') + +/** + * @function skipNulls + * @description A custom customizer for lodash's mergeWith. If the target value is an array, it replaces it completely with the source value instead of merging elements. + * @param {*} objValue - The value from the target object. + * @param {*} srcValue - The value from the source object. + * @returns {*} The source array if the target was an array, otherwise undefined to let mergeWith handle it natively. + */ +const skipNulls = (objValue, srcValue) => { + if (_.isArray(objValue)) { + return srcValue + } + return undefined +} + +/** + * @function handleShortNameUpdate + * @description Manages the complex process of updating an organization's short name, ensuring all instances and references (legacy and registry) are synchronized with the new short name. + * @param {object} incomingOrg - The payload containing the requested updates. + * @param {object} registryOrg - The current registry organization Mongoose document. + * @param {object} legacyOrg - The current legacy organization Mongoose document. + * @param {object} registryObjectRaw - The raw data payload mapped for the registry schema. + * @param {object} legacyObjectRaw - The raw data payload mapped for the legacy schema. + */ +function handleShortNameUpdate (incomingOrg, registryOrg, legacyOrg, registryObjectRaw, legacyObjectRaw) { + if (incomingOrg?.new_short_name) { + const newName = incomingOrg.new_short_name + registryOrg.short_name = newName + legacyOrg.short_name = newName + registryObjectRaw.short_name = newName + legacyObjectRaw.short_name = newName + delete registryObjectRaw.new_short_name + delete legacyObjectRaw.new_short_name + delete incomingOrg.new_short_name + } +} + +/** + * @function automateProgramDataDates + * @description Automatically sets or removes `partner_active_date` and `partner_inactive_date` based on state transitions in the organization's program_data status. + * @param {object} registryObjectRaw - The raw data payload mapped for the registry schema. + * @param {object} registryOrg - The current registry organization Mongoose document. + */ +function automateProgramDataDates (registryObjectRaw, registryOrg) { + if (registryObjectRaw.program_data && registryObjectRaw.program_data.status) { + const incomingStatus = registryObjectRaw.program_data.status + const currentStatus = registryOrg.program_data?.status || 'inactive' + if (incomingStatus !== currentStatus) { + if (incomingStatus === 'active') { + registryObjectRaw.program_data.partner_active_date = new Date().toISOString().split('T')[0] + if (registryObjectRaw.program_data.partner_inactive_date !== undefined) { + delete registryObjectRaw.program_data.partner_inactive_date + } + } else if (incomingStatus === 'inactive') { + registryObjectRaw.program_data.partner_inactive_date = new Date().toISOString().split('T')[0] + if (registryObjectRaw.program_data.partner_active_date !== undefined) { + delete registryObjectRaw.program_data.partner_active_date + } + } + } else { + if (registryOrg.program_data?.partner_active_date) { + registryObjectRaw.program_data.partner_active_date = registryOrg.program_data.partner_active_date + } + if (registryOrg.program_data?.partner_inactive_date) { + registryObjectRaw.program_data.partner_inactive_date = registryOrg.program_data.partner_inactive_date + } + } + } +} + +/** + * @function mergeAllowedFields + * @description Deep merges new fields into a Mongoose document while strictly protecting explicitly defined system fields from being overwritten. + * @param {object} targetDoc - The Mongoose document being updated. + * @param {object} rawData - The raw incoming data payload. + * @param {string[]} fieldsToProtect - Array of field keys that cannot be overwritten by the rawData. + * @returns {object} The mutated Mongoose document with the merged data. + */ +function mergeAllowedFields (targetDoc, rawData, fieldsToProtect) { + return targetDoc.overwrite( + _.mergeWith( + _.pick(targetDoc.toObject(), fieldsToProtect), + _.omit(rawData, fieldsToProtect), + skipNulls + ) + ) +} + +/** + * @async + * @function manageReviewObject + * @description Manages the lifecycle of an organization Review Object for changes that require Secretariat approval. Evaluates if restricted fields changed and will create, update, or reject the pending review object accordingly. + * @param {object} registryOrg - The current registry organization Mongoose document. + * @param {object} registryObjectRaw - The raw incoming data payload. + * @param {string[]} jointApprovalFieldsRegistry - List of field paths that require joint approval. + * @param {object} reviewObject - An existing review object, or null if one does not exist. + * @param {string} requestingUsername - Username of the user making the request. + * @param {object} options - Mongoose options for database queries. + */ +async function manageReviewObject (registryOrg, registryObjectRaw, jointApprovalFieldsRegistry, reviewObject, requestingUsername, options) { + const ReviewObjectRepository = require('./reviewObjectRepository') + const reviewObjectRepo = new ReviewObjectRepository() + + const incomingJointApprovalKeys = Object.keys(_.pick(registryObjectRaw, jointApprovalFieldsRegistry)) + const currentJointApprovalData = _.pick(registryOrg.toObject(), incomingJointApprovalKeys) + const incomingJointApprovalData = _.pick(registryObjectRaw, incomingJointApprovalKeys) + + // Normalize dates before comparison + Object.keys(currentJointApprovalData).forEach(key => { + if (currentJointApprovalData[key] instanceof Date) { + currentJointApprovalData[key] = currentJointApprovalData[key].toISOString() + } + }) + Object.keys(incomingJointApprovalData).forEach(key => { + if (incomingJointApprovalData[key] instanceof Date) { + incomingJointApprovalData[key] = incomingJointApprovalData[key].toISOString() + } + }) + + const hasJointApprovalChanges = !_.isEqual(currentJointApprovalData, incomingJointApprovalData) + + if (hasJointApprovalChanges) { + const jointApprovalRegistry = _.mergeWith({}, registryOrg.toObject(), registryObjectRaw, skipNulls) + if (reviewObject) { + await reviewObjectRepo.updateReviewOrgObject(jointApprovalRegistry, reviewObject.uuid, options) + } else { + await reviewObjectRepo.createReviewOrgObject(jointApprovalRegistry, requestingUsername, options) + } + } else { + if (reviewObject) { + await reviewObjectRepo.rejectReviewOrgObject(reviewObject.uuid, requestingUsername, options) + } + } +} + +/** + * @async + * @function processJointApprovalAndMerge + * @description Orchestrates the merging of new data into both legacy and registry documents. If the user is a standard user modifying restricted fields, it defers those changes by triggering a review object. + * @param {object} registryOrg - The current registry organization Mongoose document. + * @param {object} legacyOrg - The current legacy organization Mongoose document. + * @param {object} registryObjectRaw - The raw incoming data payload for the registry schema. + * @param {object} legacyObjectRaw - The raw incoming data payload for the legacy schema. + * @param {object} reviewObject - An existing review object for the organization. + * @param {boolean} isSecretariat - Whether the requester has Secretariat privileges. + * @param {object} options - Mongoose options for database queries. + * @param {string} requestingUsername - Username of the user making the request. + * @param {string[]} jointApprovalFieldsRegistry - Restricted fields that were changed in the registry object. + * @param {string[]} jointApprovalFieldsLegacy - Restricted fields that were changed in the legacy object. + * @returns {Promise} An object containing the securely updated `updatedRegistryOrg` and `updatedLegacyOrg` documents. + */ +async function processJointApprovalAndMerge (registryOrg, legacyOrg, registryObjectRaw, legacyObjectRaw, reviewObject, isSecretariat, options, requestingUsername, jointApprovalFieldsRegistry, jointApprovalFieldsLegacy) { + const protectedFields = ['_id', 'UUID', '__v', '__t', 'created', 'last_updated', 'createdAt', 'updatedAt', 'users', 'admins', 'inUse', 'in_use'] + let registryProtectedFields = [...protectedFields] + if (!isSecretariat) { + registryProtectedFields = [...registryProtectedFields, ...getConstants().ORG_RESTRICTED_FIELDS] + } + + let updatedRegistryOrg = null + let updatedLegacyOrg = null + + if (isSecretariat || _.isEmpty(jointApprovalFieldsRegistry)) { + updatedLegacyOrg = mergeAllowedFields(legacyOrg, legacyObjectRaw, protectedFields) + updatedRegistryOrg = mergeAllowedFields(registryOrg, registryObjectRaw, registryProtectedFields) + } else { + await manageReviewObject(registryOrg, registryObjectRaw, jointApprovalFieldsRegistry, reviewObject, requestingUsername, options) + + const restrictedRegistryFields = [...registryProtectedFields, ...jointApprovalFieldsRegistry] + const restrictedLegacyFields = [...protectedFields, ...jointApprovalFieldsLegacy] + + updatedRegistryOrg = mergeAllowedFields(registryOrg, registryObjectRaw, restrictedRegistryFields) + updatedLegacyOrg = mergeAllowedFields(legacyOrg, legacyObjectRaw, restrictedLegacyFields) + } + + return { + updatedRegistryOrg, + updatedLegacyOrg + } +} + +/** + * @async + * @function createAuditLogEntry + * @description Creates an audit log tracing changes to an organization. It seeds the history if none exists, and appends the new change if differences are detected. + * @param {object} registryOrg - The updated registry organization Mongoose document. + * @param {object} originalRegistryOrgObject - A plain javascript object representing the organization before updates were applied. + * @param {string} requestingUserUUID - The UUID of the user who made the request. + * @param {object} options - Mongoose options for database queries. + */ +async function createAuditLogEntry (registryOrg, originalRegistryOrgObject, requestingUserUUID, options) { + if (!requestingUserUUID) return + try { + const AuditRepository = require('./auditRepository') + const auditRepo = new AuditRepository() + await auditRepo.seedAuditHistoryForOrg( + registryOrg.UUID, + originalRegistryOrgObject, + requestingUserUUID, + { ...options, upsert: true } + ) + const beforeUpdateObject = originalRegistryOrgObject + const afterUpdateObject = registryOrg.toObject() + + const cleanBefore = _.omit(beforeUpdateObject, ['_id', '__v', '__t', 'createdAt', 'updatedAt']) + const cleanAfter = _.omit(afterUpdateObject, ['_id', '__v', '__t', 'createdAt', 'updatedAt']) + + if (!_.isEqual(cleanBefore, cleanAfter)) { + await auditRepo.appendToAuditHistoryForOrg( + registryOrg.UUID, + registryOrg.toObject(), + requestingUserUUID, + { ...options, upsert: true } + ) + } + console.log('Audit entry created for registry object') + } catch (auditError) { + console.error('Audit entry creation failed:', auditError) + } +} + +/** + * @async + * @function handleAuthorityModelChange + * @description Detects if the organization's roles (authority) have changed and gracefully re-casts the underlying Mongoose document to the appropriate discriminator type (e.g., CNAOrg, ADPOrg, SecretariatOrg) to match the new roles. + * @param {object} updatedRegistryOrg - The updated organization Mongoose document. + * @param {string[]} originalRoles - The array of roles the organization had prior to the update. + * @param {object} options - Mongoose options for database queries. + * @returns {Promise} The organization document, cast to the correct Mongoose discriminator model if a change occurred. + */ +async function handleAuthorityModelChange (updatedRegistryOrg, originalRoles, options) { + let roleChange = false + if (!_.isEqual([...originalRoles].sort(), [...updatedRegistryOrg?.authority].sort())) { + roleChange = true + } + + let TargetModel = null + if (updatedRegistryOrg.authority?.includes('SECRETARIAT')) { + TargetModel = SecretariatOrgModel + } else if (updatedRegistryOrg.authority?.includes('CNA')) { + TargetModel = CNAOrgModel + } else if (updatedRegistryOrg.authority?.includes('ADP')) { + TargetModel = ADPOrgModel + } else if (updatedRegistryOrg.authority?.includes('BULK_DOWNLOAD')) { + TargetModel = BulkDownloadModel + } else if (updatedRegistryOrg.authority?.includes('ROOT')) { + TargetModel = RootOrgModel + } + + if (TargetModel && roleChange) { + const oldId = updatedRegistryOrg._id + await BaseOrgModel.deleteOne({ _id: oldId }, options) + const newDocData = updatedRegistryOrg.toObject() + delete newDocData.__t + newDocData._id = oldId + const newDoc = new TargetModel(newDocData) + await newDoc.save(options) + updatedRegistryOrg = newDoc + } + + return updatedRegistryOrg +} + +module.exports = { + skipNulls, + handleShortNameUpdate, + automateProgramDataDates, + mergeAllowedFields, + manageReviewObject, + processJointApprovalAndMerge, + createAuditLogEntry, + handleAuthorityModelChange +} diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index 2c49f3a63..60886b86f 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -400,6 +400,48 @@ describe('Testing /registryOrg endpoints', () => { .delete(`/api/registryOrg/${subOrg.short_name}`) .set(secretariatHeaders) }) + it('Preserves inUse and in_use properties across updates', async () => { + // Create an organization + const tempOrg = { + short_name: 'temp_org_for_in_use_test', + long_name: 'Temp Org In Use Test', + authority: ['CNA'], + hard_quota: 10 + } + await chai.request(app) + .post('/api/registry/org') + .set(secretariatHeaders) + .send(tempOrg) + + // Set the inUse and in_use flags via mongo directly + const Org = require('../../../src/model/org') + const BaseOrg = require('../../../src/model/baseorg') + await Org.findOneAndUpdate({ short_name: tempOrg.short_name }, { $set: { inUse: true } }) + await BaseOrg.findOneAndUpdate({ short_name: tempOrg.short_name }, { $set: { in_use: true } }) + + // Update the org using the API + await chai.request(app) + .put(`/api/registry/org/${tempOrg.short_name}`) + .set(secretariatHeaders) + .send({ + ...tempOrg, + long_name: 'Temp Org In Use Test Updated' + }) + .then((res) => { + expect(res).to.have.status(200) + }) + + // Verify the inUse flags are preserved + const legacyOrgCheck = await Org.findOne({ short_name: tempOrg.short_name }) + const registryOrgCheck = await BaseOrg.findOne({ short_name: tempOrg.short_name }) + expect(legacyOrgCheck.inUse).to.be.true + expect(registryOrgCheck.in_use).to.be.true + + // Cleanup + await chai.request(app) + .delete(`/api/registryOrg/${tempOrg.short_name}`) + .set(secretariatHeaders) + }) }) context('Negative Tests', () => { it('Fails to update a registry organization that does not exist', async () => {