From bc01075d77f481b6e0f3aa691f319217142004a5 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Thu, 4 Jun 2026 12:17:15 -0400 Subject: [PATCH 1/5] Add registry user expansion and update coverage --- schemas/registry-user/BaseUser.json | 16 +- .../registry-user.controller.js | 18 +- src/controller/user.controller/error.js | 21 ++ src/model/baseuser.js | 4 +- .../registry-user/registryUserCRUDTest.js | 98 +++++- test/integration-tests/user/getUsersTest.js | 67 ++++ test/integration-tests/user/updateUserTest.js | 110 ++++++ .../org/baseOrgRepositoryHelpersTest.js | 279 +++++++++++++++ .../org/registryOrgControllerTest.js | 189 +++++++++++ test/unit-tests/user/baseUserModelTest.js | 20 ++ .../user/registryUserControllerTest.js | 318 ++++++++++++++++++ test/unit-tests/utils/dateOnlyTest.js | 61 ++++ test/unit-tests/utils/utilsTest.js | 90 +++++ 13 files changed, 1281 insertions(+), 10 deletions(-) create mode 100644 test/integration-tests/user/getUsersTest.js create mode 100644 test/unit-tests/org/baseOrgRepositoryHelpersTest.js create mode 100644 test/unit-tests/org/registryOrgControllerTest.js create mode 100644 test/unit-tests/user/baseUserModelTest.js create mode 100644 test/unit-tests/user/registryUserControllerTest.js create mode 100644 test/unit-tests/utils/dateOnlyTest.js create mode 100644 test/unit-tests/utils/utilsTest.js diff --git a/schemas/registry-user/BaseUser.json b/schemas/registry-user/BaseUser.json index 5fa85687e..935a2262c 100644 --- a/schemas/registry-user/BaseUser.json +++ b/schemas/registry-user/BaseUser.json @@ -87,6 +87,20 @@ "inactive" ] }, + "org_affiliations": { + "description": "UUIDs of organizations the user is affiliated with", + "type": "array", + "items": { + "$ref": "#/definitions/uuidType" + } + }, + "cve_program_org_membership": { + "description": "UUIDs of CVE program organizations the user is a member of", + "type": "array", + "items": { + "$ref": "#/definitions/uuidType" + } + }, "role": { "description": "The user's role in the organization", "type": "string" @@ -101,4 +115,4 @@ "required": [ "username" ] -} \ No newline at end of file +} diff --git a/src/controller/registry-user.controller/registry-user.controller.js b/src/controller/registry-user.controller/registry-user.controller.js index 0cdc0d254..9fd195a87 100644 --- a/src/controller/registry-user.controller/registry-user.controller.js +++ b/src/controller/registry-user.controller/registry-user.controller.js @@ -6,6 +6,14 @@ const error = new errors.UserControllerError() const validateUUID = require('uuid').validate const _ = require('lodash') +const immutableUpdateFields = ['created', 'last_updated'] + +function removeImmutableUpdateFields (body) { + immutableUpdateFields.forEach(field => { + delete body[field] + }) +} + /** * Retrieves information about all registry users. * @@ -247,6 +255,9 @@ async function updateUser (req, res, next) { const body = req.ctx.body + // These fields are returned by GET responses, but remain server-managed on update. + removeImmutableUpdateFields(body) + if ('secret' in body) { logger.info({ uuid: req.ctx.uuid, message: 'User attempted to update the secret.' }) return res.status(400).json(error.secretUpdateNotAllowed()) @@ -356,13 +367,6 @@ async function updateUser (req, res, next) { logger.info({ uuid: req.ctx.uuid, message: userToEditParameters.username + ' user could not be found.' }) return res.status(404).json(error.userDne(userToEditParameters.username)) } - - if (!isSecretariat) { - // For now, we want to make sure that no one, other than a secretariat can edit time fields - delete body.created - delete body.last_updated - } - let result let updatedUser let updatedUserUUID diff --git a/src/controller/user.controller/error.js b/src/controller/user.controller/error.js index 7e0f7006f..7c6b60a78 100644 --- a/src/controller/user.controller/error.js +++ b/src/controller/user.controller/error.js @@ -37,6 +37,27 @@ class UserControllerError extends idrErr.IDRError { return err } + userExists (username) { // org + const err = {} + err.error = 'USER_EXISTS' + err.message = `The user '${username}' already exists.` + return err + } + + uuidProvided (creationType) { + const err = {} + err.error = 'UUID_PROVIDED' + err.message = `Providing UUIDs for ${creationType} creation or update is not allowed.` + return err + } + + userLimitReached () { + const err = {} + err.error = 'NUMBER_OF_USERS_IN_ORG_LIMIT_REACHED' + err.message = 'The requested user can not be created and added to the organization because the organization has hit its limit of 100 users. Contact the Secretariat.' + return err + } + duplicateUsername () { // org const err = {} err.error = 'DUPLICATE_USERNAME' diff --git a/src/model/baseuser.js b/src/model/baseuser.js index fcd4b1777..775484617 100644 --- a/src/model/baseuser.js +++ b/src/model/baseuser.js @@ -26,7 +26,9 @@ const schema = { last: String, suffix: String }, - status: { type: String, enum: ['active', 'inactive'] } + status: { type: String, enum: ['active', 'inactive'] }, + org_affiliations: [String], + cve_program_org_membership: [String] } // Export BaseUser model diff --git a/test/integration-tests/registry-user/registryUserCRUDTest.js b/test/integration-tests/registry-user/registryUserCRUDTest.js index 84a76d36d..c65b47765 100644 --- a/test/integration-tests/registry-user/registryUserCRUDTest.js +++ b/test/integration-tests/registry-user/registryUserCRUDTest.js @@ -1,6 +1,9 @@ +/* eslint-disable no-unused-expressions */ + const chai = require('chai') const expect = chai.expect chai.use(require('chai-http')) +const { v4: uuidv4 } = require('uuid') const constants = require('../constants.js') const app = require('../../../src/index.js') @@ -9,7 +12,100 @@ const secretariatHeaders = { ...constants.headers, 'content-type': 'application/ describe('Testing /registryUser endpoints', () => { context('Positive Tests', () => { - // TODO + it('Gets a list of all registry users', async () => { + await chai.request(app) + .get('/api/registryUser') + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body).to.have.property('users') + expect(res.body.users).to.be.an('array').that.is.not.empty + }) + }) + + it('Gets a registry user by UUID', async () => { + let user + await chai.request(app) + .get('/api/registry/org/win_5/user/jasminesmith@win_5.com') + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + user = res.body + }) + + await chai.request(app) + .get(`/api/registryUser/${user.UUID}`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body).to.have.property('UUID', user.UUID) + expect(res.body).to.have.property('username', user.username) + expect(res.body).to.not.have.property('secret') + }) + }) + + it('Creates, updates, and deletes a registry user by UUID', async () => { + const username = `${uuidv4()}@registry-user.test` + let userUUID + + await chai.request(app) + .post('/api/registryUser/range_4') + .set(secretariatHeaders) + .send({ + username, + name: { + first: 'Registry', + last: 'User' + }, + status: 'active' + }) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body).to.have.property('created') + expect(res.body.created).to.have.property('UUID') + expect(res.body.created).to.have.property('username', username) + userUUID = res.body.created.UUID + }) + + let user + await chai.request(app) + .get(`/api/registryUser/${userUUID}`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + user = res.body + }) + + await chai.request(app) + .put(`/api/registryUser/${userUUID}`) + .set(secretariatHeaders) + .send({ + ...user, + name: { + ...user.name, + first: 'UpdatedRegistry' + } + }) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.updated.name.first).to.equal('UpdatedRegistry') + }) + + await chai.request(app) + .delete(`/api/registryUser/${userUUID}`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.message).to.contain(`${username} was successfully deleted`) + }) + + await chai.request(app) + .get(`/api/registryUser/${userUUID}`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(404) + }) + }) }) context('Negative Tests', () => { it('Fails when page query parameter is not an integer', async () => { diff --git a/test/integration-tests/user/getUsersTest.js b/test/integration-tests/user/getUsersTest.js new file mode 100644 index 000000000..10f4a5401 --- /dev/null +++ b/test/integration-tests/user/getUsersTest.js @@ -0,0 +1,67 @@ +/* eslint-disable no-unused-expressions */ + +const chai = require('chai') +const expect = chai.expect +chai.use(require('chai-http')) + +const constants = require('../constants.js') +const app = require('../../../src/index.js') + +const secretariatHeaders = { ...constants.headers, 'content-type': 'application/json' } + +describe('Testing global user list endpoints', () => { + context('Positive Tests', () => { + it('Should get all registry users from /registry/users as Secretariat', async () => { + await chai.request(app) + .get('/api/registry/users') + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body).to.have.property('users') + expect(res.body.users).to.be.an('array').that.is.not.empty + }) + }) + + it('Should get all users from /users as Secretariat', async () => { + await chai.request(app) + .get('/api/users') + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body).to.have.property('users') + expect(res.body.users).to.be.an('array').that.is.not.empty + }) + }) + + it('Should get all registry users from /users with registry query as Secretariat', async () => { + await chai.request(app) + .get('/api/users?registry=true') + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body).to.have.property('users') + expect(res.body.users).to.be.an('array').that.is.not.empty + }) + }) + }) + + context('Negative Tests', () => { + it('Should reject non-Secretariat requests to /registry/users', async () => { + await chai.request(app) + .get('/api/registry/users') + .set(constants.nonSecretariatUserHeaders) + .then((res) => { + expect(res).to.have.status(403) + }) + }) + + it('Should reject non-Secretariat requests to /users', async () => { + await chai.request(app) + .get('/api/users') + .set(constants.nonSecretariatUserHeaders) + .then((res) => { + expect(res).to.have.status(403) + }) + }) + }) +}) diff --git a/test/integration-tests/user/updateUserTest.js b/test/integration-tests/user/updateUserTest.js index 24cd8cb19..7a4565a54 100644 --- a/test/integration-tests/user/updateUserTest.js +++ b/test/integration-tests/user/updateUserTest.js @@ -4,10 +4,40 @@ const chai = require('chai') chai.use(require('chai-http')) const expect = chai.expect +const { v4: uuidv4 } = require('uuid') const constants = require('../constants.js') const app = require('../../../src/index.js') +async function createRegistryUserForUpdateTest (orgShortName) { + const username = `${uuidv4()}@update-user.test` + await chai.request(app) + .post(`/api/registry/org/${orgShortName}/user`) + .set(constants.headers) + .send({ + username, + name: { + first: 'Update', + last: 'User' + }, + status: 'active' + }) + .then((res) => { + expect(res).to.have.status(200) + }) + + let user + await chai.request(app) + .get(`/api/registry/org/${orgShortName}/user/${username}`) + .set(constants.headers) + .then((res) => { + expect(res).to.have.status(200) + user = res.body + }) + + return user +} + describe('Testing Edit user endpoint', () => { context('Positive Tests', () => { it('Should correctly remove an admin from the original organization admins array when migrated to a new organization', async () => { @@ -183,6 +213,54 @@ describe('Testing Edit user endpoint', () => { expect(res.body.updated.status).to.equal('active') }) }) + it('Should update org affiliations with registry enabled', async () => { + const user = await createRegistryUserForUpdateTest('range_4') + let affiliatedOrg + await chai.request(app) + .get('/api/registry/org/win_5') + .set(constants.headers) + .then((res) => { + expect(res).to.have.status(200) + affiliatedOrg = res.body + }) + + await chai.request(app) + .put(`/api/registry/org/range_4/user/${user.username}`) + .set(constants.headers) + .send({ + ...user, + org_affiliations: [affiliatedOrg.UUID] + }) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body.updated.org_affiliations).to.deep.equal([affiliatedOrg.UUID]) + }) + }) + it('Should update CVE program org membership with registry enabled', async () => { + const user = await createRegistryUserForUpdateTest('range_4') + let memberOrg + await chai.request(app) + .get('/api/registry/org/range_4') + .set(constants.headers) + .then((res) => { + expect(res).to.have.status(200) + memberOrg = res.body + }) + + await chai.request(app) + .put(`/api/registry/org/range_4/user/${user.username}`) + .set(constants.headers) + .send({ + ...user, + cve_program_org_membership: [memberOrg.UUID] + }) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body.updated.cve_program_org_membership).to.deep.equal([memberOrg.UUID]) + }) + }) }) context('Negative Tests', () => { it('Should return an error when admin is required', async () => { @@ -211,6 +289,38 @@ describe('Testing Edit user endpoint', () => { expect(res.body.error).to.contain('NOT_ALLOWED_TO_CHANGE_FIELD') }) }) + it('Should return an error when a regular user attempts to update org affiliations with registry enabled', async () => { + let user + await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body }) + await chai.request(app) + .put('/api/registry/org/win_5/user/jasminesmith@win_5.com') + .set(constants.nonSecretariatUserHeaders) + .send({ + ...user, + org_affiliations: ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] + }) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(400) + expect(res.body.error).to.contain('NOT_ALLOWED_TO_CHANGE_FIELD') + }) + }) + it('Should return an error when a regular user attempts to update CVE program org membership with registry enabled', async () => { + let user + await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body }) + await chai.request(app) + .put('/api/registry/org/win_5/user/jasminesmith@win_5.com') + .set(constants.nonSecretariatUserHeaders) + .send({ + ...user, + cve_program_org_membership: ['7f593536-7cbc-46fd-bdd9-b6176c9cd93f'] + }) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(400) + expect(res.body.error).to.contain('NOT_ALLOWED_TO_CHANGE_FIELD') + }) + }) it('Should not allow a first name of more than 100 characters', async () => { await chai.request(app) .put('/api/org/win_5/user/jasminesmith@win_5.com?name.first=1:1234567,2:1234567,3:1234567,4:1234567,5:1234567,6:1234567,7:1234567,8:1234567,9:1234567,10:1234567,11:1234567') diff --git a/test/unit-tests/org/baseOrgRepositoryHelpersTest.js b/test/unit-tests/org/baseOrgRepositoryHelpersTest.js new file mode 100644 index 000000000..293a2f725 --- /dev/null +++ b/test/unit-tests/org/baseOrgRepositoryHelpersTest.js @@ -0,0 +1,279 @@ +const { expect } = require('chai') +const sinon = require('sinon') + +const helpers = require('../../../src/repositories/baseOrgRepositoryHelpers') +const AuditRepository = require('../../../src/repositories/auditRepository') +const ReviewObjectRepository = require('../../../src/repositories/reviewObjectRepository') +const BaseOrgModel = require('../../../src/model/baseorg') +const ADPOrgModel = require('../../../src/model/adporg') + +describe('Testing BaseOrgRepository helper functions', () => { + afterEach(() => { + sinon.restore() + }) + + context('mergeAllowedFields', () => { + it('Should preserve protected fields and replace arrays from incoming data', () => { + const targetDoc = { + toObject: () => ({ + UUID: 'original-uuid', + aliases: ['old-alias'], + long_name: 'Old Name' + }), + overwrite: sinon.stub().callsFake(arg => arg) + } + + const result = helpers.mergeAllowedFields(targetDoc, { + UUID: 'malicious-uuid', + aliases: ['new-alias'], + long_name: 'New Name' + }, ['UUID']) + + expect(result).to.deep.equal({ + UUID: 'original-uuid', + aliases: ['new-alias'], + long_name: 'New Name' + }) + expect(targetDoc.overwrite.calledOnce).to.equal(true) + }) + }) + + context('manageReviewObject', () => { + it('Should create a review object when joint approval fields change and none exists', async () => { + const createReviewOrgObject = sinon.stub(ReviewObjectRepository.prototype, 'createReviewOrgObject').resolves({}) + const registryOrg = { + toObject: () => ({ + UUID: 'org-uuid', + short_name: 'old_short_name', + long_name: 'Old Name' + }) + } + + await helpers.manageReviewObject( + registryOrg, + { short_name: 'new_short_name' }, + ['short_name'], + null, + 'requester@example.org', + {} + ) + + expect(createReviewOrgObject.calledOnce).to.equal(true) + expect(createReviewOrgObject.firstCall.args[0]).to.include({ + UUID: 'org-uuid', + short_name: 'new_short_name' + }) + expect(createReviewOrgObject.firstCall.args[1]).to.equal('requester@example.org') + }) + + it('Should reject an existing review object when joint approval fields no longer change', async () => { + const rejectReviewOrgObject = sinon.stub(ReviewObjectRepository.prototype, 'rejectReviewOrgObject').resolves({}) + const registryOrg = { + toObject: () => ({ + UUID: 'org-uuid', + short_name: 'same_short_name' + }) + } + + await helpers.manageReviewObject( + registryOrg, + { short_name: 'same_short_name' }, + ['short_name'], + { uuid: 'review-uuid' }, + 'requester@example.org', + {} + ) + + expect(rejectReviewOrgObject.calledOnce).to.equal(true) + expect(rejectReviewOrgObject.firstCall.args[0]).to.equal('review-uuid') + expect(rejectReviewOrgObject.firstCall.args[1]).to.equal('requester@example.org') + expect(rejectReviewOrgObject.firstCall.args[2]).to.deep.equal({}) + }) + }) + + context('processJointApprovalAndMerge', () => { + it('Should merge all allowed fields immediately for Secretariat users', async () => { + const registryDoc = { + toObject: () => ({ + UUID: 'registry-uuid', + short_name: 'old_registry', + users: ['existing-user'] + }), + overwrite: sinon.stub().callsFake(arg => arg) + } + const legacyDoc = { + toObject: () => ({ + UUID: 'legacy-uuid', + short_name: 'old_legacy' + }), + overwrite: sinon.stub().callsFake(arg => arg) + } + + const result = await helpers.processJointApprovalAndMerge( + registryDoc, + legacyDoc, + { + UUID: 'malicious-registry-uuid', + short_name: 'new_registry', + users: ['malicious-user'] + }, + { + UUID: 'malicious-legacy-uuid', + short_name: 'new_legacy' + }, + null, + true, + {}, + 'secretariat@example.org', + [], + [] + ) + + expect(result.updatedRegistryOrg).to.deep.equal({ + UUID: 'registry-uuid', + short_name: 'new_registry', + users: ['existing-user'] + }) + expect(result.updatedLegacyOrg).to.deep.equal({ + UUID: 'legacy-uuid', + short_name: 'new_legacy' + }) + }) + + it('Should defer joint approval fields and merge non-joint fields for non-Secretariat users', async () => { + const createReviewOrgObject = sinon.stub(ReviewObjectRepository.prototype, 'createReviewOrgObject').resolves({}) + const registryDoc = { + toObject: () => ({ + UUID: 'registry-uuid', + short_name: 'old_registry', + long_name: 'Old Registry Name' + }), + overwrite: sinon.stub().callsFake(arg => arg) + } + const legacyDoc = { + toObject: () => ({ + UUID: 'legacy-uuid', + short_name: 'old_legacy', + name: 'Old Legacy Name' + }), + overwrite: sinon.stub().callsFake(arg => arg) + } + + const result = await helpers.processJointApprovalAndMerge( + registryDoc, + legacyDoc, + { + short_name: 'new_registry', + long_name: 'New Registry Name' + }, + { + short_name: 'new_legacy', + name: 'New Legacy Name' + }, + null, + false, + {}, + 'admin@example.org', + ['short_name'], + ['short_name'] + ) + + expect(createReviewOrgObject.calledOnce).to.equal(true) + expect(result.updatedRegistryOrg).to.deep.equal({ + UUID: 'registry-uuid', + short_name: 'old_registry', + long_name: 'New Registry Name' + }) + expect(result.updatedLegacyOrg).to.deep.equal({ + UUID: 'legacy-uuid', + short_name: 'old_legacy', + name: 'New Legacy Name' + }) + }) + }) + + context('createAuditLogEntry', () => { + it('Should seed audit history and skip append when the org did not change', async () => { + sinon.stub(console, 'log') + const seedAuditHistoryForOrg = sinon.stub(AuditRepository.prototype, 'seedAuditHistoryForOrg').resolves({}) + const appendToAuditHistoryForOrg = sinon.stub(AuditRepository.prototype, 'appendToAuditHistoryForOrg').resolves({}) + const originalOrg = { + UUID: 'org-uuid', + short_name: 'same_org' + } + const registryOrg = { + UUID: 'org-uuid', + toObject: () => ({ + UUID: 'org-uuid', + short_name: 'same_org' + }) + } + + await helpers.createAuditLogEntry(registryOrg, originalOrg, 'requester-uuid', {}) + + expect(seedAuditHistoryForOrg.calledOnce).to.equal(true) + expect(appendToAuditHistoryForOrg.notCalled).to.equal(true) + }) + + it('Should append audit history when the org changed', async () => { + sinon.stub(console, 'log') + sinon.stub(AuditRepository.prototype, 'seedAuditHistoryForOrg').resolves({}) + const appendToAuditHistoryForOrg = sinon.stub(AuditRepository.prototype, 'appendToAuditHistoryForOrg').resolves({}) + const registryOrg = { + UUID: 'org-uuid', + toObject: () => ({ + UUID: 'org-uuid', + short_name: 'new_org' + }) + } + + await helpers.createAuditLogEntry(registryOrg, { + UUID: 'org-uuid', + short_name: 'old_org' + }, 'requester-uuid', {}) + + expect(appendToAuditHistoryForOrg.calledOnce).to.equal(true) + expect(appendToAuditHistoryForOrg.firstCall.args[0]).to.equal('org-uuid') + expect(appendToAuditHistoryForOrg.firstCall.args[2]).to.equal('requester-uuid') + }) + }) + + context('handleAuthorityModelChange', () => { + it('Should not recast the org when authority has not changed', async () => { + const deleteOne = sinon.stub(BaseOrgModel, 'deleteOne').resolves({}) + const org = { + authority: ['CNA'], + toObject: () => ({ authority: ['CNA'] }) + } + + const result = await helpers.handleAuthorityModelChange(org, ['CNA'], {}) + + expect(result).to.equal(org) + expect(deleteOne.notCalled).to.equal(true) + }) + + it('Should recast the org document when authority changes', async () => { + const deleteOne = sinon.stub(BaseOrgModel, 'deleteOne').resolves({}) + const save = sinon.stub(ADPOrgModel.prototype, 'save').resolves({}) + const org = { + _id: 'mongo-id', + authority: ['ADP'], + toObject: () => ({ + _id: 'mongo-id', + UUID: 'org-uuid', + short_name: 'adp_org', + long_name: 'ADP Org', + authority: ['ADP'] + }) + } + + const result = await helpers.handleAuthorityModelChange(org, ['CNA'], {}) + + expect(deleteOne.calledOnce).to.equal(true) + expect(deleteOne.firstCall.args[0]).to.deep.equal({ _id: 'mongo-id' }) + expect(deleteOne.firstCall.args[1]).to.deep.equal({}) + expect(save.calledOnce).to.equal(true) + expect(result.toObject().authority).to.deep.equal(['ADP']) + }) + }) +}) diff --git a/test/unit-tests/org/registryOrgControllerTest.js b/test/unit-tests/org/registryOrgControllerTest.js new file mode 100644 index 000000000..9d408727f --- /dev/null +++ b/test/unit-tests/org/registryOrgControllerTest.js @@ -0,0 +1,189 @@ +const { expect } = require('chai') +const sinon = require('sinon') +const mongoose = require('mongoose') + +const controller = require('../../../src/controller/registry-org.controller/registry-org.controller') + +function mockResponse () { + const res = {} + res.status = sinon.stub().returns(res) + res.json = sinon.stub().returns(res) + return res +} + +function mockSession () { + return { + startTransaction: sinon.stub(), + abortTransaction: sinon.stub().resolves(), + commitTransaction: sinon.stub().resolves(), + endSession: sinon.stub().resolves(), + inTransaction: sinon.stub().returns(false) + } +} + +describe('Testing Registry Org Controller', () => { + afterEach(() => { + sinon.restore() + }) + + context('SINGLE_ORG', () => { + it('Should strip internal conversation fields for non-Secretariat users', async () => { + const res = mockResponse() + const orgRepo = { + findOneByShortName: sinon.stub().resolves({ UUID: 'org-uuid', short_name: 'activity_6' }), + isSecretariat: sinon.stub().resolves(false), + getOrg: sinon.stub().resolves({ UUID: 'org-uuid', short_name: 'activity_6' }) + } + const conversationRepo = { + getAllByTargetUUID: sinon.stub().resolves([{ + UUID: 'conversation-uuid', + body: 'Visible body', + visibility: 'private', + target_uuid: 'org-uuid', + previous_conversation_uuid: null, + next_conversation_uuid: null, + _id: 'mongo-id', + __v: 0 + }]) + } + const req = { + ctx: { + uuid: 'request-uuid', + org: 'activity_6', + params: { identifier: 'activity_6' }, + repositories: { + getBaseOrgRepository: () => orgRepo, + getConversationRepository: () => conversationRepo + } + } + } + + await controller.SINGLE_ORG(req, res, sinon.stub()) + + expect(res.status.calledOnceWith(200)).to.equal(true) + const payload = res.json.firstCall.args[0] + expect(payload.conversation).to.have.lengthOf(1) + expect(payload.conversation[0]).to.have.property('body', 'Visible body') + expect(payload.conversation[0]).to.not.have.property('UUID') + expect(payload.conversation[0]).to.not.have.property('visibility') + expect(payload.conversation[0]).to.not.have.property('target_uuid') + expect(payload.conversation[0]).to.not.have.property('_id') + expect(payload.conversation[0]).to.not.have.property('__v') + }) + + it('Should reject non-Secretariat access to another organization', async () => { + const res = mockResponse() + const orgRepo = { + findOneByShortName: sinon.stub().resolves({ UUID: 'requester-org-uuid', short_name: 'win_5' }), + isSecretariat: sinon.stub().resolves(false), + getOrg: sinon.stub() + } + const req = { + ctx: { + uuid: 'request-uuid', + org: 'win_5', + params: { identifier: 'activity_6' }, + repositories: { + getBaseOrgRepository: () => orgRepo, + getConversationRepository: () => ({}) + } + } + } + + await controller.SINGLE_ORG(req, res, sinon.stub()) + + expect(res.status.calledOnceWith(403)).to.equal(true) + expect(res.json.firstCall.args[0].error).to.equal('NOT_SAME_ORG_OR_SECRETARIAT') + expect(orgRepo.getOrg.notCalled).to.equal(true) + }) + }) + + context('UPDATE_ORG', () => { + it('Should reject attempts to change an existing organization UUID', async () => { + const session = mockSession() + sinon.stub(mongoose, 'startSession').resolves(session) + const res = mockResponse() + const orgRepo = { + isSecretariatByShortName: sinon.stub().resolves(true), + findOneByShortName: sinon.stub().resolves({ + UUID: 'stored-org-uuid', + short_name: 'activity_6' + }) + } + const userRepo = { + isAdmin: sinon.stub().resolves(false), + findOneByUsernameAndOrgShortname: sinon.stub().resolves({ UUID: 'requester-user-uuid' }) + } + const req = { + ctx: { + uuid: 'request-uuid', + org: 'mitre', + user: 'test_secretariat_0@mitre.org', + params: { shortname: 'activity_6' }, + body: { + UUID: 'different-org-uuid', + short_name: 'activity_6' + }, + repositories: { + getBaseOrgRepository: () => orgRepo, + getBaseUserRepository: () => userRepo, + getConversationRepository: () => ({}) + } + } + } + + await controller.UPDATE_ORG(req, res, sinon.stub()) + + expect(res.status.calledOnceWith(400)).to.equal(true) + expect(res.json.firstCall.args[0].error).to.equal('UUID_PROVIDED') + expect(session.abortTransaction.calledOnce).to.equal(true) + expect(session.endSession.calledOnce).to.equal(true) + }) + + it('Should update an existing pending review object when the org is not yet approved', async () => { + const session = mockSession() + sinon.stub(mongoose, 'startSession').resolves(session) + const res = mockResponse() + const orgRepo = { + isSecretariatByShortName: sinon.stub().resolves(true), + findOneByShortName: sinon.stub().resolves(null) + } + const userRepo = { + isAdmin: sinon.stub().resolves(false), + findOneByUsernameAndOrgShortname: sinon.stub().resolves({ UUID: 'requester-user-uuid' }) + } + const reviewRepo = { + getOrgReviewObjectByOrgShortname: sinon.stub().resolves({ uuid: 'review-uuid' }), + updateReviewOrgObject: sinon.stub().resolves({ uuid: 'review-uuid' }) + } + const req = { + ctx: { + uuid: 'request-uuid', + org: 'mitre', + user: 'test_secretariat_0@mitre.org', + params: { shortname: 'pending_org' }, + body: { + UUID: 'review-uuid', + short_name: 'pending_org', + long_name: 'Pending Org' + }, + repositories: { + getBaseOrgRepository: () => orgRepo, + getBaseUserRepository: () => userRepo, + getConversationRepository: () => ({}), + getReviewObjectRepository: () => reviewRepo + } + } + } + + await controller.UPDATE_ORG(req, res, sinon.stub()) + + expect(reviewRepo.updateReviewOrgObject.calledOnce).to.equal(true) + expect(reviewRepo.updateReviewOrgObject.firstCall.args[1]).to.equal('review-uuid') + expect(session.commitTransaction.calledOnce).to.equal(true) + expect(session.endSession.calledOnce).to.equal(true) + expect(res.status.calledOnceWith(200)).to.equal(true) + expect(res.json.firstCall.args[0].message).to.equal('Review object updated successfully') + }) + }) +}) diff --git a/test/unit-tests/user/baseUserModelTest.js b/test/unit-tests/user/baseUserModelTest.js new file mode 100644 index 000000000..983fa6edc --- /dev/null +++ b/test/unit-tests/user/baseUserModelTest.js @@ -0,0 +1,20 @@ +const { expect } = require('chai') + +const BaseUser = require('../../../src/model/baseuser') + +describe('Testing BaseUser Model', () => { + it('Should validate and model registry user organization membership fields', () => { + const orgAffiliationUUID = '7f593536-7cbc-46fd-bdd9-b6176c9cd93f' + const cveProgramMembershipUUID = 'dd5389e6-7cbc-46fd-bdd9-b6176c9cd93f' + + const result = BaseUser.validateUser({ + username: 'registry_user@example.org', + org_affiliations: [orgAffiliationUUID], + cve_program_org_membership: [cveProgramMembershipUUID] + }) + + expect(result.isValid).to.equal(true) + expect(BaseUser.schema.path('org_affiliations')).to.not.equal(undefined) + expect(BaseUser.schema.path('cve_program_org_membership')).to.not.equal(undefined) + }) +}) diff --git a/test/unit-tests/user/registryUserControllerTest.js b/test/unit-tests/user/registryUserControllerTest.js new file mode 100644 index 000000000..6a173689f --- /dev/null +++ b/test/unit-tests/user/registryUserControllerTest.js @@ -0,0 +1,318 @@ +const { expect } = require('chai') +const sinon = require('sinon') +const mongoose = require('mongoose') + +const controller = require('../../../src/controller/registry-user.controller/registry-user.controller') + +function mockResponse () { + const res = {} + res.status = sinon.stub().returns(res) + res.json = sinon.stub().returns(res) + return res +} + +function mockSession () { + return { + startTransaction: sinon.stub(), + abortTransaction: sinon.stub().resolves(), + commitTransaction: sinon.stub().resolves(), + endSession: sinon.stub().resolves() + } +} + +describe('Testing Registry User Controller', () => { + afterEach(() => { + sinon.restore() + }) + + context('ALL_USERS', () => { + it('Should hydrate ADMIN role from organization admins', async () => { + const res = mockResponse() + const userRepo = { + getAllUsers: sinon.stub().resolves({ + users: [ + { UUID: 'admin-user-uuid', org_UUID: 'org-uuid' }, + { UUID: 'regular-user-uuid', org_UUID: 'org-uuid' } + ] + }) + } + const orgRepo = { + findOneByUUID: sinon.stub().resolves({ admins: ['admin-user-uuid'] }) + } + const req = { + ctx: { + uuid: 'request-uuid', + query: {}, + repositories: { + getBaseUserRepository: () => userRepo, + getBaseOrgRepository: () => orgRepo + } + } + } + + await controller.ALL_USERS(req, res, sinon.stub()) + + expect(res.status.calledOnceWith(200)).to.equal(true) + const payload = res.json.firstCall.args[0] + expect(payload.users[0]).to.have.property('role', 'ADMIN') + expect(payload.users[1]).to.not.have.property('role') + }) + }) + + context('SINGLE_USER', () => { + it('Should reject non-UUID identifiers on the utility endpoint', async () => { + const res = mockResponse() + const req = { + ctx: { + params: { identifier: 'not-a-uuid' } + } + } + + await controller.SINGLE_USER(req, res, sinon.stub()) + + expect(res.status.calledOnceWith(400)).to.equal(true) + expect(res.json.firstCall.args[0].error).to.equal('This function expects a UUID when called this way') + }) + }) + + context('CREATE_USER', () => { + it('Should reject user UUIDs in the request body', async () => { + sinon.stub(mongoose, 'startSession').resolves(mockSession()) + const res = mockResponse() + const orgRepo = { + getOrgUUID: sinon.stub().resolves('org-uuid') + } + const req = { + ctx: { + uuid: 'request-uuid', + body: { + UUID: 'provided-user-uuid', + username: 'created_user@example.org' + }, + params: { shortname: 'range_4' }, + repositories: { + getBaseOrgRepository: () => orgRepo, + getBaseUserRepository: () => ({}) + } + } + } + + await controller.CREATE_USER(req, res, sinon.stub()) + + expect(res.status.calledOnceWith(400)).to.equal(true) + expect(res.json.firstCall.args[0].error).to.equal('UUID_PROVIDED') + }) + + it('Should reject org UUIDs in the request body', async () => { + sinon.stub(mongoose, 'startSession').resolves(mockSession()) + const res = mockResponse() + const orgRepo = { + getOrgUUID: sinon.stub().resolves('org-uuid') + } + const req = { + ctx: { + uuid: 'request-uuid', + body: { + org_UUID: 'provided-org-uuid', + username: 'created_user@example.org' + }, + params: { shortname: 'range_4' }, + repositories: { + getBaseOrgRepository: () => orgRepo, + getBaseUserRepository: () => ({}) + } + } + } + + await controller.CREATE_USER(req, res, sinon.stub()) + + expect(res.status.calledOnceWith(400)).to.equal(true) + expect(res.json.firstCall.args[0].error).to.equal('UUID_PROVIDED') + }) + + it('Should reject duplicate users in the organization', async () => { + const session = mockSession() + sinon.stub(mongoose, 'startSession').resolves(session) + const res = mockResponse() + const orgRepo = { + getOrgUUID: sinon.stub().resolves('org-uuid') + } + const userRepo = { + validateUser: sinon.stub().resolves({ isValid: true }), + orgHasUser: sinon.stub().resolves(true) + } + const req = { + ctx: { + uuid: 'request-uuid', + body: { + username: 'created_user@example.org' + }, + params: { shortname: 'range_4' }, + repositories: { + getBaseOrgRepository: () => orgRepo, + getBaseUserRepository: () => userRepo + } + } + } + + await controller.CREATE_USER(req, res, sinon.stub()) + + expect(res.status.calledOnceWith(400)).to.equal(true) + expect(res.json.firstCall.args[0].error).to.equal('USER_EXISTS') + expect(session.abortTransaction.calledOnce).to.equal(true) + expect(session.endSession.calledOnce).to.equal(true) + }) + + it('Should reject creating users after the organization reaches the user limit', async () => { + const session = mockSession() + sinon.stub(mongoose, 'startSession').resolves(session) + const res = mockResponse() + const orgRepo = { + getOrgUUID: sinon.stub().resolves('org-uuid') + } + const userRepo = { + validateUser: sinon.stub().resolves({ isValid: true }), + orgHasUser: sinon.stub().resolves(false), + findUsersByOrgShortname: sinon.stub().resolves(Array.from({ length: 100 }, (_, index) => ({ UUID: `existing-user-${index}` }))) + } + const req = { + ctx: { + uuid: 'request-uuid', + body: { + username: 'created_user@example.org' + }, + params: { shortname: 'range_4' }, + repositories: { + getBaseOrgRepository: () => orgRepo, + getBaseUserRepository: () => userRepo + } + } + } + + await controller.CREATE_USER(req, res, sinon.stub()) + + expect(res.status.calledOnceWith(400)).to.equal(true) + expect(res.json.firstCall.args[0].error).to.equal('NUMBER_OF_USERS_IN_ORG_LIMIT_REACHED') + expect(session.abortTransaction.calledOnce).to.equal(true) + expect(session.endSession.calledOnce).to.equal(true) + }) + }) + + context('UPDATE_USER', () => { + it('Should reject non-UUID identifiers on the utility endpoint', async () => { + sinon.stub(mongoose, 'startSession').resolves(mockSession()) + const res = mockResponse() + const req = { + ctx: { + params: { identifier: 'username@example.org' } + } + } + + await controller.UPDATE_USER(req, res, sinon.stub()) + + expect(res.status.calledOnceWith(400)).to.equal(true) + expect(res.json.firstCall.args[0].error).to.equal('This function expects a UUID when called this way') + }) + + it('Should ignore immutable timestamps and update registry membership fields', async () => { + const session = mockSession() + sinon.stub(mongoose, 'startSession').resolves(session) + const res = mockResponse() + const userUUID = 'd41d8cd9-8f00-4204-a980-0998ecf8427e' + const orgUUID = '405450a6-8f00-4204-a980-0998ecf8427e' + const affiliatedOrgUUID = '7f593536-7cbc-46fd-bdd9-b6176c9cd93f' + const memberOrgUUID = 'dd5389e6-7cbc-46fd-bdd9-b6176c9cd93f' + const userToEdit = { + UUID: userUUID, + username: 'created_user@example.org', + org_UUID: orgUUID + } + const org = { + UUID: orgUUID, + short_name: 'range_4', + admins: [] + } + const body = { + UUID: userUUID, + username: 'created_user@example.org', + name: { + first: 'Registry', + last: 'User' + }, + status: 'active', + created: '2026-01-01T00:00:00.000Z', + last_updated: '2026-01-02T00:00:00.000Z', + org_affiliations: [affiliatedOrgUUID], + cve_program_org_membership: [memberOrgUUID] + } + const updatedUser = { + UUID: userUUID, + username: 'created_user@example.org', + name: body.name, + status: 'active', + org_affiliations: [affiliatedOrgUUID], + cve_program_org_membership: [memberOrgUUID] + } + const userRepo = { + isAdmin: sinon.stub().resolves(false), + findUserByUUID: sinon.stub().resolves(userToEdit), + validateUser: sinon.stub().returns({ isValid: true }), + getUserUUID: sinon.stub().resolves('requesting-user-uuid'), + updateUserFull: sinon.stub().resolves(updatedUser) + } + const orgRepo = { + isSecretariatByShortName: sinon.stub().resolves(true), + getOrgUUIDByUserUUID: sinon.stub().resolves(orgUUID), + findOneByUUID: sinon.stub().resolves(org) + } + const req = { + ctx: { + uuid: 'request-uuid', + user: 'secretariat@example.org', + org: 'mitre', + body, + params: { identifier: userUUID }, + repositories: { + getBaseOrgRepository: () => orgRepo, + getBaseUserRepository: () => userRepo + } + } + } + + await controller.UPDATE_USER(req, res, sinon.stub()) + + expect(res.status.calledOnceWith(200)).to.equal(true) + const validatedBody = userRepo.validateUser.firstCall.args[0] + expect(validatedBody).to.not.have.property('created') + expect(validatedBody).to.not.have.property('last_updated') + expect(validatedBody.org_affiliations).to.deep.equal([affiliatedOrgUUID]) + expect(validatedBody.cve_program_org_membership).to.deep.equal([memberOrgUUID]) + expect(userRepo.updateUserFull.firstCall.args[1]).to.equal(validatedBody) + expect(res.json.firstCall.args[0].updated).to.deep.equal(updatedUser) + }) + }) + + context('DELETE_USER', () => { + it('Should return 404 when the user does not exist', async () => { + const res = mockResponse() + const userRepo = { + findUserByUUID: sinon.stub().resolves(null) + } + const req = { + ctx: { + uuid: 'request-uuid', + params: { identifier: 'missing-user-uuid' }, + repositories: { + getBaseUserRepository: () => userRepo, + getBaseOrgRepository: () => ({}) + } + } + } + + await controller.DELETE_USER(req, res, sinon.stub()) + + expect(res.status.calledOnceWith(404)).to.equal(true) + expect(res.json.firstCall.args[0].error).to.equal('USER_DNE') + }) + }) +}) diff --git a/test/unit-tests/utils/dateOnlyTest.js b/test/unit-tests/utils/dateOnlyTest.js new file mode 100644 index 000000000..bc47d1045 --- /dev/null +++ b/test/unit-tests/utils/dateOnlyTest.js @@ -0,0 +1,61 @@ +const { expect } = require('chai') + +const { + isValidDateOnlyString, + normalizeDateOnlyInput, + normalizeDateOnlyOutput, + normalizeOrgCveWebsiteUpdateDate +} = require('../../../src/utils/dateOnly') + +describe('Testing date-only utilities', () => { + context('isValidDateOnlyString', () => { + it('Should return true for a valid date-only string', () => { + expect(isValidDateOnlyString('2026-06-04')).to.equal(true) + }) + + it('Should return false for invalid dates and non-date-only strings', () => { + expect(isValidDateOnlyString('2026-02-29')).to.equal(false) + expect(isValidDateOnlyString('2026-06-04T00:00:00.000Z')).to.equal(false) + expect(isValidDateOnlyString(null)).to.equal(false) + }) + }) + + context('normalizeDateOnlyInput', () => { + it('Should normalize Date and ISO date-time values to YYYY-MM-DD', () => { + expect(normalizeDateOnlyInput(new Date('2026-06-04T12:30:00.000Z'))).to.equal('2026-06-04') + expect(normalizeDateOnlyInput('2026-06-04T12:30:00.000Z')).to.equal('2026-06-04') + }) + + it('Should leave invalid dates and non-string values unchanged', () => { + expect(normalizeDateOnlyInput('2026-02-29T12:30:00.000Z')).to.equal('2026-02-29T12:30:00.000Z') + expect(normalizeDateOnlyInput(123)).to.equal(123) + }) + }) + + context('normalizeDateOnlyOutput', () => { + it('Should normalize Mongoose date strings to YYYY-MM-DD', () => { + const mongooseDate = 'Thu Jun 04 2026 00:00:00 GMT+0000 (Coordinated Universal Time)' + expect(normalizeDateOnlyOutput(mongooseDate)).to.equal('2026-06-04') + }) + }) + + context('normalizeOrgCveWebsiteUpdateDate', () => { + it('Should normalize program_data.cve_website_update_date in place', () => { + const org = { + program_data: { + cve_website_update_date: '2026-06-04T12:30:00.000Z' + } + } + + const result = normalizeOrgCveWebsiteUpdateDate(org) + + expect(result).to.equal(org) + expect(org.program_data.cve_website_update_date).to.equal('2026-06-04') + }) + + it('Should leave objects without the field unchanged', () => { + const org = { program_data: {} } + expect(normalizeOrgCveWebsiteUpdateDate(org)).to.deep.equal({ program_data: {} }) + }) + }) +}) diff --git a/test/unit-tests/utils/utilsTest.js b/test/unit-tests/utils/utilsTest.js new file mode 100644 index 000000000..e24bb73e9 --- /dev/null +++ b/test/unit-tests/utils/utilsTest.js @@ -0,0 +1,90 @@ +const { expect } = require('chai') + +const { + booleanIsTrue, + deepRemoveEmpty, + getUserFullName, + isEnrichedContainer, + toDate +} = require('../../../src/utils/utils') + +describe('Testing shared utility helpers', () => { + context('booleanIsTrue', () => { + it('Should return true for accepted true-like values', () => { + expect(booleanIsTrue('1')).to.equal(true) + expect(booleanIsTrue('true')).to.equal(true) + expect(booleanIsTrue('TRUE')).to.equal(true) + expect(booleanIsTrue('yes')).to.equal(true) + }) + + it('Should return false for other values', () => { + expect(booleanIsTrue('0')).to.equal(false) + expect(booleanIsTrue('false')).to.equal(false) + expect(booleanIsTrue('no')).to.equal(false) + }) + }) + + context('toDate', () => { + it('Should convert valid ISO timestamp strings to Date objects', () => { + const result = toDate('2026-06-04T12:30:00Z') + + expect(result).to.be.instanceOf(Date) + expect(result.toISOString()).to.equal('2026-06-04T12:30:00.000Z') + }) + + it('Should return null for invalid timestamp strings', () => { + expect(toDate('2026-13-04T12:30:00Z')).to.equal(null) + }) + }) + + context('isEnrichedContainer', () => { + it('Should return true when a container has CVSS and CWE data', () => { + const container = { + metrics: [{ cvssV3_1: {} }], + problemTypes: [{ descriptions: [{ cweId: 'CWE-79' }] }] + } + + expect(isEnrichedContainer(container)).to.equal(true) + }) + + it('Should return false when CVSS or CWE data is missing', () => { + expect(isEnrichedContainer({ metrics: [{ cvssV3_1: {} }], problemTypes: [] })).to.equal(false) + expect(isEnrichedContainer({ metrics: [], problemTypes: [{ descriptions: [{ cweId: 'CWE-79' }] }] })).to.equal(false) + }) + }) + + context('deepRemoveEmpty', () => { + it('Should remove null and empty object values without mutating the input', () => { + const input = { + keep: 'value', + removeNull: null, + nested: { + removeObject: {}, + keepNested: 'nested value' + } + } + + const result = deepRemoveEmpty(input) + + expect(result).to.deep.equal({ + keep: 'value', + nested: { + keepNested: 'nested value' + } + }) + expect(input).to.have.property('removeNull', null) + }) + }) + + context('getUserFullName', () => { + it('Should build a full name from first and last name', () => { + expect(getUserFullName({ name: { first: 'Test', last: 'User' } })).to.equal('Test User') + }) + + it('Should fall back to Unknown when name parts are missing', () => { + expect(getUserFullName({})).to.equal('Unknown User') + expect(getUserFullName({ name: { last: 'User' } })).to.equal('Unknown User') + expect(getUserFullName({ name: { first: 'Test' } })).to.equal('Test Unknown') + }) + }) +}) From a8a6172a763124abdf3900aab5399405f9d9e1ae Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Fri, 12 Jun 2026 13:15:15 -0400 Subject: [PATCH 2/5] Fix registry user CRUD test syntax --- test/integration-tests/registry-user/registryUserCRUDTest.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/integration-tests/registry-user/registryUserCRUDTest.js b/test/integration-tests/registry-user/registryUserCRUDTest.js index 38875d340..01c089c98 100644 --- a/test/integration-tests/registry-user/registryUserCRUDTest.js +++ b/test/integration-tests/registry-user/registryUserCRUDTest.js @@ -166,6 +166,8 @@ describe('Testing /registryUser endpoints', () => { .then((res) => { expect(res).to.have.status(404) }) + }) + it('Logs the updated user UUID when updating a registry user by identifier', async () => { const { createdUser } = await createRegistryUser() const loggerInfoStub = sinon.stub(logger, 'info') From ab82ed78501418770ba029704eb88076f8e3e342 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Tue, 16 Jun 2026 14:45:06 -0400 Subject: [PATCH 3/5] Fix registry org controller auth context test --- .../org/registryOrgControllerTest.js | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/test/unit-tests/org/registryOrgControllerTest.js b/test/unit-tests/org/registryOrgControllerTest.js index 9d408727f..8a66da3a1 100644 --- a/test/unit-tests/org/registryOrgControllerTest.js +++ b/test/unit-tests/org/registryOrgControllerTest.js @@ -29,8 +29,11 @@ describe('Testing Registry Org Controller', () => { context('SINGLE_ORG', () => { it('Should strip internal conversation fields for non-Secretariat users', async () => { const res = mockResponse() + const requesterOrg = { UUID: 'org-uuid', short_name: 'activity_6', authority: ['CNA'] } const orgRepo = { - findOneByShortName: sinon.stub().resolves({ UUID: 'org-uuid', short_name: 'activity_6' }), + getOrgUUID: sinon.stub().resolves(requesterOrg.UUID), + findOneByShortName: sinon.stub().resolves(requesterOrg), + findOneByUUID: sinon.stub().resolves(requesterOrg), isSecretariat: sinon.stub().resolves(false), getOrg: sinon.stub().resolves({ UUID: 'org-uuid', short_name: 'activity_6' }) } @@ -48,8 +51,12 @@ describe('Testing Registry Org Controller', () => { } const req = { ctx: { + authenticated: true, uuid: 'request-uuid', org: 'activity_6', + orgUUID: requesterOrg.UUID, + user: 'user@activity_6.com', + userUUID: 'user-uuid', params: { identifier: 'activity_6' }, repositories: { getBaseOrgRepository: () => orgRepo, @@ -73,15 +80,26 @@ describe('Testing Registry Org Controller', () => { it('Should reject non-Secretariat access to another organization', async () => { const res = mockResponse() + const requesterOrg = { UUID: 'requester-org-uuid', short_name: 'win_5', authority: ['CNA'] } const orgRepo = { - findOneByShortName: sinon.stub().resolves({ UUID: 'requester-org-uuid', short_name: 'win_5' }), + getOrgUUID: sinon.stub().callsFake((shortName) => { + if (shortName === requesterOrg.short_name) return requesterOrg.UUID + if (shortName === 'activity_6') return 'activity-org-uuid' + return null + }), + findOneByShortName: sinon.stub().resolves(requesterOrg), + findOneByUUID: sinon.stub().resolves(requesterOrg), isSecretariat: sinon.stub().resolves(false), getOrg: sinon.stub() } const req = { ctx: { + authenticated: true, uuid: 'request-uuid', org: 'win_5', + orgUUID: requesterOrg.UUID, + user: 'user@win_5.com', + userUUID: 'requester-user-uuid', params: { identifier: 'activity_6' }, repositories: { getBaseOrgRepository: () => orgRepo, From edcdd5bed4bf2351b84f7c36fed87fd303e1b832 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Tue, 16 Jun 2026 15:42:39 -0400 Subject: [PATCH 4/5] Remove unsupported registry user membership fields --- schemas/registry-user/BaseUser.json | 14 ---- .../create-registry-user-request.json | 14 ---- .../create-registry-user-response.json | 16 +--- .../get-registry-user-response.json | 16 +--- .../update-registry-user-request.json | 14 ---- .../update-registry-user-response.json | 16 +--- .../registry-user.middleware.js | 4 +- src/model/baseuser.js | 4 +- test/integration-tests/user/updateUserTest.js | 80 ------------------- test/unit-tests/user/baseUserModelTest.js | 20 ----- .../user/registryUserControllerTest.js | 12 +-- 11 files changed, 7 insertions(+), 203 deletions(-) delete mode 100644 test/unit-tests/user/baseUserModelTest.js diff --git a/schemas/registry-user/BaseUser.json b/schemas/registry-user/BaseUser.json index 935a2262c..65db8c71e 100644 --- a/schemas/registry-user/BaseUser.json +++ b/schemas/registry-user/BaseUser.json @@ -87,20 +87,6 @@ "inactive" ] }, - "org_affiliations": { - "description": "UUIDs of organizations the user is affiliated with", - "type": "array", - "items": { - "$ref": "#/definitions/uuidType" - } - }, - "cve_program_org_membership": { - "description": "UUIDs of CVE program organizations the user is a member of", - "type": "array", - "items": { - "$ref": "#/definitions/uuidType" - } - }, "role": { "description": "The user's role in the organization", "type": "string" diff --git a/schemas/registry-user/create-registry-user-request.json b/schemas/registry-user/create-registry-user-request.json index 652bf1d39..15277cb8b 100644 --- a/schemas/registry-user/create-registry-user-request.json +++ b/schemas/registry-user/create-registry-user-request.json @@ -31,20 +31,6 @@ }, "required": ["first", "last"] }, - "org_affiliations": { - "type": "array", - "items": { - "type": "string" - }, - "description": "UUIDs of organizations the user is affiliated with" - }, - "cve_program_org_membership": { - "type": "array", - "items": { - "type": "string" - }, - "description": "UUIDs of CVE program organizations the user is a member of" - }, "authority": { "type": "object", "properties": { diff --git a/schemas/registry-user/create-registry-user-response.json b/schemas/registry-user/create-registry-user-response.json index 7cc963a9c..fe783f2fc 100644 --- a/schemas/registry-user/create-registry-user-response.json +++ b/schemas/registry-user/create-registry-user-response.json @@ -42,20 +42,6 @@ }, "required": ["first", "last"] }, - "org_affiliations": { - "type": "array", - "items": { - "type": "string" - }, - "description": "UUIDs of organizations the user is affiliated with" - }, - "cve_program_org_membership": { - "type": "array", - "items": { - "type": "string" - }, - "description": "UUIDs of CVE program organizations the user is a member of" - }, "authority": { "type": "object", "properties": { @@ -115,4 +101,4 @@ } } } -} \ No newline at end of file +} diff --git a/schemas/registry-user/get-registry-user-response.json b/schemas/registry-user/get-registry-user-response.json index 7b23db936..527c6c4fe 100644 --- a/schemas/registry-user/get-registry-user-response.json +++ b/schemas/registry-user/get-registry-user-response.json @@ -43,20 +43,6 @@ "enum": ["ADMIN"], "description": "The role of the user in the organization. Currently only 'ADMIN' is supported." }, - "org_affiliations": { - "type": "array", - "items": { - "type": "string" - }, - "description": "UUIDs of organizations the user is affiliated with" - }, - "cve_program_org_membership": { - "type": "array", - "items": { - "type": "string" - }, - "description": "UUIDs of CVE program organizations the user is a member of" - }, "authority": { "type": "object", "properties": { @@ -127,4 +113,4 @@ "description": "Timestamp of the last update to the user data" } } -} \ No newline at end of file +} diff --git a/schemas/registry-user/update-registry-user-request.json b/schemas/registry-user/update-registry-user-request.json index 3a084282f..0e305f407 100644 --- a/schemas/registry-user/update-registry-user-request.json +++ b/schemas/registry-user/update-registry-user-request.json @@ -30,20 +30,6 @@ } } }, - "org_affiliations": { - "type": "array", - "items": { - "type": "string" - }, - "description": "UUIDs of organizations the user is affiliated with" - }, - "cve_program_org_membership": { - "type": "array", - "items": { - "type": "string" - }, - "description": "UUIDs of CVE program organizations the user is a member of" - }, "authority": { "type": "object", "properties": { diff --git a/schemas/registry-user/update-registry-user-response.json b/schemas/registry-user/update-registry-user-response.json index 56d6d2802..709698e65 100644 --- a/schemas/registry-user/update-registry-user-response.json +++ b/schemas/registry-user/update-registry-user-response.json @@ -42,20 +42,6 @@ }, "required": ["first", "last"] }, - "org_affiliations": { - "type": "array", - "items": { - "type": "string" - }, - "description": "UUIDs of organizations the user is affiliated with" - }, - "cve_program_org_membership": { - "type": "array", - "items": { - "type": "string" - }, - "description": "UUIDs of CVE program organizations the user is a member of" - }, "authority": { "type": "object", "properties": { @@ -115,4 +101,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/controller/registry-user.controller/registry-user.middleware.js b/src/controller/registry-user.controller/registry-user.middleware.js index e39b721c0..cffcb7e6f 100644 --- a/src/controller/registry-user.controller/registry-user.middleware.js +++ b/src/controller/registry-user.controller/registry-user.middleware.js @@ -8,9 +8,7 @@ function parsePostParams (req, res, next) { utils.reqCtxMapping(req, 'params', ['identifier', 'shortname']) utils.reqCtxMapping(req, 'query', [ 'new_username', - 'name.first', 'name.last', 'name.middle', 'name.suffix', - 'org_affiliations.add', 'org_affiliations.remove', - 'cve_program_org_membership.add', 'cve_program_org_membership.remove' + 'name.first', 'name.last', 'name.middle', 'name.suffix' ]) next() } diff --git a/src/model/baseuser.js b/src/model/baseuser.js index 775484617..fcd4b1777 100644 --- a/src/model/baseuser.js +++ b/src/model/baseuser.js @@ -26,9 +26,7 @@ const schema = { last: String, suffix: String }, - status: { type: String, enum: ['active', 'inactive'] }, - org_affiliations: [String], - cve_program_org_membership: [String] + status: { type: String, enum: ['active', 'inactive'] } } // Export BaseUser model diff --git a/test/integration-tests/user/updateUserTest.js b/test/integration-tests/user/updateUserTest.js index 7a4565a54..48bc8af19 100644 --- a/test/integration-tests/user/updateUserTest.js +++ b/test/integration-tests/user/updateUserTest.js @@ -213,54 +213,6 @@ describe('Testing Edit user endpoint', () => { expect(res.body.updated.status).to.equal('active') }) }) - it('Should update org affiliations with registry enabled', async () => { - const user = await createRegistryUserForUpdateTest('range_4') - let affiliatedOrg - await chai.request(app) - .get('/api/registry/org/win_5') - .set(constants.headers) - .then((res) => { - expect(res).to.have.status(200) - affiliatedOrg = res.body - }) - - await chai.request(app) - .put(`/api/registry/org/range_4/user/${user.username}`) - .set(constants.headers) - .send({ - ...user, - org_affiliations: [affiliatedOrg.UUID] - }) - .then((res, err) => { - expect(err).to.be.undefined - expect(res).to.have.status(200) - expect(res.body.updated.org_affiliations).to.deep.equal([affiliatedOrg.UUID]) - }) - }) - it('Should update CVE program org membership with registry enabled', async () => { - const user = await createRegistryUserForUpdateTest('range_4') - let memberOrg - await chai.request(app) - .get('/api/registry/org/range_4') - .set(constants.headers) - .then((res) => { - expect(res).to.have.status(200) - memberOrg = res.body - }) - - await chai.request(app) - .put(`/api/registry/org/range_4/user/${user.username}`) - .set(constants.headers) - .send({ - ...user, - cve_program_org_membership: [memberOrg.UUID] - }) - .then((res, err) => { - expect(err).to.be.undefined - expect(res).to.have.status(200) - expect(res.body.updated.cve_program_org_membership).to.deep.equal([memberOrg.UUID]) - }) - }) }) context('Negative Tests', () => { it('Should return an error when admin is required', async () => { @@ -289,38 +241,6 @@ describe('Testing Edit user endpoint', () => { expect(res.body.error).to.contain('NOT_ALLOWED_TO_CHANGE_FIELD') }) }) - it('Should return an error when a regular user attempts to update org affiliations with registry enabled', async () => { - let user - await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body }) - await chai.request(app) - .put('/api/registry/org/win_5/user/jasminesmith@win_5.com') - .set(constants.nonSecretariatUserHeaders) - .send({ - ...user, - org_affiliations: ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] - }) - .then((res, err) => { - expect(err).to.be.undefined - expect(res).to.have.status(400) - expect(res.body.error).to.contain('NOT_ALLOWED_TO_CHANGE_FIELD') - }) - }) - it('Should return an error when a regular user attempts to update CVE program org membership with registry enabled', async () => { - let user - await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body }) - await chai.request(app) - .put('/api/registry/org/win_5/user/jasminesmith@win_5.com') - .set(constants.nonSecretariatUserHeaders) - .send({ - ...user, - cve_program_org_membership: ['7f593536-7cbc-46fd-bdd9-b6176c9cd93f'] - }) - .then((res, err) => { - expect(err).to.be.undefined - expect(res).to.have.status(400) - expect(res.body.error).to.contain('NOT_ALLOWED_TO_CHANGE_FIELD') - }) - }) it('Should not allow a first name of more than 100 characters', async () => { await chai.request(app) .put('/api/org/win_5/user/jasminesmith@win_5.com?name.first=1:1234567,2:1234567,3:1234567,4:1234567,5:1234567,6:1234567,7:1234567,8:1234567,9:1234567,10:1234567,11:1234567') diff --git a/test/unit-tests/user/baseUserModelTest.js b/test/unit-tests/user/baseUserModelTest.js deleted file mode 100644 index 983fa6edc..000000000 --- a/test/unit-tests/user/baseUserModelTest.js +++ /dev/null @@ -1,20 +0,0 @@ -const { expect } = require('chai') - -const BaseUser = require('../../../src/model/baseuser') - -describe('Testing BaseUser Model', () => { - it('Should validate and model registry user organization membership fields', () => { - const orgAffiliationUUID = '7f593536-7cbc-46fd-bdd9-b6176c9cd93f' - const cveProgramMembershipUUID = 'dd5389e6-7cbc-46fd-bdd9-b6176c9cd93f' - - const result = BaseUser.validateUser({ - username: 'registry_user@example.org', - org_affiliations: [orgAffiliationUUID], - cve_program_org_membership: [cveProgramMembershipUUID] - }) - - expect(result.isValid).to.equal(true) - expect(BaseUser.schema.path('org_affiliations')).to.not.equal(undefined) - expect(BaseUser.schema.path('cve_program_org_membership')).to.not.equal(undefined) - }) -}) diff --git a/test/unit-tests/user/registryUserControllerTest.js b/test/unit-tests/user/registryUserControllerTest.js index 6a173689f..1ceeba8a9 100644 --- a/test/unit-tests/user/registryUserControllerTest.js +++ b/test/unit-tests/user/registryUserControllerTest.js @@ -220,8 +220,6 @@ describe('Testing Registry User Controller', () => { const res = mockResponse() const userUUID = 'd41d8cd9-8f00-4204-a980-0998ecf8427e' const orgUUID = '405450a6-8f00-4204-a980-0998ecf8427e' - const affiliatedOrgUUID = '7f593536-7cbc-46fd-bdd9-b6176c9cd93f' - const memberOrgUUID = 'dd5389e6-7cbc-46fd-bdd9-b6176c9cd93f' const userToEdit = { UUID: userUUID, username: 'created_user@example.org', @@ -241,17 +239,13 @@ describe('Testing Registry User Controller', () => { }, status: 'active', created: '2026-01-01T00:00:00.000Z', - last_updated: '2026-01-02T00:00:00.000Z', - org_affiliations: [affiliatedOrgUUID], - cve_program_org_membership: [memberOrgUUID] + last_updated: '2026-01-02T00:00:00.000Z' } const updatedUser = { UUID: userUUID, username: 'created_user@example.org', name: body.name, - status: 'active', - org_affiliations: [affiliatedOrgUUID], - cve_program_org_membership: [memberOrgUUID] + status: 'active' } const userRepo = { isAdmin: sinon.stub().resolves(false), @@ -285,8 +279,6 @@ describe('Testing Registry User Controller', () => { const validatedBody = userRepo.validateUser.firstCall.args[0] expect(validatedBody).to.not.have.property('created') expect(validatedBody).to.not.have.property('last_updated') - expect(validatedBody.org_affiliations).to.deep.equal([affiliatedOrgUUID]) - expect(validatedBody.cve_program_org_membership).to.deep.equal([memberOrgUUID]) expect(userRepo.updateUserFull.firstCall.args[1]).to.equal(validatedBody) expect(res.json.firstCall.args[0].updated).to.deep.equal(updatedUser) }) From e1418bcc5ea5eac3311dd215558e6c1cb01aff34 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Tue, 16 Jun 2026 15:47:27 -0400 Subject: [PATCH 5/5] Remove unused registry user update helper --- test/integration-tests/user/updateUserTest.js | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/test/integration-tests/user/updateUserTest.js b/test/integration-tests/user/updateUserTest.js index 48bc8af19..24cd8cb19 100644 --- a/test/integration-tests/user/updateUserTest.js +++ b/test/integration-tests/user/updateUserTest.js @@ -4,40 +4,10 @@ const chai = require('chai') chai.use(require('chai-http')) const expect = chai.expect -const { v4: uuidv4 } = require('uuid') const constants = require('../constants.js') const app = require('../../../src/index.js') -async function createRegistryUserForUpdateTest (orgShortName) { - const username = `${uuidv4()}@update-user.test` - await chai.request(app) - .post(`/api/registry/org/${orgShortName}/user`) - .set(constants.headers) - .send({ - username, - name: { - first: 'Update', - last: 'User' - }, - status: 'active' - }) - .then((res) => { - expect(res).to.have.status(200) - }) - - let user - await chai.request(app) - .get(`/api/registry/org/${orgShortName}/user/${username}`) - .set(constants.headers) - .then((res) => { - expect(res).to.have.status(200) - user = res.body - }) - - return user -} - describe('Testing Edit user endpoint', () => { context('Positive Tests', () => { it('Should correctly remove an admin from the original organization admins array when migrated to a new organization', async () => {