From 698d5fef92b3459746e55e0c3b5015b258097da1 Mon Sep 17 00:00:00 2001 From: Vincent Minnocci Date: Thu, 18 May 2023 14:48:30 -0400 Subject: [PATCH 1/5] added GET /user-account/{id}/teams --- app/controllers/user-accounts-controller.js | 27 +++++++++ app/routes/user-accounts-routes.js | 7 +++ app/services/user-accounts-service.js | 66 +++++++++++++++++++++ 3 files changed, 100 insertions(+) diff --git a/app/controllers/user-accounts-controller.js b/app/controllers/user-accounts-controller.js index 3bccf47f..976b8d30 100644 --- a/app/controllers/user-accounts-controller.js +++ b/app/controllers/user-accounts-controller.js @@ -171,3 +171,30 @@ exports.register = async function(req, res) { } } }; + +exports.retrieveTeamsByUserId = function(req, res) { + const options = { + offset: req.query.offset || 0, + limit: req.query.limit || 0, + status: req.query.status, + includePagination: req.query.includePagination, + }; + + const userId = req.params.id; + + userAccountsService.retrieveTeamsByUserId(userId, options, function(err, results) { + if (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get teams. Server error.'); + } + else { + if (options.includePagination) { + logger.debug(`Success: Retrieved ${ results.data.length } of ${ results.pagination.total } total team(s)`); + } + else { + logger.debug(`Success: Retrieved ${ results.length } team(s)`); + } + return res.status(200).send(results); + } + }); +}; \ No newline at end of file diff --git a/app/routes/user-accounts-routes.js b/app/routes/user-accounts-routes.js index 11558b5e..c26185d5 100644 --- a/app/routes/user-accounts-routes.js +++ b/app/routes/user-accounts-routes.js @@ -37,6 +37,13 @@ router.route('/user-accounts/:id') userAccountsController.delete ); +router.route('/user-accounts/:id/teams') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + userAccountsController.retrieveTeamsByUserId + ); + router.route('/user-accounts/register') .post( // authn and authz handled in controller diff --git a/app/services/user-accounts-service.js b/app/services/user-accounts-service.js index 46e29db3..d8d32528 100644 --- a/app/services/user-accounts-service.js +++ b/app/services/user-accounts-service.js @@ -2,6 +2,7 @@ const uuid = require('uuid'); const UserAccount = require('../models/user-account-model'); +const Team = require('../models/team-model'); const regexValidator = require('../lib/regex'); const errors = { @@ -334,3 +335,68 @@ exports.addCreatedByUserAccountToAll = async function(attackObjects) { await addCreatedByUserAccount(attackObject); } } + +exports.retrieveTeamsByUserId = function(userAccountId ,options, callback) { + if (!userAccountId) { + const error = new Error(errors.missingParameter); + error.parameterName = 'userId'; + return callback(error); + } + + // Build the aggregation + const aggregation = [ + { $sort: { 'name': 1 } }, + ]; + + const match = { + $match: { + userIDs: {$in : [userAccountId]} + } + }; + + aggregation.push(match); + + const facet = { + $facet: { + totalCount: [ { $count: 'totalCount' }], + documents: [ ] + } + }; + if (options.offset) { + facet.$facet.documents.push({ $skip: options.offset }); + } + else { + facet.$facet.documents.push({ $skip: 0 }); + } + if (options.limit) { + facet.$facet.documents.push({ $limit: options.limit }); + } + aggregation.push(facet); + + // Retrieve the documents + Team.aggregate(aggregation, function(err, results) { + if (err) { + return callback(err); + } + else { + const teams = results[0].documents; + if (options.includePagination) { + let derivedTotalCount = 0; + if (results[0].totalCount.length > 0) { + derivedTotalCount = results[0].totalCount[0].totalCount; + } + const returnValue = { + pagination: { + total: derivedTotalCount, + offset: options.offset, + limit: options.limit + }, + data: teams + }; + return callback(null, returnValue); + } else { + return callback(null, teams); + } + } + }); +}; \ No newline at end of file From 1e2e1699898a6f793c08bb84f55f13f9817bb4a2 Mon Sep 17 00:00:00 2001 From: Vincent Minnocci Date: Thu, 18 May 2023 14:48:42 -0400 Subject: [PATCH 2/5] added route to openAPI --- app/api/definitions/openapi.yml | 3 ++ .../definitions/paths/user-accounts-paths.yml | 51 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/app/api/definitions/openapi.yml b/app/api/definitions/openapi.yml index 6469bea1..fa40db63 100644 --- a/app/api/definitions/openapi.yml +++ b/app/api/definitions/openapi.yml @@ -277,6 +277,9 @@ paths: /api/user-accounts/{id}: $ref: 'paths/user-accounts-paths.yml#/paths/~1api~1user-accounts~1{id}' + + /api/user-accounts/{id}/teams: + $ref: 'paths/user-accounts-paths.yml#/paths/~1api~1user-accounts~1{id}~1teams' /api/user-accounts/register: $ref: 'paths/user-accounts-paths.yml#/paths/~1api~1user-accounts~1register' diff --git a/app/api/definitions/paths/user-accounts-paths.yml b/app/api/definitions/paths/user-accounts-paths.yml index 9ed932cc..268240ab 100644 --- a/app/api/definitions/paths/user-accounts-paths.yml +++ b/app/api/definitions/paths/user-accounts-paths.yml @@ -210,3 +210,54 @@ paths: $ref: '../components/user-accounts.yml#/components/schemas/user-account' '400': description: 'Missing or invalid parameters were provided. The user account was not created.' + + /api/user-accounts/{id}/teams: + get: + summary: 'Get a list of teams the user account is associated with' + operationId: 'user-account-get-teams' + description: | + This endpoint gets gets a list of teams that a user is associated with, identified by the user account id. + tags: + - 'User Accounts' + parameters: + - name: id + in: path + description: 'User account id of the user account to retrieve a list of associated teams for' + required: true + schema: + type: string + - name: limit + in: query + description: | + The number of user accounts to retrieve. + The default (0) will retrieve all user accounts. + schema: + type: number + default: 0 + - name: offset + in: query + description: | + The number of user accounts to skip. + The default (0) will start with the first user account. + schema: + type: number + default: 0 + - name: includePagination + in: query + description: | + Whether to include pagination data in the returned value. + Wraps returned objects in a larger object. + schema: + type: boolean + default: false + responses: + '200': + description: 'A list of teams which the user is associated with.' + content: + application/json: + schema: + type: array + items: + $ref: '../components/teams.yml#/components/schemas/team' + '404': + description: 'A user account with the requested user account id was not found.' From b2375c9da8d1eae2e27890e07abf6b9795b649da Mon Sep 17 00:00:00 2001 From: Vincent Minnocci Date: Thu, 18 May 2023 14:49:01 -0400 Subject: [PATCH 3/5] added test --- .../api/user-accounts/user-accounts.spec.js | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/app/tests/api/user-accounts/user-accounts.spec.js b/app/tests/api/user-accounts/user-accounts.spec.js index 311c25a7..db8f2ae0 100644 --- a/app/tests/api/user-accounts/user-accounts.spec.js +++ b/app/tests/api/user-accounts/user-accounts.spec.js @@ -7,6 +7,7 @@ logger.level = 'debug'; const database = require('../../../lib/database-in-memory'); const databaseConfiguration = require('../../../lib/database-configuration'); const UserAccount = require('../../../models/user-account-model'); +const Team = require('../../../models/team-model'); const login = require('../../shared/login'); @@ -33,6 +34,7 @@ describe('User Accounts API', function () { // Wait until the indexes are created await UserAccount.init(); + await Team.init(); // Initialize the express app app = await require('../../../index').initializeApp(); @@ -293,6 +295,7 @@ describe('User Accounts API', function () { }); }); + let anonymousUserId = null; it('GET /api/user-accounts returns an array with the anonymous user account', function (done) { request(app) .get('/api/user-accounts') @@ -309,11 +312,36 @@ describe('User Accounts API', function () { expect(userAccount).toBeDefined(); expect(Array.isArray(userAccount)).toBe(true); expect(userAccount.length).toBe(1); + anonymousUserId = userAccount[0].id; done(); } }); }); + it('GET /api/user-accounts/{id}/teams returns an array with the list of accounts a user is associated with', function (done) { + Team.create({userIDs:[anonymousUserId], name: 'Example', created: new Date(), modified: new Date()}).then((response)=>{ + request(app) + .get(`/api/user-accounts/${anonymousUserId}/teams`) + .set('Accept', 'application/json') + .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + const teams = res.body; + expect(teams).toBeDefined(); + expect(Array.isArray(teams)).toBe(true); + expect(teams.length).toBe(1); + expect(teams[0].userIDs.length).toBe(1); + expect(teams[0].userIDs[0]).toBe(anonymousUserId); + done(); + } + }); + }); + }); + after(async function() { await database.closeConnection(); }); From ba9caa2bc88bdb74ffa56f80d38b44445a188e5b Mon Sep 17 00:00:00 2001 From: Vincent Minnocci Date: Thu, 18 May 2023 14:57:20 -0400 Subject: [PATCH 4/5] added another test --- .../api/user-accounts/user-accounts.spec.js | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/app/tests/api/user-accounts/user-accounts.spec.js b/app/tests/api/user-accounts/user-accounts.spec.js index db8f2ae0..495bef54 100644 --- a/app/tests/api/user-accounts/user-accounts.spec.js +++ b/app/tests/api/user-accounts/user-accounts.spec.js @@ -318,8 +318,28 @@ describe('User Accounts API', function () { }); }); + it('GET /api/user-accounts/{id}/teams returns an array with the list of accounts a user is associated with, empty array if no teams are found', function (done) { + request(app) + .get(`/api/user-accounts/${anonymousUserId}/teams`) + .set('Accept', 'application/json') + .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + const teams = res.body; + expect(teams).toBeDefined(); + expect(Array.isArray(teams)).toBe(true); + expect(teams.length).toBe(0); + done(); + } + }); + }); + it('GET /api/user-accounts/{id}/teams returns an array with the list of accounts a user is associated with', function (done) { - Team.create({userIDs:[anonymousUserId], name: 'Example', created: new Date(), modified: new Date()}).then((response)=>{ + Team.create({userIDs:[anonymousUserId], name: 'Example', created: new Date(), modified: new Date()}).then(newTeam => { request(app) .get(`/api/user-accounts/${anonymousUserId}/teams`) .set('Accept', 'application/json') @@ -334,6 +354,7 @@ describe('User Accounts API', function () { expect(teams).toBeDefined(); expect(Array.isArray(teams)).toBe(true); expect(teams.length).toBe(1); + expect(teams[0].name).toBe(newTeam.name); expect(teams[0].userIDs.length).toBe(1); expect(teams[0].userIDs[0]).toBe(anonymousUserId); done(); From d300d5d2a4f543844dd5c09972a38255c93d6d37 Mon Sep 17 00:00:00 2001 From: Jack Sheriff Date: Thu, 1 Jun 2023 15:06:38 -0400 Subject: [PATCH 5/5] Modify indentation to match rest of code. Change test function to async and use await on function calls. Add test team using the teamsService instead of the TeamModel. --- app/services/user-accounts-service.js | 123 +++++++++--------- .../api/user-accounts/user-accounts.spec.js | 89 +++++++------ 2 files changed, 108 insertions(+), 104 deletions(-) diff --git a/app/services/user-accounts-service.js b/app/services/user-accounts-service.js index d8d32528..3a910926 100644 --- a/app/services/user-accounts-service.js +++ b/app/services/user-accounts-service.js @@ -336,67 +336,68 @@ exports.addCreatedByUserAccountToAll = async function(attackObjects) { } } -exports.retrieveTeamsByUserId = function(userAccountId ,options, callback) { - if (!userAccountId) { - const error = new Error(errors.missingParameter); - error.parameterName = 'userId'; - return callback(error); - } +exports.retrieveTeamsByUserId = function(userAccountId, options, callback) { + if (!userAccountId) { + const error = new Error(errors.missingParameter); + error.parameterName = 'userId'; + return callback(error); + } + + // Build the aggregation + const aggregation = [ + { $sort: { 'name': 1 } }, + ]; - // Build the aggregation - const aggregation = [ - { $sort: { 'name': 1 } }, - ]; + const match = { + $match: { + userIDs: { $in: [userAccountId] } + } + }; + + aggregation.push(match); - const match = { - $match: { - userIDs: {$in : [userAccountId]} + const facet = { + $facet: { + totalCount: [{ $count: 'totalCount' }], + documents: [] + } + }; + if (options.offset) { + facet.$facet.documents.push({ $skip: options.offset }); } - }; - - aggregation.push(match); - - const facet = { - $facet: { - totalCount: [ { $count: 'totalCount' }], - documents: [ ] - } - }; - if (options.offset) { - facet.$facet.documents.push({ $skip: options.offset }); - } - else { - facet.$facet.documents.push({ $skip: 0 }); - } - if (options.limit) { - facet.$facet.documents.push({ $limit: options.limit }); - } - aggregation.push(facet); - - // Retrieve the documents - Team.aggregate(aggregation, function(err, results) { - if (err) { - return callback(err); - } - else { - const teams = results[0].documents; - if (options.includePagination) { - let derivedTotalCount = 0; - if (results[0].totalCount.length > 0) { - derivedTotalCount = results[0].totalCount[0].totalCount; - } - const returnValue = { - pagination: { - total: derivedTotalCount, - offset: options.offset, - limit: options.limit - }, - data: teams - }; - return callback(null, returnValue); - } else { - return callback(null, teams); - } - } - }); -}; \ No newline at end of file + else { + facet.$facet.documents.push({ $skip: 0 }); + } + if (options.limit) { + facet.$facet.documents.push({ $limit: options.limit }); + } + aggregation.push(facet); + + // Retrieve the documents + Team.aggregate(aggregation, function (err, results) { + if (err) { + return callback(err); + } + else { + const teams = results[0].documents; + if (options.includePagination) { + let derivedTotalCount = 0; + if (results[0].totalCount.length > 0) { + derivedTotalCount = results[0].totalCount[0].totalCount; + } + const returnValue = { + pagination: { + total: derivedTotalCount, + offset: options.offset, + limit: options.limit + }, + data: teams + }; + return callback(null, returnValue); + } + else { + return callback(null, teams); + } + } + }); +}; diff --git a/app/tests/api/user-accounts/user-accounts.spec.js b/app/tests/api/user-accounts/user-accounts.spec.js index 495bef54..7a156a54 100644 --- a/app/tests/api/user-accounts/user-accounts.spec.js +++ b/app/tests/api/user-accounts/user-accounts.spec.js @@ -8,6 +8,7 @@ const database = require('../../../lib/database-in-memory'); const databaseConfiguration = require('../../../lib/database-configuration'); const UserAccount = require('../../../models/user-account-model'); const Team = require('../../../models/team-model'); +const teamsService = require('../../../services/teams-service'); const login = require('../../shared/login'); @@ -318,50 +319,52 @@ describe('User Accounts API', function () { }); }); - it('GET /api/user-accounts/{id}/teams returns an array with the list of accounts a user is associated with, empty array if no teams are found', function (done) { - request(app) - .get(`/api/user-accounts/${anonymousUserId}/teams`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - const teams = res.body; - expect(teams).toBeDefined(); - expect(Array.isArray(teams)).toBe(true); - expect(teams.length).toBe(0); - done(); - } - }); - }); - - it('GET /api/user-accounts/{id}/teams returns an array with the list of accounts a user is associated with', function (done) { - Team.create({userIDs:[anonymousUserId], name: 'Example', created: new Date(), modified: new Date()}).then(newTeam => { + it('GET /api/user-accounts/{id}/teams should return an empty array when no teams have been associated with a user', function (done) { request(app) - .get(`/api/user-accounts/${anonymousUserId}/teams`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - const teams = res.body; - expect(teams).toBeDefined(); - expect(Array.isArray(teams)).toBe(true); - expect(teams.length).toBe(1); - expect(teams[0].name).toBe(newTeam.name); - expect(teams[0].userIDs.length).toBe(1); - expect(teams[0].userIDs[0]).toBe(anonymousUserId); - done(); - } - }); - }); - }); + .get(`/api/user-accounts/${ anonymousUserId }/teams`) + .set('Accept', 'application/json') + .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + const teams = res.body; + expect(teams).toBeDefined(); + expect(Array.isArray(teams)).toBe(true); + expect(teams.length).toBe(0); + done(); + } + }); + }); + + it('GET /api/user-accounts/{id}/teams should return an array with the list of teams a user is associated with', async function () { + // Add a new team to the database with the anonymous user as a member + const timestamp = new Date().toISOString(); + const teamData = { + userIDs: [ anonymousUserId ], + name: 'Example Team', + created: timestamp, + modified: timestamp + }; + await teamsService.create(teamData); + + const res = await request(app) + .get(`/api/user-accounts/${ anonymousUserId }/teams`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + + const teams = res.body; + expect(teams).toBeDefined(); + expect(Array.isArray(teams)).toBe(true); + expect(teams.length).toBe(1); + expect(teams[0].name).toBe(teamData.name); + expect(teams[0].userIDs.length).toBe(1); + expect(teams[0].userIDs[0]).toBe(anonymousUserId); + }); after(async function() { await database.closeConnection();