From 94ad3a0e0541d3424ddea31532bc83e1522d9f88 Mon Sep 17 00:00:00 2001 From: emathew Date: Wed, 12 Nov 2025 14:14:05 -0500 Subject: [PATCH 1/4] auditChanges --- .../audit.controller/audit.controller.js | 118 +++++---- src/controller/audit.controller/index.js | 6 +- src/repositories/auditRepository.js | 61 ++--- src/repositories/baseOrgRepository.js | 47 +++- .../audit/registryOrgCreatesAuditTest.js | 224 ++++++++++++++++++ 5 files changed, 372 insertions(+), 84 deletions(-) create mode 100644 test/integration-tests/audit/registryOrgCreatesAuditTest.js diff --git a/src/controller/audit.controller/audit.controller.js b/src/controller/audit.controller/audit.controller.js index 1540f41b..771b9a66 100644 --- a/src/controller/audit.controller/audit.controller.js +++ b/src/controller/audit.controller/audit.controller.js @@ -8,7 +8,7 @@ const validateUUID = require('uuid').validate * Create a new audit document * Called by POST /api/audit/org/ */ -async function createAuditDocument (req, res, next) { +async function createAuditDocumentForOrg (req, res, next) { try { const session = await mongoose.startSession() const repo = req.ctx.repositories.getAuditRepository() @@ -72,9 +72,25 @@ async function createAuditDocument (req, res, next) { await session.abortTransaction() return res.status(400).json(error.missingRequiredField('change_author')) } + + // Process entry immediately after validation + returnValue = await repo.appendToAuditHistoryForOrg( + body.target_uuid, + entry.audit_object, + entry.change_author, + { session } + ) } + } else { + // Create audit document with initial empty entry or default entry + returnValue = await repo.appendToAuditHistoryForOrg( + body.target_uuid, + body.audit_object || {}, + body.change_author || req.ctx.org, + { session } + ) } - returnValue = await repo.createAuditDocument(body, { session }) + await session.commitTransaction() logger.info({ @@ -100,7 +116,7 @@ async function createAuditDocument (req, res, next) { * Called by PUT /api/audit/org/ * Allows for multiple appends in a single request */ -async function appendToAuditHistory (req, res, next) { +async function appendToAuditHistoryForOrg (req, res, next) { try { const session = await mongoose.startSession() const repo = req.ctx.repositories.getAuditRepository() @@ -149,7 +165,7 @@ async function appendToAuditHistory (req, res, next) { } // Append this history entry - returnValue = await repo.appendToAuditHistory( + returnValue = await repo.appendToAuditHistoryForOrg( body.target_uuid, entry.audit_object, entry.change_author, @@ -190,7 +206,7 @@ async function appendToAuditHistory (req, res, next) { * Get all audit documents * Called by GET /api/audit/org/ */ -async function getAllAuditDocuments (req, res, next) { +async function getAllOrgAuditDocuments (req, res, next) { try { const session = await mongoose.startSession() const repo = req.ctx.repositories.getAuditRepository() @@ -213,7 +229,7 @@ async function getAllAuditDocuments (req, res, next) { * Get audit document by its document UUID * Called by GET /api/audit/org/document/:document_uuid */ -async function getAuditByDocumentUUID (req, res, next) { +async function getOrgAuditByDocumentUUID (req, res, next) { try { const session = await mongoose.startSession() const repo = req.ctx.repositories.getAuditRepository() @@ -247,48 +263,53 @@ async function getAuditByDocumentUUID (req, res, next) { next(err) } } + /** - * Get audit history by target UUID - * Called by GET /api/audit/org/:target_uuid - * TODO: remove comment-> I changed parameter name from org_identifier to target_uuid to be more generic. + * Get audit history by target identifier (shortname or UUID) + * Called by GET /api/audit/org/:identifier */ -async function getAuditByTargetUUID (req, res, next) { +async function getOrgAuditByOrgIdentifier (req, res, next) { try { const session = await mongoose.startSession() const repo = req.ctx.repositories.getAuditRepository() const orgRepo = req.ctx.repositories.getBaseOrgRepository() - const targetUUID = req.ctx.params.target_uuid + const identifier = req.ctx.params.org_identifier + const identifierIsUUID = validateUUID(identifier) let returnValue - if (!targetUUID) { - logger.info({ uuid: req.ctx.uuid, message: 'Missing target_uuid parameter' }) - return res.status(400).json(error.missingRequiredField('target_uuid')) - } - - if (!validateUUID(targetUUID)) { - logger.info({ uuid: req.ctx.uuid, message: 'Invalid target_uuid format' }) - return res.status(400).json(error.invalidUUID('target_uuid')) + if (!identifier) { + return res.status(400).json(error.missingRequiredField('identifier')) } try { session.startTransaction() - // Find the target organization - const targetOrg = await orgRepo.findOneByUUID(targetUUID, { session }) + // Find the target organization by either UUID or shortname + const targetOrg = identifierIsUUID + ? await orgRepo.findOneByUUID(identifier, { session }) + : await orgRepo.findOneByShortName(identifier, { session }) + if (!targetOrg) { - logger.info({ uuid: req.ctx.uuid, message: `No organization found with UUID ${targetUUID}` }) + logger.info({ + uuid: req.ctx.uuid, + message: `No organization found with ${identifierIsUUID ? 'UUID' : 'shortname'} ${identifier}` + }) await session.abortTransaction() - return res.status(404).json(error.orgDne(targetUUID)) + return res.status(404).json(error.orgDne(identifier)) } - // TODO: confirm middleware is checking admin and secretariat permissions properly + // Get the org's UUID for audit lookup + const targetUUID = targetOrg.UUID returnValue = await repo.findOneByTargetUUID(targetUUID, { session }) if (!returnValue) { - logger.info({ uuid: req.ctx.uuid, message: `No audit history found for target UUID ${targetUUID}` }) + logger.info({ + uuid: req.ctx.uuid, + message: `No audit history found for organization ${identifier} (UUID: ${targetUUID})` + }) await session.abortTransaction() - return res.status(404).json(error.auditDneByTarget(targetUUID)) + return res.status(404).json(error.auditDneByTarget(identifier)) } await session.commitTransaction() @@ -301,7 +322,7 @@ async function getAuditByTargetUUID (req, res, next) { logger.info({ uuid: req.ctx.uuid, - message: `Audit history for target UUID ${targetUUID} sent to user ${req.ctx.user}` + message: `Audit history for ${identifierIsUUID ? 'UUID' : 'shortname'} ${identifier} sent to user ${req.ctx.user}` }) return res.status(200).json(returnValue) } catch (err) { @@ -317,18 +338,14 @@ async function getLastXChanges (req, res, next) { try { const session = await mongoose.startSession() const repo = req.ctx.repositories.getAuditRepository() - const targetUUID = req.ctx.params.target_uuid + const orgRepo = req.ctx.repositories.getBaseOrgRepository() + const identifier = req.ctx.params.org_identifier + const identifierIsUUID = validateUUID(identifier) const numberOfChanges = parseInt(req.ctx.params.number_of_changes) let returnValue - if (!targetUUID) { - logger.info({ uuid: req.ctx.uuid, message: 'Missing org_identifier parameter' }) - return res.status(400).json(error.missingRequiredField('org_identifier')) - } - - if (!validateUUID(targetUUID)) { - logger.info({ uuid: req.ctx.uuid, message: 'Invalid target_uuid format' }) - return res.status(400).json(error.invalidUUID('target_uuid')) + if (!identifier) { + return res.status(400).json(error.missingRequiredField('identifier')) } if (isNaN(numberOfChanges) || numberOfChanges < 1) { @@ -339,6 +356,23 @@ async function getLastXChanges (req, res, next) { try { session.startTransaction() + // Find the target organization by either UUID or shortname + const targetOrg = identifierIsUUID + ? await orgRepo.findOneByUUID(identifier, { session }) + : await orgRepo.findOneByShortName(identifier, { session }) + + if (!targetOrg) { + logger.info({ + uuid: req.ctx.uuid, + message: `No organization found with ${identifierIsUUID ? 'UUID' : 'shortname'} ${identifier}` + }) + await session.abortTransaction() + return res.status(404).json(error.orgDne(identifier)) + } + + // Get the org's UUID for audit lookup + const targetUUID = targetOrg.UUID + const lastChanges = await repo.getLastXChanges(targetUUID, numberOfChanges, { session }) if (!lastChanges || lastChanges.length === 0) { @@ -362,7 +396,7 @@ async function getLastXChanges (req, res, next) { logger.info({ uuid: req.ctx.uuid, - message: `Last ${numberOfChanges} changes for ${targetUUID} sent to user ${req.ctx.user}` + message: `Last ${numberOfChanges} changes for ${identifier} sent to user ${req.ctx.user}` }) return res.status(200).json(returnValue) } catch (err) { @@ -371,10 +405,10 @@ async function getLastXChanges (req, res, next) { } module.exports = { - AUDIT_CREATE_SINGLE: createAuditDocument, - AUDIT_UPDATE: appendToAuditHistory, - AUDIT_GET_ALL: getAllAuditDocuments, - AUDIT_GET_BY_UUID: getAuditByDocumentUUID, - AUDIT_GET_BY_TARGET_UUID: getAuditByTargetUUID, + AUDIT_CREATE_SINGLE: createAuditDocumentForOrg, + AUDIT_UPDATE: appendToAuditHistoryForOrg, + AUDIT_GET_ALL: getAllOrgAuditDocuments, + AUDIT_GET_BY_UUID: getOrgAuditByDocumentUUID, + AUDIT_GET_BY_ORG_IDENTIFIER: getOrgAuditByOrgIdentifier, AUDIT_GET_LAST: getLastXChanges } diff --git a/src/controller/audit.controller/index.js b/src/controller/audit.controller/index.js index ba5b876a..10da8f70 100644 --- a/src/controller/audit.controller/index.js +++ b/src/controller/audit.controller/index.js @@ -27,15 +27,15 @@ router.get('/audit/org/document/:document_uuid', ) // Get audit by org identifier (Secretariat or Admin) -router.get('/audit/org/:target_uuid', +router.get('/audit/org/:org_identifier', mw.validateUser, mw.onlySecretariatOrAdmin, auditMw.parseGetParams, - controller.AUDIT_GET_BY_TARGET_UUID + controller.AUDIT_GET_BY_ORG_IDENTIFIER ) // Get last X changes (Secretariat or Org Admin) -router.get('/audit/org/:target_uuid/:number_of_changes', +router.get('/audit/org/:org_identifier/:number_of_changes', mw.onlySecretariatOrAdmin, mw.validateUser, auditMw.parseGetParams, diff --git a/src/repositories/auditRepository.js b/src/repositories/auditRepository.js index d79578d4..1beabdbc 100644 --- a/src/repositories/auditRepository.js +++ b/src/repositories/auditRepository.js @@ -1,5 +1,6 @@ const Audit = require('../model/audit') const BaseRepository = require('./baseRepository') +const BaseOrgRepository = require('./baseOrgRepository') const uuid = require('uuid') class AuditRepository extends BaseRepository { @@ -13,49 +14,52 @@ class AuditRepository extends BaseRepository { return validateObject } - /** - * Create a new audit document - */ - async createAuditDocument (data, options = {}) { - const auditData = { - uuid: uuid.v4(), - target_uuid: data.target_uuid, - history: data.history || [] - } - - const audit = new Audit(auditData) - const result = await audit.save(options) - return result.toObject() - } - /** * Append a new entry to the audit history * Creates document if it doesn't exist */ - async appendToAuditHistory (targetUUID, auditObject, changeAuthor, options = {}) { + async appendToAuditHistoryForOrg (targetUUID, auditObject, changeAuthor, options = {}) { const historyEntry = { timestamp: new Date(), audit_object: auditObject, change_author: changeAuthor } + try { // Try to find existing document - let audit = await Audit.findOne({ target_uuid: targetUUID }) + let audit = await this.findOneByTargetUUID(targetUUID, options) - if (!audit) { + if (!audit) { // Create new document if doesn't exist - audit = new Audit({ - uuid: uuid.v4(), - target_uuid: targetUUID, - history: [historyEntry] - }) - } else { + // Assuming 'uuid' is available for generating a new UUID + audit = new Audit({ + uuid: uuid.v4(), + target_uuid: targetUUID, + history: [historyEntry] + }) + } else { // Append to existing history - audit.history.push(historyEntry) + audit.history.push(historyEntry) + } + + const result = await audit.save(options) + return result.toObject() + } catch (error) { + throw new Error('Failed to save audit history entry.') } + } - const result = await audit.save(options) - return result.toObject() + /** + * Find audit document by target UUID + */ + async findOneByOrgShortname (orgShortName, options = {}) { + const baseOrgRepository = new BaseOrgRepository() + const org = await baseOrgRepository.findOneByShortName(orgShortName) + if (!org) { + return null + } + const query = { target_uuid: org.UUID } + return this.collection.findOne(query, null, options) } /** @@ -63,7 +67,8 @@ class AuditRepository extends BaseRepository { */ async findOneByTargetUUID (targetUUID, options = {}) { const query = { target_uuid: targetUUID } - return this.collection.findOne(query, null, options) + const auditObject = await Audit.findOne(query, null, options) + return auditObject } /** diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index 56d89378..0917a57c 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -238,14 +238,13 @@ class BaseOrgRepository extends BaseRepository { if (requestingUserUUID) { try { const auditRepo = new AuditRepository() - await auditRepo.appendToAuditHistory( + await auditRepo.appendToAuditHistoryForOrg( registryObjectRaw.UUID, registryObjectRaw, requestingUserUUID, options ) } catch (auditError) { - // Don't fail the transaction if audit fails - just log it } } @@ -261,7 +260,7 @@ class BaseOrgRepository extends BaseRepository { if ( legacyObjectRaw.authority.active_roles.length === 1 && ( legacyObjectRaw.authority.active_roles[0] === 'ADP' || - legacyObjectRaw.authority.active_roles[0] === 'BULK_DOWNLOAD') + legacyObjectRaw.authority.active_roles[0] === 'BULK_DOWNLOAD') ) { // ADPs have quota of 0 _.set(legacyObjectRaw, 'policies.id_quota', 0) @@ -269,7 +268,11 @@ class BaseOrgRepository extends BaseRepository { // The legacy way of doing this, the way this is written under the hood there is no other way // This await does not return a value, even though there is a return in it. :shrugg: - await legacyOrgRepo.updateByOrgUUID(sharedUUID, legacyObjectRaw, options) + try { + await legacyOrgRepo.updateByOrgUUID(sharedUUID, legacyObjectRaw, options) + } catch (error) { + + } if (isLegacyObject) { // This gets us the mongoose object that has all the right data in it, the "legacyObjectRaw" is the custom JSON we are sending. NOT the post written object. @@ -386,14 +389,36 @@ class BaseOrgRepository extends BaseRepository { if (requestingUserUUID) { try { const auditRepo = new AuditRepository() - await auditRepo.appendToAuditHistory( - registryOrg.UUID, - registryOrg.toObject(), - requestingUserUUID, - options - ) + // Check if an audit document exists, if not we need to create one first and seed it with the existing org data + if (!(await auditRepo.findOneByTargetUUID(registryOrg.UUID, options))) { + const currentRegistryOrg = await this.findOneByShortName(shortName, options) + await auditRepo.appendToAuditHistoryForOrg( + registryOrg.UUID, + currentRegistryOrg.toObject(), + requestingUserUUID, + options + ) + } + // Get the org state before save for comparison + const beforeUpdateOrg = await this.findOneByShortName(shortName, options) + const beforeUpdateObject = beforeUpdateOrg.toObject() + 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 + ) + } } catch (auditError) { - // Don't fail the transaction if audit fails - just log it + // Don't fail the transaction if audit fails - just log it } } diff --git a/test/integration-tests/audit/registryOrgCreatesAuditTest.js b/test/integration-tests/audit/registryOrgCreatesAuditTest.js new file mode 100644 index 00000000..74d9534b --- /dev/null +++ b/test/integration-tests/audit/registryOrgCreatesAuditTest.js @@ -0,0 +1,224 @@ +const chai = require('chai') +chai.use(require('chai-http')) +const { expect } = chai +const { v4: uuidv4 } = require('uuid') +const AuditRepo = require('../../../src/repositories/auditRepository') + +const app = require('../../../src/index.js') +const constants = require('../constants.js') + +const secretariatHeaders = { ...constants.headers } +const MAX_SHORTNAME_LENGTH = 32 + +async function createTestOrg (customProps = {}) { + const shortName = uuidv4().slice(0, MAX_SHORTNAME_LENGTH) + const defaultProps = { + short_name: shortName, + long_name: `Test Org ${shortName}`, + hard_quota: 1000, + authority: ['CNA'] + } + + const orgData = { ...defaultProps, ...customProps } + + const res = await chai.request(app) + .post('/api/registry/org') + .set(secretariatHeaders) + .send(orgData) + + expect(res).to.have.status(200) + + return { + shortName: orgData.short_name, + longName: orgData.long_name, + uuid: res.body.created.UUID, + fullResponse: res.body + } +} + +describe.only('Create and Update Audit Collection with Org Endpoints', () => { + it('Should automatically create audit document when org is created', async () => { + // Create org + const org = await createTestOrg({ + hard_quota: 1500, + authority: ['CNA'] + }) + + // Verify audit was created + const auditRes = await chai.request(app) + .get(`/api/audit/org/${org.uuid}`) + .set(constants.headers) + + expect(auditRes).to.have.status(200) + + // Verify audit structure + const audit = auditRes.body + expect(audit).to.have.property('uuid') + expect(audit).to.have.property('target_uuid') + expect(audit).to.have.property('history') + expect(audit.target_uuid).to.equal(org.uuid) + expect(audit.history).to.be.an('array').with.lengthOf(1) + + // Verify initial history entry + const initialEntry = audit.history[0] + expect(initialEntry).to.have.property('audit_object') + expect(initialEntry.timestamp).to.be.a('string') + expect(initialEntry.change_author).to.be.a('string') + + // Verify audit object matches created org + const auditObject = initialEntry.audit_object + expect(auditObject.short_name).to.equal(org.shortName) + expect(auditObject.long_name).to.equal(org.longName) + expect(auditObject.hard_quota).to.equal(1500) + expect(auditObject.UUID).to.equal(org.uuid) + }) + + it('Should create separate audit documents for multiple orgs', async () => { + // Create multiple orgs + const [org1, org2, org3] = await Promise.all([ + createTestOrg({ long_name: 'First Org' }), + createTestOrg({ long_name: 'Second Org' }), + createTestOrg({ long_name: 'Third Org' }) + ]) + + // Verify each has its own audit + const audits = await Promise.all([ + chai.request(app).get(`/api/audit/org/${org1.uuid}`).set(constants.headers), + chai.request(app).get(`/api/audit/org/${org2.uuid}`).set(constants.headers), + chai.request(app).get(`/api/audit/org/${org3.uuid}`).set(constants.headers) + ]) + + // Each should have its own audit document + audits.forEach((auditRes, index) => { + expect(auditRes).to.have.status(200) + const org = [org1, org2, org3][index] + expect(auditRes.body.target_uuid).to.equal(org.uuid) + expect(auditRes.body.history[0].audit_object.long_name).to.equal(org.longName) + }) + + // Audit UUIDs should all be different + const auditUUIDs = audits.map(res => res.body.uuid) + expect(new Set(auditUUIDs).size).to.equal(3) + }) + + it('Should NOT add audit entry when updating with no actual changes', async () => { + const org = await createTestOrg({ + hard_quota: 1500, + authority: ['CNA'] + }) + const updateRes = await chai.request(app) + .put(`/api/registry/org/${org.shortName}?long_name=${org.longName}`) + .set(secretariatHeaders) + expect(updateRes).to.have.status(200) + const auditResUpdate = await chai.request(app) + .get(`/api/audit/org/${org.uuid}`) + .set(constants.headers) + expect(auditResUpdate.body.history).to.have.lengthOf(1) + + // Now update with same values + const updateResAgain = await chai.request(app) + .put(`/api/registry/org/${org.shortName}?long_name=${org.longName}`) + .set(secretariatHeaders) + expect(updateResAgain).to.have.status(200) + + // Check audit history + const auditRes = await chai.request(app) + .get(`/api/audit/org/${org.uuid}`) + .set(constants.headers) + + expect(auditRes.body.history).to.have.lengthOf(1) + }) + + it.only('Should add audit entry when single field is changed', async () => { + const testOrg = await createTestOrg({ + hard_quota: 1500, + authority: ['CNA'] + }) + + // Update org name + const updateRes = await chai.request(app) + .put(`/api/registry/org/${testOrg.shortName}?id_quota=100`) + .set(secretariatHeaders) + + expect(updateRes).to.have.status(200) + + // Check audit history + const auditRes = await chai.request(app) + .get(`/api/audit/org/${testOrg.shortName}`) + .set(constants.headers) + + expect(auditRes.body.history).to.have.lengthOf(2) + + // Original entry + expect(auditRes.body.history[0].audit_object.hard_quota).to.equal(1500) + + // New entry + expect(auditRes.body.history[1].audit_object.hard_quota).to.equal(100) + }) + + it('Should maintain chronological order in audit history', async () => { + const testOrg = await createTestOrg({ + hard_quota: 1500, + authority: ['CNA'] + }) + await chai.request(app) + .put(`/api/registry/org/${testOrg.shortName}`) + .set(secretariatHeaders) + // Make sequential updates + await chai.request(app) + .put(`/api/registry/org/${testOrg.shortName}?id_quota=2000`) + .set(secretariatHeaders) + + await chai.request(app) + .put(`/api/registry/org/${testOrg.shortName}?id_quota=3000`) + .set(secretariatHeaders) + + await chai.request(app) + .put(`/api/registry/org/${testOrg.shortName}?id_quota=4000`) + .set(secretariatHeaders) + + // Check audit history + const auditRes = await chai.request(app) + .get(`/api/audit/org/${testOrg.uuid}`) + .set(constants.headers) + + expect(auditRes.body.history).to.have.lengthOf(4) + + // Verify chronological order + const quotas = auditRes.body.history.map(h => h.audit_object.hard_quota) + expect(quotas).to.deep.equal([1000, 2000, 3000, 4000]) + + // Verify timestamps are in order + for (let i = 1; i < auditRes.body.history.length; i++) { + const prev = new Date(auditRes.body.history[i - 1].timestamp) + const curr = new Date(auditRes.body.history[i].timestamp) + expect(curr.getTime()).to.be.greaterThan(prev.getTime()) + } + }) + + it('Should create an audit when updating an Org if it does not exist', async () => { + const testOrg = await createTestOrg({ + hard_quota: 1500, + authority: ['CNA'] + }) + // Manually delete audit document + const repo = new AuditRepo() + repo.deleteByTargetUUID(testOrg.uuid) + // Check audit history + const auditRes = await chai.request(app) + .get(`/api/audit/org/${testOrg.uuid}`) + .set(constants.headers) + expect(auditRes).to.have.status(404) + // Now update org to trigger audit creation + const updateRes = await chai.request(app) + .put(`/api/registry/org/${testOrg.shortName}?id_quota=2500`) + .set(secretariatHeaders) + expect(updateRes).to.have.status(200) + // Check audit history + const auditResCreation = await chai.request(app) + .get(`/api/audit/org/${testOrg.uuid}`) + .set(constants.headers) + + expect(auditResCreation.body.history).to.have.lengthOf(1) + }) +}) From f3a82417b53cc38feba5b1b296142e3816c66a06 Mon Sep 17 00:00:00 2001 From: emathew Date: Sun, 23 Nov 2025 21:56:17 -0500 Subject: [PATCH 2/4] fix tests --- src/repositories/baseOrgRepository.js | 54 +++++++++++++------ .../audit/registryOrgCreatesAuditTest.js | 29 +++++----- 2 files changed, 51 insertions(+), 32 deletions(-) diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index 71fd2733..59a77b10 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -375,16 +375,6 @@ class BaseOrgRepository extends BaseRepository { // legacy Only Stuff _.set(legacyOrg, 'policies.id_quota', (incomingParameters?.id_quota ?? legacyOrg.policies.id_quota)) - // Save changes - await registryOrg.save({ options }) - await legacyOrg.save({ options }) - if (isLegacyObject) { - const plainJavascriptLegacyOrg = legacyOrg.toObject() - delete plainJavascriptLegacyOrg.__v - delete plainJavascriptLegacyOrg._id - return deepRemoveEmpty(plainJavascriptLegacyOrg) - } - // ADD AUDIT ENTRY AUTOMATICALLY for the registry object. At this point permissions and object validation have been done. if (requestingUserUUID) { try { @@ -421,6 +411,15 @@ class BaseOrgRepository extends BaseRepository { // Don't fail the transaction if audit fails - just log it } } + // Save changes + await registryOrg.save({ options }) + await legacyOrg.save({ options }) + if (isLegacyObject) { + const plainJavascriptLegacyOrg = legacyOrg.toObject() + delete plainJavascriptLegacyOrg.__v + delete plainJavascriptLegacyOrg._id + return deepRemoveEmpty(plainJavascriptLegacyOrg) + } const plainJavascriptRegistryOrg = registryOrg.toObject() // Remove private things @@ -473,16 +472,37 @@ class BaseOrgRepository extends BaseRepository { return deepRemoveEmpty(plainJavascriptLegacyOrg) } - // ADD AUDIT ENTRY AUTOMATICALLY for the registry object. At this point permissions and object validation have been done. if (requestingUserUUID) { try { const auditRepo = new AuditRepository() - await auditRepo.appendToAuditHistory( - updatedRegistryOrg.UUID, - updatedRegistryOrg.toObject(), - requestingUserUUID, - options - ) + // Check if an audit document exists, if not we need to create one first and seed it with the existing org data + if (!(await auditRepo.findOneByTargetUUID(registryOrg.UUID, options))) { + const currentRegistryOrg = await this.findOneByShortName(shortName, options) + await auditRepo.appendToAuditHistoryForOrg( + registryOrg.UUID, + currentRegistryOrg.toObject(), + requestingUserUUID, + options + ) + } + // Get the org state before save for comparison + const beforeUpdateOrg = await this.findOneByShortName(shortName, options) + const beforeUpdateObject = beforeUpdateOrg.toObject() + 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 + ) + } } catch (auditError) { // Don't fail the transaction if audit fails - just log it } diff --git a/test/integration-tests/audit/registryOrgCreatesAuditTest.js b/test/integration-tests/audit/registryOrgCreatesAuditTest.js index 74d9534b..524d84be 100644 --- a/test/integration-tests/audit/registryOrgCreatesAuditTest.js +++ b/test/integration-tests/audit/registryOrgCreatesAuditTest.js @@ -36,7 +36,7 @@ async function createTestOrg (customProps = {}) { } } -describe.only('Create and Update Audit Collection with Org Endpoints', () => { +describe('Create and Update Audit Collection with Org Endpoints', () => { it('Should automatically create audit document when org is created', async () => { // Create org const org = await createTestOrg({ @@ -113,7 +113,7 @@ describe.only('Create and Update Audit Collection with Org Endpoints', () => { const auditResUpdate = await chai.request(app) .get(`/api/audit/org/${org.uuid}`) .set(constants.headers) - expect(auditResUpdate.body.history).to.have.lengthOf(1) + expect(auditResUpdate.body.history).to.have.lengthOf(2) // Now update with same values const updateResAgain = await chai.request(app) @@ -126,10 +126,10 @@ describe.only('Create and Update Audit Collection with Org Endpoints', () => { .get(`/api/audit/org/${org.uuid}`) .set(constants.headers) - expect(auditRes.body.history).to.have.lengthOf(1) + expect(auditRes.body.history).to.have.lengthOf(2) }) - it.only('Should add audit entry when single field is changed', async () => { + it('Should add audit entry when single field is changed', async () => { const testOrg = await createTestOrg({ hard_quota: 1500, authority: ['CNA'] @@ -161,22 +161,21 @@ describe.only('Create and Update Audit Collection with Org Endpoints', () => { hard_quota: 1500, authority: ['CNA'] }) - await chai.request(app) - .put(`/api/registry/org/${testOrg.shortName}`) - .set(secretariatHeaders) // Make sequential updates - await chai.request(app) + const updatedRes1 = await chai.request(app) .put(`/api/registry/org/${testOrg.shortName}?id_quota=2000`) .set(secretariatHeaders) + expect(updatedRes1).to.have.status(200) - await chai.request(app) + const updatedRes2 = await chai.request(app) .put(`/api/registry/org/${testOrg.shortName}?id_quota=3000`) .set(secretariatHeaders) + expect(updatedRes2).to.have.status(200) - await chai.request(app) + const updatedRes3 = await chai.request(app) .put(`/api/registry/org/${testOrg.shortName}?id_quota=4000`) .set(secretariatHeaders) - + expect(updatedRes3).to.have.status(200) // Check audit history const auditRes = await chai.request(app) .get(`/api/audit/org/${testOrg.uuid}`) @@ -186,7 +185,7 @@ describe.only('Create and Update Audit Collection with Org Endpoints', () => { // Verify chronological order const quotas = auditRes.body.history.map(h => h.audit_object.hard_quota) - expect(quotas).to.deep.equal([1000, 2000, 3000, 4000]) + expect(quotas).to.deep.equal([1500, 2000, 3000, 4000]) // Verify timestamps are in order for (let i = 1; i < auditRes.body.history.length; i++) { @@ -203,7 +202,7 @@ describe.only('Create and Update Audit Collection with Org Endpoints', () => { }) // Manually delete audit document const repo = new AuditRepo() - repo.deleteByTargetUUID(testOrg.uuid) + await repo.deleteByTargetUUID(testOrg.uuid) // Check audit history const auditRes = await chai.request(app) .get(`/api/audit/org/${testOrg.uuid}`) @@ -218,7 +217,7 @@ describe.only('Create and Update Audit Collection with Org Endpoints', () => { const auditResCreation = await chai.request(app) .get(`/api/audit/org/${testOrg.uuid}`) .set(constants.headers) - - expect(auditResCreation.body.history).to.have.lengthOf(1) + // Should have 2 entries: initial creation of current org object + new update + expect(auditResCreation.body.history).to.have.lengthOf(2) }) }) From 9439f328339e48950b2d677142a40fdb2281e72a Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 24 Nov 2025 13:32:39 -0500 Subject: [PATCH 3/4] remove unused import --- test/unit-tests/org/orgCreateADPTest.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/unit-tests/org/orgCreateADPTest.js b/test/unit-tests/org/orgCreateADPTest.js index b5dac740..47cd9f12 100644 --- a/test/unit-tests/org/orgCreateADPTest.js +++ b/test/unit-tests/org/orgCreateADPTest.js @@ -6,12 +6,6 @@ const { faker } = require('@faker-js/faker') const expect = chai.expect const mongoose = require('mongoose') -const OrgRepository = require('../../../src/repositories/orgRepository.js') -const UserRepository = require('../../../src/repositories/userRepository.js') - -const RegistryOrgRepository = require('../../../src/repositories/registryOrgRepository.js') -const RegistryUserRepository = require('../../../src/repositories/registryUserRepository.js') - const { ORG_CREATE_SINGLE } = require('../../../src/controller/org.controller/org.controller.js') const CONSTANTS = require('../../../src/constants/index.js') const BaseOrgRepository = require('../../../src/repositories/baseOrgRepository.js') From 1fa78d65c8f1a073776de4e042b422a6ac59c210 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 24 Nov 2025 13:42:35 -0500 Subject: [PATCH 4/4] linting issues --- .../registry-user.controller/registry-user.controller.js | 3 --- .../registry-org/registryOrgWithJointReviewTest.js | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/controller/registry-user.controller/registry-user.controller.js b/src/controller/registry-user.controller/registry-user.controller.js index 5a39f6f5..5815add6 100644 --- a/src/controller/registry-user.controller/registry-user.controller.js +++ b/src/controller/registry-user.controller/registry-user.controller.js @@ -24,9 +24,6 @@ async function getAllUsers (req, res, next) { const agt = setAggregateUserObj({}) const pg = await repo.aggregatePaginate(agt, options) - await RegistryOrg.populateOrgAffiliations(pg.itemsList) - await RegistryOrg.populateCVEProgramOrgMembership(pg.itemsList) - const payload = { users: pg.itemsList } if (pg.itemCount >= CONSTANTS.PAGINATOR_OPTIONS.limit) { diff --git a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js index 902652f9..f863250f 100644 --- a/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js +++ b/test/integration-tests/registry-org/registryOrgWithJointReviewTest.js @@ -69,7 +69,6 @@ describe('Testing Joint approval', () => { let secret let orgUUID let reviewUUID - let createdOrg it('Create an org to use for testing', async () => { await chai.request(app) .post('/api/registryOrg') @@ -97,8 +96,6 @@ describe('Testing Joint approval', () => { expect(res.body.created).to.haveOwnProperty('hard_quota') expect(res.body.created.hard_quota).to.equal(testRegistryOrgForReview.hard_quota) - - createdOrg = res.body.created }) }) it('Create an User', async () => {