diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 84fe59bcf..d4c33d370 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -11,7 +11,7 @@ }, "servers": [ { - "url": "https://cveawg-dev.mitre.org/api" + "url": "urlplaceholder" } ], "paths": { @@ -3199,6 +3199,79 @@ } } }, + "/registry/org/cnas": { + "get": { + "tags": [ + "Registry Organization" + ], + "summary": "Lists active CNAs in the CVE.org partner-list format (Secretariat only)", + "description": "

Access Control

User must belong to an organization with the Secretariat role.

Expected Behavior

Secretariat: Retrieves the active CNA list built from registry organization data in the CVE.org partner-list format.

", + "operationId": "registryOrgActiveCnas", + "parameters": [ + { + "$ref": "#/components/parameters/apiEntityHeader" + }, + { + "$ref": "#/components/parameters/apiUserHeader" + }, + { + "$ref": "#/components/parameters/apiSecretHeader" + } + ], + "responses": { + "200": { + "description": "Returns active CNAs in the CVE.org partner-list format", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/registry-org/list-active-cnas-response.json" + } + } + } + }, + "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" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/errors/generic.json" + } + } + } + } + } + } + }, "/registry/org/{shortname}/users": { "get": { "tags": [ diff --git a/schemas/registry-org/list-active-cnas-response.json b/schemas/registry-org/list-active-cnas-response.json new file mode 100644 index 000000000..ffd862fff --- /dev/null +++ b/schemas/registry-org/list-active-cnas-response.json @@ -0,0 +1,98 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "link": { + "type": "object", + "required": ["label", "url"], + "properties": { + "label": { "type": "string" }, + "url": { "type": "string" } + }, + "additionalProperties": false + }, + "policyLink": { + "type": "object", + "required": ["label", "language", "url"], + "properties": { + "label": { "type": "string" }, + "language": { "type": "string" }, + "url": { "type": "string" } + }, + "additionalProperties": false + }, + "email": { + "type": "object", + "required": ["label", "emailAddr"], + "properties": { + "label": { "type": "string" }, + "emailAddr": { "type": "string" } + }, + "additionalProperties": false + }, + "contact": { + "type": "object", + "required": ["email", "contact", "form"], + "properties": { + "email": { "type": "array", "items": { "$ref": "#/definitions/email" } }, + "contact": { "type": "array", "items": { "$ref": "#/definitions/link" } }, + "form": { "type": "array", "items": { "$ref": "#/definitions/link" } } + }, + "additionalProperties": false + }, + "orgReference": { + "type": "object", + "required": ["shortName", "organizationName"], + "properties": { + "shortName": { "type": "string" }, + "organizationName": { "type": "string" } + }, + "additionalProperties": false + }, + "role": { + "type": "object", + "required": ["helpText", "role"], + "properties": { + "helpText": { "type": "string" }, + "role": { "type": "string" } + }, + "additionalProperties": false + } + }, + "type": "array", + "items": { + "type": "object", + "required": ["shortName", "cnaID", "organizationName", "scope", "contact", "disclosurePolicy", "securityAdvisories", "resources", "CNA", "country"], + "properties": { + "shortName": { "type": "string" }, + "cnaID": { "type": "string" }, + "organizationName": { "type": "string" }, + "scope": { "type": "string" }, + "contact": { "type": "array", "items": { "$ref": "#/definitions/contact" } }, + "disclosurePolicy": { "type": "array", "items": { "$ref": "#/definitions/policyLink" } }, + "securityAdvisories": { + "type": "object", + "required": ["alerts", "advisories"], + "properties": { + "alerts": { "type": "array", "items": { "$ref": "#/definitions/link" } }, + "advisories": { "type": "array", "items": { "$ref": "#/definitions/link" } } + }, + "additionalProperties": false + }, + "resources": { "type": "array", "items": { "$ref": "#/definitions/link" } }, + "CNA": { + "type": "object", + "required": ["isRoot", "root", "type", "TLR", "roles"], + "properties": { + "isRoot": { "type": "boolean" }, + "root": { "$ref": "#/definitions/orgReference" }, + "type": { "type": "array", "items": { "type": "string" } }, + "TLR": { "$ref": "#/definitions/orgReference" }, + "roles": { "type": "array", "items": { "$ref": "#/definitions/role" } } + }, + "additionalProperties": false + }, + "country": { "type": "string" } + }, + "additionalProperties": false + } +} diff --git a/src/controller/registry.controller/index.js b/src/controller/registry.controller/index.js index 2735f8cdf..6a8eb9832 100644 --- a/src/controller/registry.controller/index.js +++ b/src/controller/registry.controller/index.js @@ -92,6 +92,64 @@ router.get('/registry/org', registryOrgController.ALL_ORGS ) +router.get('/registry/org/cnas', + /* + #swagger.tags = ['Registry Organization'] + #swagger.operationId = 'registryOrgActiveCnas' + #swagger.summary = 'Lists active CNAs in the CVE.org partner-list format (Secretariat only)' + #swagger.description = '

Access Control

User must belong to an organization with the Secretariat role.

Expected Behavior

Secretariat: Retrieves the active CNA list built from registry organization data in the CVE.org partner-list format.

' + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Returns active CNAs in the CVE.org partner-list format', + content: { + 'application/json': { + schema: { $ref: '../schemas/registry-org/list-active-cnas-response.json' } + } + } + } + #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[500] = { + description: 'Internal Server Error', + content: { + 'application/json': { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.useRegistry(), + mw.validateUser, + mw.onlySecretariat, + registryOrgController.ACTIVE_CNAS +) + router.get('/registry/org/:shortname/users', /* #swagger.tags = ['Registry User'] diff --git a/src/controller/registry.controller/org.registry.controller.js b/src/controller/registry.controller/org.registry.controller.js index 56ff62871..2d54e9356 100644 --- a/src/controller/registry.controller/org.registry.controller.js +++ b/src/controller/registry.controller/org.registry.controller.js @@ -150,6 +150,74 @@ async function getAllOrgs (req, res, next) { } } +function asUrlEntries (urls, label) { + return (urls || []).filter(Boolean).map(url => ({ label, url: url.trim() })) +} + +function asPublicOrgReference (org) { + return org + ? { shortName: org.short_name || 'n/a', organizationName: org.long_name || 'n/a' } + : { shortName: 'n/a', organizationName: 'n/a' } +} + +function mapAuthorityRoles (authority, isRoot, isTopLevelRoot) { + const roles = [] + if (isRoot) roles.push({ helpText: '', role: isTopLevelRoot ? 'Top-Level Root' : 'Root' }) + if (authority.includes('CNA')) roles.push({ helpText: '', role: isRoot ? 'CNA-LR' : 'CNA' }) + if (authority.includes('ADP')) roles.push({ helpText: '', role: 'ADP' }) + if (authority.includes('SECRETARIAT')) roles.push({ helpText: '', role: 'Secretariat' }) + return roles.length ? roles : [{ helpText: '', role: 'CNA' }] +} + +function mapActiveCnaToPublicFormat (org) { + const emails = (org.contact_info?.emails || []).filter(Boolean).map(emailAddr => ({ label: 'Email', emailAddr })) + const contacts = asUrlEntries(org.contact_info?.websites, 'Website') + const authority = org.authority || [] + const isRoot = org.__t === 'RootOrg' || authority.includes('ROOT') + const isTopLevelRoot = isRoot && String(org.top_level_root).toLowerCase() === 'true' + + return { + shortName: org.short_name || '', + cnaID: org.partner_number || '', + organizationName: org.long_name || '', + scope: org.charter_or_scope || '', + contact: [{ email: emails, contact: contacts, form: [] }], + disclosurePolicy: asUrlEntries((org.disclosure_policy || '').split(';'), 'Policy').map(policy => ({ ...policy, language: '' })), + securityAdvisories: { alerts: [], advisories: asUrlEntries(org.advisory_locations, 'Advisories') }, + resources: [], + CNA: { + isRoot, + root: isRoot ? asPublicOrgReference() : asPublicOrgReference(org._root), + type: org.partner_role_type || [], + TLR: isTopLevelRoot ? asPublicOrgReference() : asPublicOrgReference(org._tlr), + roles: mapAuthorityRoles(authority, isRoot, isTopLevelRoot) + }, + country: org.partner_country || '' + } +} + +/** + * Retrieves active CNA partners in the CVE.org partner-list response format. + * + * @async + * @function getActiveCnas + * @param {object} req - The Express request object. + * @param {object} res - The Express response object. + * @param {function} next - The next middleware function. + * @returns {Promise} - A promise that resolves when the response is sent. + * @description This endpoint is restricted to Secretariat users and reads active CNA data from the registry database. + * Called by GET /api/registry/org/cnas + */ +async function getActiveCnas (req, res, next) { + try { + const repo = req.ctx.repositories.getBaseOrgRepository() + const activeCnas = await repo.getActiveCnas() + return res.status(200).json(activeCnas.map(mapActiveCnaToPublicFormat)) + } catch (err) { + next(err) + } +} + /** * Retrieves information about a specific registry organization. * @@ -1005,6 +1073,8 @@ async function editConversationForOrg (req, res, next) { module.exports = { ALL_ORGS: getAllOrgs, + ACTIVE_CNAS: getActiveCnas, + mapActiveCnaToPublicFormat, SINGLE_ORG: getOrg, CREATE_ORG: createOrg, UPDATE_ORG: updateOrg, diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index 8585ef2aa..9f8150276 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -25,6 +25,50 @@ function exactCaseInsensitiveRegex (value) { return new RegExp(`^${_.escapeRegExp(String(value))}$`, 'i') } +function normalizeOrgReference (value) { + if (typeof value !== 'string') return null + const normalized = value.trim().toLowerCase().replace(/\s+tlr$/, '').replace(/[^a-z0-9]+/g, '') + return normalized || null +} + +function isUsableTlrReference (value) { + const normalized = String(value || '').trim().toLowerCase() + return normalized !== '' && normalized !== 'false' && normalized !== 'n/a' +} + +function addOrgReferences (orgByReference, org) { + const references = [org.short_name, org.long_name, ...(org.aliases || [])] + references.forEach(reference => { + const normalized = normalizeOrgReference(reference) + if (normalized && !orgByReference.has(normalized)) orgByReference.set(normalized, org) + }) +} + +function decorateCnaRelationships (activeOrgs, hierarchyOrgs) { + const orgByReference = new Map() + ;[...activeOrgs, ...hierarchyOrgs].forEach(org => addOrgReferences(orgByReference, org)) + + return activeOrgs.map(org => { + const root = hierarchyOrgs.find(candidate => candidate.UUID !== org.UUID && candidate.oversees?.includes(org.UUID)) + const hasOwnTlrReference = isUsableTlrReference(org.top_level_root) + const tlrReference = hasOwnTlrReference ? org.top_level_root : root?.top_level_root + const normalizedTlrReference = String(tlrReference || '').trim().toLowerCase() + let tlr + + if (normalizedTlrReference === 'true') { + tlr = hasOwnTlrReference ? org : root + } else if (isUsableTlrReference(tlrReference)) { + tlr = orgByReference.get(normalizeOrgReference(tlrReference)) + } + + return { + ...org, + _root: root, + _tlr: tlr + } + }) +} + function isResponseExtensionField (key) { return key.startsWith('_') && !INTERNAL_UNDERSCORE_FIELDS.includes(key) } @@ -562,6 +606,52 @@ class BaseOrgRepository extends BaseRepository { return data } + /** + * Retrieves active CNA organizations for the CVE.org partner-list response. + * + * The status comparison supports normalized lowercase and legacy title-case data. + * + * @returns {Promise} Active CNA organization documents. + */ + async getActiveCnas () { + const projection = { + _id: false, + __t: true, + UUID: true, + short_name: true, + long_name: true, + aliases: true, + partner_number: true, + charter_or_scope: true, + disclosure_policy: true, + advisory_locations: true, + contact_info: true, + partner_role_type: true, + partner_country: true, + top_level_root: true, + authority: true, + oversees: true + } + const activeCnaQuery = BaseOrgModel.find({ + authority: { $in: ['CNA', 'ROOT'] }, + 'program_data.status': { $in: ['active', 'Active'] } + }) + .select(projection) + .sort({ short_name: 1 }) + .lean() + const hierarchyOrgQuery = BaseOrgModel.find({ + $or: [ + { authority: 'ROOT' }, + { 'oversees.0': { $exists: true } } + ] + }) + .select(projection) + .lean() + const [activeOrgs, hierarchyOrgs] = await Promise.all([activeCnaQuery, hierarchyOrgQuery]) + + return decorateCnaRelationships(activeOrgs, hierarchyOrgs) + } + /** * @async * @function getOrgObject diff --git a/src/scripts/migrate.js b/src/scripts/migrate.js index 1bc3d8eb9..1f0331098 100644 --- a/src/scripts/migrate.js +++ b/src/scripts/migrate.js @@ -215,6 +215,9 @@ async function orgHelper (db) { websites: site ? [site] : [], phone: null }, + program_data: { + status: 'active' + }, inUse: doc.inUse, created: doc.time.created, last_updated: doc.time.modified diff --git a/test/integration-tests/cve-id/getCveIdTest.js b/test/integration-tests/cve-id/getCveIdTest.js index 053b9b84f..636b92765 100644 --- a/test/integration-tests/cve-id/getCveIdTest.js +++ b/test/integration-tests/cve-id/getCveIdTest.js @@ -9,10 +9,9 @@ const expect = chai.expect const constants = require('../constants.js') const helpers = require('../helpers.js') const app = require('../../../src/index.js') +const CveId = require('../../../src/model/cve-id') describe('Testing Get CVE-ID endpoint', () => { - // TODO: Update this test to dynamically calculate reserved count. - const RESESRVED_COUNT = 124 const YEAR_COUNT = 10 const PUB_YEAR_COUNT = 4 const TIME_WINDOW_COUNT = 40 @@ -59,6 +58,8 @@ describe('Testing Get CVE-ID endpoint', () => { }) }) it('Get all CVE-IDs in the RESERVED state', async () => { + const reservedCount = await CveId.countDocuments({ state: 'RESERVED' }) + await chai.request(app) .get('/api/cve-id?state=RESERVED') .set(constants.headers) @@ -66,7 +67,7 @@ describe('Testing Get CVE-ID endpoint', () => { expect(err).to.be.undefined expect(res).to.have.status(200) expect(_.every(res.body.cve_ids, { state: 'RESERVED' })).to.be.true - expect(res.body.cve_ids).to.have.length(RESESRVED_COUNT) + expect(res.body.cve_ids).to.have.length(reservedCount) }) }) it('Get all CVE-IDs in the PUBLISHED state', async () => { diff --git a/test/integration-tests/registry-org/activeCnaListTest.js b/test/integration-tests/registry-org/activeCnaListTest.js new file mode 100644 index 000000000..cdc671729 --- /dev/null +++ b/test/integration-tests/registry-org/activeCnaListTest.js @@ -0,0 +1,141 @@ +/* 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') +const BaseOrg = require('../../../src/model/baseorg') + +const runId = uuidv4() +const uuids = { + tlr: uuidv4(), + root: uuidv4(), + child: uuidv4(), + inactive: uuidv4() +} +const shortNames = { + tlr: `secretariat-tlr-${runId}`, + root: `secretariat-root-${runId}`, + child: `secretariat-cna-${runId}`, + inactive: `secretariat-inactive-${runId}` +} + +describe('Secretariat active CNA list', () => { + before(async () => { + await BaseOrg.collection.insertMany([{ + __t: 'RootOrg', + UUID: uuids.tlr, + short_name: shortNames.tlr, + long_name: 'Secretariat Test TLR', + authority: ['ROOT'], + top_level_root: 'true', + oversees: [uuids.root], + program_data: { status: 'active' } + }, { + __t: 'CNAOrg', + UUID: uuids.root, + short_name: shortNames.root, + long_name: 'Secretariat Test Root', + authority: ['CNA', 'ROOT'], + top_level_root: 'false', + oversees: [uuids.child], + program_data: { status: 'active' } + }, { + __t: 'CNAOrg', + UUID: uuids.child, + short_name: shortNames.child, + long_name: 'Secretariat Test CNA', + authority: ['CNA'], + top_level_root: `${shortNames.tlr} TLR`, + partner_number: 'CNA-TEST-0001', + partner_role_type: ['Vendor'], + partner_country: 'USA', + charter_or_scope: 'Secretariat-only endpoint integration test.', + contact_info: { emails: ['security@example.test'], websites: ['https://example.test/contact'] }, + disclosure_policy: 'https://example.test/policy', + advisory_locations: ['https://example.test/advisories'], + program_data: { status: 'active' } + }, { + __t: 'CNAOrg', + UUID: uuids.inactive, + short_name: shortNames.inactive, + long_name: 'Inactive Secretariat Test CNA', + authority: ['CNA'], + program_data: { status: 'inactive' } + }]) + }) + + after(async () => { + await BaseOrg.collection.deleteMany({ UUID: { $in: Object.values(uuids) } }) + }) + + it('returns a CNA created by the standard populate and migration flow', async () => { + const migratedCna = await BaseOrg.findOne({ short_name: 'window_1' }).lean() + + expect(migratedCna).to.not.equal(null) + expect(migratedCna.program_data.status).to.equal('active') + + const res = await chai.request(app) + .get('/api/registry/org/cnas') + .set(constants.headers) + + expect(res).to.have.status(200) + expect(res.body.some(org => org.shortName === migratedCna.short_name)).to.equal(true) + }) + + it('returns database-backed active CNAs to Secretariat and excludes inactive CNAs', async () => { + const res = await chai.request(app) + .get('/api/registry/org/cnas') + .set(constants.headers) + + expect(res).to.have.status(200) + const child = res.body.find(org => org.shortName === shortNames.child) + expect(res.body.some(org => org.shortName === shortNames.inactive)).to.equal(false) + expect(child).to.include({ + cnaID: 'CNA-TEST-0001', + organizationName: 'Secretariat Test CNA', + country: 'USA' + }) + }) + + it('resolves CNA-discriminator roots and inherits sentinel top-level-root relationships', async () => { + const res = await chai.request(app) + .get('/api/registry/org/cnas') + .set(constants.headers) + + expect(res).to.have.status(200) + const child = res.body.find(org => org.shortName === shortNames.child) + const root = res.body.find(org => org.shortName === shortNames.root) + expect(child.CNA).to.deep.include({ + isRoot: false, + root: { shortName: shortNames.root, organizationName: 'Secretariat Test Root' }, + TLR: { shortName: shortNames.tlr, organizationName: 'Secretariat Test TLR' } + }) + expect(root.CNA).to.deep.include({ + isRoot: true, + root: { shortName: 'n/a', organizationName: 'n/a' }, + TLR: { shortName: shortNames.tlr, organizationName: 'Secretariat Test TLR' } + }) + }) + + it('requires authentication', async () => { + const res = await chai.request(app).get('/api/registry/org/cnas') + + expect(res).to.have.status(400) + expect(res.body).to.deep.equal({ + error: 'BAD_REQUEST', + message: 'CVE-API-ORG header field required.' + }) + }) + + it('rejects authenticated non-Secretariat users', async () => { + const res = await chai.request(app) + .get('/api/registry/org/cnas') + .set(constants.nonSecretariatUserHeaders) + + expect(res).to.have.status(403) + expect(res.body.error).to.equal('SECRETARIAT_ONLY') + }) +}) diff --git a/test/unit-tests/org/activeCnaListTest.js b/test/unit-tests/org/activeCnaListTest.js new file mode 100644 index 000000000..7d8917c9b --- /dev/null +++ b/test/unit-tests/org/activeCnaListTest.js @@ -0,0 +1,155 @@ +const { expect } = require('chai') +const sinon = require('sinon') +const { + ACTIVE_CNAS, + mapActiveCnaToPublicFormat +} = require('../../../src/controller/registry.controller/org.registry.controller') +const BaseOrgRepository = require('../../../src/repositories/baseOrgRepository') +const BaseOrgModel = require('../../../src/model/baseorg') + +describe('Active CNA list', () => { + afterEach(() => sinon.restore()) + + it('returns active CNAs from the database in the CVE.org partner-list format', async () => { + const res = { status: sinon.stub().returnsThis(), json: sinon.stub() } + const activeCnas = [{ + UUID: 'child-uuid', + short_name: 'example', + partner_number: 'CNA-2026-0001', + long_name: 'Example Organization', + charter_or_scope: 'Example products.', + disclosure_policy: 'https://example.test/policy', + advisory_locations: ['https://example.test/advisories'], + contact_info: { emails: ['security@example.test'], websites: ['https://example.test/contact'] }, + partner_role_type: ['Vendor'], + partner_country: 'USA', + top_level_root: 'MITRE TLR', + authority: ['CNA'], + _root: { short_name: 'example-root', long_name: 'Example Root' }, + _tlr: { short_name: 'mitre', long_name: 'MITRE Corporation' } + }] + const repo = { getActiveCnas: sinon.stub().resolves(activeCnas) } + const req = { ctx: { repositories: { getBaseOrgRepository: () => repo } } } + + await ACTIVE_CNAS(req, res, sinon.stub()) + + expect(repo.getActiveCnas.calledOnce).to.equal(true) + expect(res.status.calledWith(200)).to.equal(true) + expect(res.json.firstCall.args[0]).to.deep.equal([{ + shortName: 'example', + cnaID: 'CNA-2026-0001', + organizationName: 'Example Organization', + scope: 'Example products.', + contact: [{ + email: [{ label: 'Email', emailAddr: 'security@example.test' }], + contact: [{ label: 'Website', url: 'https://example.test/contact' }], + form: [] + }], + disclosurePolicy: [{ label: 'Policy', url: 'https://example.test/policy', language: '' }], + securityAdvisories: { alerts: [], advisories: [{ label: 'Advisories', url: 'https://example.test/advisories' }] }, + resources: [], + CNA: { + isRoot: false, + root: { shortName: 'example-root', organizationName: 'Example Root' }, + type: ['Vendor'], + TLR: { shortName: 'mitre', organizationName: 'MITRE Corporation' }, + roles: [{ helpText: '', role: 'CNA' }] + }, + country: 'USA' + }]) + }) + + it('derives root roles and resolves a root CNA top-level root', () => { + const result = mapActiveCnaToPublicFormat({ + __t: 'RootOrg', + authority: ['ROOT', 'CNA'], + short_name: 'icscert', + long_name: 'ICS-CERT', + top_level_root: 'CISA TLR', + _tlr: { short_name: 'CISA', long_name: 'Cybersecurity and Infrastructure Security Agency (CISA)' } + }) + + expect(result.CNA).to.deep.equal({ + isRoot: true, + root: { shortName: 'n/a', organizationName: 'n/a' }, + type: [], + TLR: { + shortName: 'CISA', + organizationName: 'Cybersecurity and Infrastructure Security Agency (CISA)' + }, + roles: [ + { helpText: '', role: 'Root' }, + { helpText: '', role: 'CNA-LR' } + ] + }) + }) + + it('queries active CNA/root organizations and decorates their hierarchy', async () => { + const activeRecords = [{ + UUID: 'child-uuid', + short_name: 'example', + top_level_root: 'false' + }, { + UUID: 'child-na-uuid', + short_name: 'example-na', + top_level_root: ' N/A ' + }, { + UUID: 'tlr-uuid', + short_name: 'mitre', + long_name: 'MITRE Corporation', + __t: 'RootOrg', + authority: ['ROOT'], + top_level_root: 'true' + }] + const rootRecords = [{ + UUID: 'root-uuid', + short_name: 'example-root', + long_name: 'Example Root', + __t: 'CNAOrg', + authority: ['CNA', 'ROOT'], + oversees: ['child-uuid', 'child-na-uuid'], + top_level_root: 'MITRE TLR' + }, { + UUID: 'tlr-uuid', + short_name: 'mitre', + long_name: 'MITRE Corporation', + oversees: [], + top_level_root: 'true' + }] + const activeQuery = { + select: sinon.stub(), + sort: sinon.stub(), + lean: sinon.stub().resolves(activeRecords) + } + activeQuery.select.returns(activeQuery) + activeQuery.sort.returns(activeQuery) + const rootQuery = { + select: sinon.stub(), + lean: sinon.stub().resolves(rootRecords) + } + rootQuery.select.returns(rootQuery) + const baseOrgFindStub = sinon.stub(BaseOrgModel, 'find') + baseOrgFindStub.onFirstCall().returns(activeQuery) + baseOrgFindStub.onSecondCall().returns(rootQuery) + + const result = await new BaseOrgRepository().getActiveCnas() + + expect(BaseOrgModel.find.firstCall.args[0]).to.deep.equal({ + authority: { $in: ['CNA', 'ROOT'] }, + 'program_data.status': { $in: ['active', 'Active'] } + }) + expect(BaseOrgModel.find.secondCall.args[0]).to.deep.equal({ + $or: [ + { authority: 'ROOT' }, + { 'oversees.0': { $exists: true } } + ] + }) + expect(activeQuery.sort.calledOnceWith({ short_name: 1 })).to.equal(true) + expect(result[0]._root).to.equal(rootRecords[0]) + expect(result[0]._tlr).to.equal(activeRecords[2]) + expect(result[1]._root).to.equal(rootRecords[0]) + expect(result[1]._tlr).to.equal(activeRecords[2]) + expect(result[2]._root).to.equal(undefined) + expect(result[2]._tlr).to.equal(activeRecords[2]) + }) +})