diff --git a/api-docs/openapi.json b/api-docs/openapi.json index efd583b28..71f81b07d 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -4267,6 +4267,218 @@ } } }, + "/registry/org/{shortname}/oversees/{shortname2}/add": { + "post": { + "tags": [ + "Registry Organization" + ], + "summary": "Assigns an organization to report to a ROOT organization (accessible to Secretariat only)", + "description": "

Access Control

User must belong to an organization with the Secretariat role.

Expected Behavior

The organization identified by shortname must have ROOT authority. The organization identified by shortname2 will report to it. If shortname2 already reported to a different ROOT. It will be removed from that and both will receive an audit entry.

", + "operationId": "registryOrgAddOverseeRelationship", + "parameters": [ + { + "name": "shortname", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The shortname of the overseeing ROOT organization" + }, + { + "name": "shortname2", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The shortname of the reporting organization" + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "Reports-to relationship added successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/bad-request.json" + } + } + } + }, + "401": { + "description": "Not Authenticated", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + } + } + } + }, + "/registry/org/{shortname}/oversees/{shortname2}/remove": { + "post": { + "tags": [ + "Registry Organization" + ], + "summary": "Removes an organization reports-to relationship (accessible to Secretariat only)", + "description": "

Access Control

User must belong to an organization with the Secretariat role.

Expected Behavior

The organization identified by shortname must have ROOT authority. The organization identified by shortname2 will no longer report to it.

", + "operationId": "registryOrgRemoveOverseeRelationship", + "parameters": [ + { + "name": "shortname", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The shortname of the overseeing ROOT organization" + }, + { + "name": "shortname2", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The shortname of the reporting organization" + }, + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "Reports-to relationship removed successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/bad-request.json" + } + } + } + }, + "401": { + "description": "Not Authenticated", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + } + } + } + }, "/registry/org/{shortname}/user/{username}/grant-role": { "post": { "tags": [ diff --git a/src/controller/registry.controller/index.js b/src/controller/registry.controller/index.js index 36874272e..2735f8cdf 100644 --- a/src/controller/registry.controller/index.js +++ b/src/controller/registry.controller/index.js @@ -1090,6 +1090,77 @@ router.put('/registry/org/:shortname/user/:username/reset_secret', controller.USER_RESET_SECRET ) +router.post('/registry/org/:shortname/oversees/:shortname2/add', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'registryOrgAddOverseeRelationship' + #swagger.summary = 'Assigns an organization to report to a ROOT organization (accessible to Secretariat only)' + #swagger.description = ' +

Access Control

+

User must belong to an organization with the Secretariat role.

+

Expected Behavior

+

The organization identified by shortname must have ROOT authority. The organization identified by shortname2 will + report to it. If shortname2 already reported to a different ROOT. It will be removed from that and both will receive an audit entry.

' + #swagger.parameters['shortname'] = { description: 'The shortname of the overseeing ROOT organization' } + #swagger.parameters['shortname2'] = { description: 'The shortname of the reporting organization' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Reports-to relationship added successfully', + content: { 'application/json': { schema: { type: 'object', properties: { message: { type: 'string' } } } } } + } + #swagger.responses[400] = { description: 'Bad Request', content: { 'application/json': { schema: { $ref: '../schemas/errors/bad-request.json' } } } } + #swagger.responses[401] = { description: 'Not Authenticated', content: { 'application/json': { schema: { $ref: '../schemas/errors/generic.json' } } } } + #swagger.responses[403] = { description: 'Forbidden', content: { 'application/json': { schema: { $ref: '../schemas/errors/generic.json' } } } } + #swagger.responses[404] = { description: 'Not Found', content: { 'application/json': { schema: { $ref: '../schemas/errors/generic.json' } } } } + #swagger.responses[500] = { description: 'Internal Server Error', content: { 'application/json': { schema: { $ref: '../schemas/errors/generic.json' } } } } + */ + mw.useRegistry(), + mw.validateUser, + mw.onlySecretariat, + parseError, + parsePostParams, + registryOrgController.ADD_OVERSEE_RELATIONSHIP +) + +router.post('/registry/org/:shortname/oversees/:shortname2/remove', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'registryOrgRemoveOverseeRelationship' + #swagger.summary = 'Removes an organization reports-to relationship (accessible to Secretariat only)' + #swagger.description = ' +

Access Control

+

User must belong to an organization with the Secretariat role.

+

Expected Behavior

+

The organization identified by shortname must have ROOT authority. The organization identified by shortname2 will no longer report to it.

' + #swagger.parameters['shortname'] = { description: 'The shortname of the overseeing ROOT organization' } + #swagger.parameters['shortname2'] = { description: 'The shortname of the reporting organization' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Reports-to relationship removed successfully', + content: { 'application/json': { schema: { type: 'object', properties: { message: { type: 'string' } } } } } + } + #swagger.responses[400] = { description: 'Bad Request', content: { 'application/json': { schema: { $ref: '../schemas/errors/bad-request.json' } } } } + #swagger.responses[401] = { description: 'Not Authenticated', content: { 'application/json': { schema: { $ref: '../schemas/errors/generic.json' } } } } + #swagger.responses[403] = { description: 'Forbidden', content: { 'application/json': { schema: { $ref: '../schemas/errors/generic.json' } } } } + #swagger.responses[404] = { description: 'Not Found', content: { 'application/json': { schema: { $ref: '../schemas/errors/generic.json' } } } } + #swagger.responses[500] = { description: 'Internal Server Error', content: { 'application/json': { schema: { $ref: '../schemas/errors/generic.json' } } } } + */ + mw.useRegistry(), + mw.validateUser, + mw.onlySecretariat, + parseError, + parsePostParams, + registryOrgController.REMOVE_OVERSEE_RELATIONSHIP +) + router.post('/registry/org/:shortname/user/:username/grant-role', /* #swagger.tags = ['Registry User'] diff --git a/src/controller/registry.controller/org.error.js b/src/controller/registry.controller/org.error.js index c47c26a9f..c48ec8a77 100644 --- a/src/controller/registry.controller/org.error.js +++ b/src/controller/registry.controller/org.error.js @@ -141,6 +141,13 @@ class RegistryOrgControllerError extends idrErr.IDRError { err.message = `The following fields can only be modified by the Secretariat: ${fields.join(', ')}.` return err } + + overseeingOrgMustBeRoot (shortname) { + const err = {} + err.error = 'OVERSEEING_ORG_MUST_BE_ROOT' + err.message = `The '${shortname}' organization must have ROOT authority to oversee another organization.` + return err + } } module.exports = { diff --git a/src/controller/registry.controller/org.registry.controller.js b/src/controller/registry.controller/org.registry.controller.js index deb83fc3c..f4705e71e 100644 --- a/src/controller/registry.controller/org.registry.controller.js +++ b/src/controller/registry.controller/org.registry.controller.js @@ -420,6 +420,9 @@ async function updateOrg (req, res, next) { const userRepo = req.ctx.repositories.getBaseUserRepository() const conversationRepo = req.ctx.repositories.getConversationRepository() const { conversation, ...body } = req.ctx.body + // oversees is managed exclusively by the dedicated reports-to endpoints. + // Ignore it during standard organization updates. + delete body.oversees let updatedOrg let jointApprovalRequired @@ -549,7 +552,15 @@ async function updateOrg (req, res, next) { // Update Org full will cause a write to the Conversations collection, to avoid a read-after-write issue, we need to get the previous conversation data first const previousConversation = await conversationRepo.getAllByTargetUUID(await repo.getOrgUUID(shortName, { session }), isSecretariat, { session }) || [] - updatedOrg = await repo.updateOrgFull(shortName, req.ctx.body, { session }, false, requestingUser.UUID, isAdmin, isSecretariat) + updatedOrg = await repo.updateOrgFull( + shortName, + conversation ? { ...body, conversation } : body, + { session }, + false, + requestingUser.UUID, + isAdmin, + isSecretariat + ) jointApprovalRequired = _.get(updatedOrg, 'joint_approval_required', false) _.unset(updatedOrg, 'joint_approval_required') // append previous conversations to any conversations that are in the org already @@ -659,6 +670,75 @@ async function deleteOrg (req, res, next) { } } +/** + * Adds or removes a reports-to relationship for a ROOT organization. + * + * @param {object} req - The Express request object. + * @param {object} res - The Express response object. + * @param {function} next - The Express next function. + * @param {'add'|'remove'} action - The relationship operation to perform. + * @returns {Promise} A promise that resolves when the response is sent. + */ +async function updateOverseeRelationship (req, res, next, action) { + try { + const session = await mongoose.startSession({ causalConsistency: false }) + const overseeingOrgShortName = req.ctx.params.shortname + const reportingOrgShortName = req.params.shortname2 + const orgRepo = req.ctx.repositories.getBaseOrgRepository() + const userRepo = req.ctx.repositories.getBaseUserRepository() + + try { + session.startTransaction({ readPreference: 'primary' }) + const overseeingOrg = await orgRepo.findOneByShortName(overseeingOrgShortName, { session }) + if (!overseeingOrg) { + await session.abortTransaction() + return res.status(404).json(error.orgDnePathParam(overseeingOrgShortName)) + } + + if (!overseeingOrg.authority?.includes('ROOT')) { + await session.abortTransaction() + return res.status(400).json(error.overseeingOrgMustBeRoot(overseeingOrgShortName)) + } + + const reportingOrg = await orgRepo.findOneByShortName(reportingOrgShortName, { session }) + if (!reportingOrg) { + await session.abortTransaction() + return res.status(404).json(error.orgDnePathParam(reportingOrgShortName)) + } + + const requestingUserUUID = await authContext.getRequesterUserUUID(req, userRepo, orgRepo, { session }) + if (action === 'add') { + await orgRepo.addOverseeRelationship(overseeingOrgShortName, reportingOrg.UUID, { session }, requestingUserUUID) + } else { + await orgRepo.removeOverseeRelationship(overseeingOrgShortName, reportingOrg.UUID, { session }, requestingUserUUID) + } + + await session.commitTransaction() + } catch (updateErr) { + await session.abortTransaction() + throw updateErr + } finally { + await session.endSession() + } + + const message = action === 'add' + ? `${reportingOrgShortName} organization now reports to ${overseeingOrgShortName}.` + : `${reportingOrgShortName} organization no longer reports to ${overseeingOrgShortName}.` + logger.info({ uuid: req.ctx.uuid, message }) + return res.status(200).json({ message }) + } catch (err) { + next(err) + } +} + +async function addOverseeRelationship (req, res, next) { + return updateOverseeRelationship(req, res, next, 'add') +} + +async function removeOverseeRelationship (req, res, next) { + return updateOverseeRelationship(req, res, next, 'remove') +} + /** * Retrieves all users for the organization with the specified short name. * @@ -928,6 +1008,8 @@ module.exports = { CREATE_ORG: createOrg, UPDATE_ORG: updateOrg, DELETE_ORG: deleteOrg, + ADD_OVERSEE_RELATIONSHIP: addOverseeRelationship, + REMOVE_OVERSEE_RELATIONSHIP: removeOverseeRelationship, USER_ALL: getUsers, USER_CREATE_SINGLE: createUserByOrg, EDIT_CONVERSATION: editConversationForOrg diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index 5591e8e96..515c72280 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -462,6 +462,63 @@ class BaseOrgRepository extends BaseRepository { return updatedOrg } + /** + * @async + * @function addOverseeRelationship + * @description Assigns a reporting organization to a ROOT organization. A reporting organization can have only one overseeing organization. + * @param {string} overseeingOrgShortName - The short name of the ROOT organization. + * @param {string} reportingOrgUUID - The UUID of the organization that reports to the ROOT organization. + * @param {object} [options={}] - Optional settings for the repository query. + * @param {string|null} [requestingUserUUID=null] - The requester UUID used for audit documentation. + * @returns {Promise} The updated ROOT organization. + */ + async addOverseeRelationship (overseeingOrgShortName, reportingOrgUUID, options = {}, requestingUserUUID = null) { + const overseeingOrg = await RootOrgModel.findOne({ short_name: overseeingOrgShortName }, null, options) + const originalOverseeingOrg = overseeingOrg.toObject() + const previousOverseeingOrgs = await RootOrgModel.find({ oversees: reportingOrgUUID }, null, options) + + for (const previousOverseeingOrg of previousOverseeingOrgs) { + if (previousOverseeingOrg.UUID === overseeingOrg.UUID) continue + const originalPreviousOverseeingOrg = previousOverseeingOrg.toObject() + previousOverseeingOrg.oversees = previousOverseeingOrg.oversees.filter(uuid => uuid !== reportingOrgUUID) + if (requestingUserUUID) { + await createAuditLogEntry(previousOverseeingOrg, originalPreviousOverseeingOrg, requestingUserUUID, options) + } + await previousOverseeingOrg.save(options) + } + + overseeingOrg.oversees = [...new Set([...(overseeingOrg.oversees || []), reportingOrgUUID])] + if (requestingUserUUID) { + await createAuditLogEntry(overseeingOrg, originalOverseeingOrg, requestingUserUUID, options) + } + await overseeingOrg.save(options) + + return overseeingOrg + } + + /** + * @async + * @function removeOverseeRelationship + * @description Removes a reporting organization from a ROOT organization's oversee list. + * @param {string} overseeingOrgShortName - The short name of the ROOT organization. + * @param {string} reportingOrgUUID - The UUID of the organization that no longer reports to the ROOT organization. + * @param {object} [options={}] - Optional settings for the repository query. + * @param {string|null} [requestingUserUUID=null] - The requester UUID used for audit documentation. + * @returns {Promise} The updated ROOT organization. + */ + async removeOverseeRelationship (overseeingOrgShortName, reportingOrgUUID, options = {}, requestingUserUUID = null) { + const overseeingOrg = await RootOrgModel.findOne({ short_name: overseeingOrgShortName }, null, options) + const originalOverseeingOrg = overseeingOrg.toObject() + overseeingOrg.oversees = (overseeingOrg.oversees || []).filter(uuid => uuid !== reportingOrgUUID) + + if (requestingUserUUID) { + await createAuditLogEntry(overseeingOrg, originalOverseeingOrg, requestingUserUUID, options) + } + await overseeingOrg.save(options) + + return overseeingOrg + } + /** * @async * @function getAllOrgs diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index 46fb6286c..8b0cf5320 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -871,7 +871,7 @@ describe('Testing /registry/org endpoints', () => { .delete('/api/registry/org/temp_org_updated_name') .set(secretariatHeaders) }) - it('Updates a registry organization to oversee another, and verifies the sub-org dynamically returns reports_to', async () => { + it('Does not update oversees through the standard registry organization PUT endpoint', async () => { // Create a sub org const subOrg = { short_name: 'sub_org_test', @@ -889,7 +889,7 @@ describe('Testing /registry/org endpoints', () => { createdSubOrgUUID = res.body.created.UUID }) - // Update the main org to oversee it + // A standard PUT must ignore oversees; reports-to relationships use dedicated endpoints. await chai.request(app) .put(`/api/registry/org/${createdOrg.short_name}`) .set(secretariatHeaders) @@ -899,36 +899,20 @@ describe('Testing /registry/org endpoints', () => { }) .then(res => { expect(res).to.have.status(200) - expect(res.body.updated.oversees).to.be.an('array').that.includes(createdSubOrgUUID) + expect(res.body.updated.oversees || []).to.not.include(createdSubOrgUUID) }) const BaseOrg = require('../../../src/model/baseorg') const registryOrgCheck = await BaseOrg.findOne({ short_name: createdOrg.short_name }) - expect(registryOrgCheck.oversees).to.be.an('array').that.includes(createdSubOrgUUID) + expect(registryOrgCheck.oversees || []).to.not.include(createdSubOrgUUID) - // Assert that the sub org dynamically returns reports_to matching the main org's UUID + // The reporting org must not acquire a computed reports_to relationship. await chai.request(app) .get(`/api/registry/org/${subOrg.short_name}`) .set(secretariatHeaders) .then(res => { expect(res).to.have.status(200) - expect(res.body).to.have.property('reports_to', createdOrg.UUID) - expect(res.body).to.have.property('_relatedOrganizations') - expect(res.body._relatedOrganizations).to.be.an('array').that.has.lengthOf(1) - expect(res.body._relatedOrganizations[0].UUID).to.equal(createdOrg.UUID) - expect(res.body._relatedOrganizations[0].short_name).to.equal(createdOrg.short_name) - }) - - // Assert that the main org also has _relatedOrganizations for the sub org it oversees - await chai.request(app) - .get(`/api/registry/org/${createdOrg.short_name}`) - .set(secretariatHeaders) - .then(res => { - expect(res).to.have.status(200) - expect(res.body).to.have.property('_relatedOrganizations') - expect(res.body._relatedOrganizations).to.be.an('array').that.has.lengthOf(1) - expect(res.body._relatedOrganizations[0].UUID).to.equal(createdSubOrgUUID) - expect(res.body._relatedOrganizations[0].short_name).to.equal(subOrg.short_name) + expect(res.body).to.not.have.property('reports_to') }) // Cleanup sub org diff --git a/test/integration-tests/registry-org/rootOrgTest.js b/test/integration-tests/registry-org/rootOrgTest.js index 689a0af81..18c4bd7e1 100644 --- a/test/integration-tests/registry-org/rootOrgTest.js +++ b/test/integration-tests/registry-org/rootOrgTest.js @@ -16,6 +16,8 @@ const testRootOrg = { authority: ['ROOT'] } let createdOrg +let reportingOrg +let secondRootOrg describe('Testing ROOT Organization Type', () => { context('Creating a ROOT org', () => { @@ -55,6 +57,134 @@ describe('Testing ROOT Organization Type', () => { }) }) + context('Managing reports-to relationships', () => { + before(async () => { + await chai.request(app) + .post('/api/registry/org') + .set(secretariatHeaders) + .send({ + short_name: 'reporting_org_for_root', + long_name: 'Reporting Organization', + authority: ['CNA'], + id_quota: 100 + }) + .then((res) => { + expect(res).to.have.status(200) + reportingOrg = res.body.created + }) + + await chai.request(app) + .post('/api/registry/org') + .set(secretariatHeaders) + .send({ + short_name: 'second_root_org_test', + long_name: 'Second Root Organization', + authority: ['ROOT'] + }) + .then((res) => { + expect(res).to.have.status(200) + secondRootOrg = res.body.created + }) + }) + + it('Secretariat can assign an organization to report to a ROOT organization', async () => { + await chai.request(app) + .post(`/api/registry/org/${testRootOrg.short_name}/oversees/${reportingOrg.short_name}/add`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.message).to.equal(`${reportingOrg.short_name} organization now reports to ${testRootOrg.short_name}.`) + }) + + await chai.request(app) + .get(`/api/registry/org/${reportingOrg.short_name}`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.reports_to).to.equal(createdOrg.UUID) + }) + + await chai.request(app) + .get(`/api/registry/org/${testRootOrg.short_name}`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.oversees).to.include(reportingOrg.UUID) + }) + }) + + it('Assigning a new ROOT parent removes the previous reports-to relationship', async () => { + await chai.request(app) + .post(`/api/registry/org/${secondRootOrg.short_name}/oversees/${reportingOrg.short_name}/add`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + }) + + await chai.request(app) + .get(`/api/registry/org/${testRootOrg.short_name}`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.oversees || []).to.not.include(reportingOrg.UUID) + }) + + await chai.request(app) + .get(`/api/registry/org/${reportingOrg.short_name}`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.reports_to).to.equal(secondRootOrg.UUID) + }) + }) + + it('Secretariat can remove a reports-to relationship', async () => { + await chai.request(app) + .post(`/api/registry/org/${secondRootOrg.short_name}/oversees/${reportingOrg.short_name}/remove`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.message).to.equal(`${reportingOrg.short_name} organization no longer reports to ${secondRootOrg.short_name}.`) + }) + + await chai.request(app) + .get(`/api/registry/org/${reportingOrg.short_name}`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body).to.not.have.property('reports_to') + }) + }) + + it('Rejects a non-ROOT overseeing organization', async () => { + await chai.request(app) + .post(`/api/registry/org/${reportingOrg.short_name}/oversees/${testRootOrg.short_name}/add`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.error).to.equal('OVERSEEING_ORG_MUST_BE_ROOT') + }) + }) + + it('Returns not found when either organization does not exist', async () => { + await chai.request(app) + .post(`/api/registry/org/missing_root/oversees/${reportingOrg.short_name}/add`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(404) + expect(res.body.error).to.equal('ORG_DNE_PARAM') + }) + + await chai.request(app) + .post(`/api/registry/org/${testRootOrg.short_name}/oversees/missing_reporting_org/add`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(404) + expect(res.body.error).to.equal('ORG_DNE_PARAM') + }) + }) + }) + context('ROOT admin permissions', () => { before(async () => { // Create a Root Admin user @@ -108,17 +238,26 @@ describe('Testing ROOT Organization Type', () => { }) }) - it('ROOT admin cannot edit oversees', async () => { + it('ROOT admin cannot set oversees through the standard PUT endpoint', async () => { await chai.request(app) .put(`/api/registry/org/${testRootOrg.short_name}`) .set(rootAdminHeaders) .send({ ...createdOrg, - oversees: ['some_other_uuid'] + oversees: [reportingOrg.UUID] + }) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.updated.oversees || []).to.not.include(reportingOrg.UUID) }) + }) + + it('ROOT admin cannot manage reports-to relationships', async () => { + await chai.request(app) + .post(`/api/registry/org/${testRootOrg.short_name}/oversees/${reportingOrg.short_name}/add`) + .set(rootAdminHeaders) .then((res) => { expect(res).to.have.status(403) - expect(res.body.error).to.equal('SECRETARIAT_ONLY') }) })