From ee23017acd67ad5fd03f99da2936611626086922 Mon Sep 17 00:00:00 2001 From: Vitor George Date: Wed, 23 Feb 2022 11:19:41 +0000 Subject: [PATCH 01/11] Add badges migration and "create" route --- .../migrations/20220222155039_add_badges.js | 16 ++++ app/manage/badges.js | 53 ++++++++++++ app/manage/index.js | 9 ++ app/tests/api/badges-api.test.js | 82 +++++++++++++++++++ app/tests/utils.js | 19 +++++ package.json | 3 +- yarn.lock | 40 +++++++++ 7 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 app/db/migrations/20220222155039_add_badges.js create mode 100644 app/manage/badges.js create mode 100644 app/tests/api/badges-api.test.js create mode 100644 app/tests/utils.js diff --git a/app/db/migrations/20220222155039_add_badges.js b/app/db/migrations/20220222155039_add_badges.js new file mode 100644 index 00000000..827f3dca --- /dev/null +++ b/app/db/migrations/20220222155039_add_badges.js @@ -0,0 +1,16 @@ +exports.up = async function (knex) { + await knex.schema.createTable('organization_badge', (table) => { + table.increments('id') + table + .integer('organization_id') + .references('id') + .inTable('organization') + .onDelete('CASCADE') + table.string('name').notNullable() + table.string('color').notNullable() + }) +} + +exports.down = async function (knex) { + await knex.schema.dropTable('organization_badge') +} diff --git a/app/manage/badges.js b/app/manage/badges.js new file mode 100644 index 00000000..6bea6a28 --- /dev/null +++ b/app/manage/badges.js @@ -0,0 +1,53 @@ +const db = require('../db') +const yup = require('yup') + +function route ({ validate, handler }) { + return async (req, reply) => { + try { + if (validate.params) { + req.params = await validate.params.validate(req.params) + } + + if (validate.body) { + req.body = await validate.body.validate(req.body) + } + } catch (error) { + console.log(error) + reply.boom.badRequest(error) + } + await handler(req, reply) + } +} + +module.exports = { + createBadge: route({ + validate: { + params: yup + .object({ + id: yup.number().required().positive().integer() + }) + .required(), + body: yup + .object({ + name: yup.string().required(), + color: yup.string().required() + }) + .required() + }, + handler: async function (req, reply) { + try { + const conn = await db() + const [badge] = await conn('organization_badge') + .insert({ + organization_id: req.params.id, + ...req.body + }) + .returning('*') + reply.send(badge) + } catch (err) { + console.log(err) + return reply.boom.badRequest(err.message) + } + } + }) +} diff --git a/app/manage/index.js b/app/manage/index.js index 3e517e03..336a0010 100644 --- a/app/manage/index.js +++ b/app/manage/index.js @@ -39,6 +39,8 @@ const { getOrgStaff } = require('./organizations') +const { createBadge } = require('./badges') + const { getUserTeamProfile, createProfileKeys, @@ -128,6 +130,13 @@ function manageRouter (nextApp) { router.post('/api/organizations/:id/teams', can('organization:create-team'), createOrgTeam) router.get('/api/organizations/:id/teams', getOrgTeams) + // Organization badges + router.post( + '/api/organizations/:id/badge', + can('organization:edit'), + createBadge + ) + /** * List, Create, Read, Update, Delete operations on profiles */ diff --git a/app/tests/api/badges-api.test.js b/app/tests/api/badges-api.test.js new file mode 100644 index 00000000..d2fc3af3 --- /dev/null +++ b/app/tests/api/badges-api.test.js @@ -0,0 +1,82 @@ +const path = require('path') +const test = require('ava') +const sinon = require('sinon') + +const db = require('../../db') +const hydra = require('../../lib/hydra') + +const { dropTables } = require('../utils') + +const migrationsDirectory = path.join( + __dirname, + '..', + '..', + 'db', + 'migrations' +) + +let app +let dbClient +let orgAdminAgent +let introspectStub = sinon.stub(hydra, 'introspect') + +async function createUserAgent ({ id }) { + await dbClient('users').insert({ id }) + introspectStub.withArgs(`user${id}`).returns({ + active: true, + sub: `${id}` + }) + return require('supertest') + .agent(app) + .set('Authorization', `Bearer user${id}`) +} + +test.before(async () => { + console.log('Connecting to test database...') + dbClient = await db() + + console.log('Dropping tables...') + await dropTables(dbClient) + + console.log('Migrating...') + await dbClient.migrate.latest({ directory: migrationsDirectory }) + + console.log('Starting server...') + app = await require('../../index')() + + // seed + console.log('Creating agents...') + orgAdminAgent = await createUserAgent({ id: 1 }) +}) + +test.after.always(async () => { + dbClient.destroy() +}) + +/** + * Test create an organization + */ +test('Add badge to organization', async (t) => { + const { body: org1 } = await orgAdminAgent + .post('/api/organizations') + .send({ name: 'create an organization' }) + .expect(200) + + const { body: badge1 } = await orgAdminAgent + .post(`/api/organizations/${org1.id}/badge`) + .send({ name: 'badge 1', color: 'red' }) + .expect(200) + + t.deepEqual(badge1, { + id: 1, + organization_id: org1.id, + name: 'badge 1', + color: 'red' + }) +}) + +// Badge creation +// - Only org admins can create badges +// - Only org admins can edit badges +// - Moderators can assign/edit/remove badges +// - Badges are include in user profile diff --git a/app/tests/utils.js b/app/tests/utils.js new file mode 100644 index 00000000..33496596 --- /dev/null +++ b/app/tests/utils.js @@ -0,0 +1,19 @@ +async function dropTables (db) { + const pgres = await db.raw(` + SELECT + 'drop table "' || tablename || '" cascade;' AS drop + FROM + pg_tables + WHERE + schemaname = 'public' + AND tablename != 'spatial_ref_sys' + `) + + for (const r of pgres.rows) { + await db.raw(r.drop) + } +} + +module.exports = { + dropTables +} diff --git a/package.json b/package.json index ebc3623d..e1c400e8 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,8 @@ "url-parse": "^1.5.6", "url-regex": "^5.0.0", "xml2js": "^0.4.23", - "yamljs": "^0.3.0" + "yamljs": "^0.3.0", + "yup": "^0.32.11" }, "devDependencies": { "@apidevtools/swagger-cli": "^4.0.4", diff --git a/yarn.lock b/yarn.lock index 2db870f6..bdee4292 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1793,6 +1793,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.15.4": + version "7.17.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941" + integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.1.0", "@babel/template@^7.1.2", "@babel/template@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237" @@ -2058,6 +2065,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== +"@types/lodash@^4.14.175": + version "4.14.178" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8" + integrity sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw== + "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -7660,6 +7672,11 @@ nan@^2.12.1: resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== +nanoclone@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4" + integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -8988,6 +9005,11 @@ prop-types@^15.6.2, prop-types@^15.7.2: object-assign "^4.1.1" react-is "^16.8.1" +property-expr@^2.0.4: + version "2.0.5" + resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.5.tgz#278bdb15308ae16af3e3b9640024524f4dc02cb4" + integrity sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA== + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -10942,6 +10964,11 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +toposort@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" + integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA= + touch@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" @@ -11795,6 +11822,19 @@ yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" +yup@^0.32.11: + version "0.32.11" + resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.11.tgz#d67fb83eefa4698607982e63f7ca4c5ed3cf18c5" + integrity sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg== + dependencies: + "@babel/runtime" "^7.15.4" + "@types/lodash" "^4.14.175" + lodash "^4.17.21" + lodash-es "^4.17.21" + nanoclone "^0.2.1" + property-expr "^2.0.4" + toposort "^2.0.2" + z-schema@^5.0.1: version "5.0.2" resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-5.0.2.tgz#f410394b2c9fcb9edaf6a7511491c0bb4e89a504" From 95ec07427b1d05e770dafde194cd5757b35d9622 Mon Sep 17 00:00:00 2001 From: Vitor George Date: Wed, 2 Mar 2022 09:13:22 +0000 Subject: [PATCH 02/11] Add patch route --- app/manage/index.js | 7 +- app/tests/api/badges-api.test.js | 131 +++++++++++++++++++++++++++---- 2 files changed, 123 insertions(+), 15 deletions(-) diff --git a/app/manage/index.js b/app/manage/index.js index 336a0010..e36ec987 100644 --- a/app/manage/index.js +++ b/app/manage/index.js @@ -39,7 +39,7 @@ const { getOrgStaff } = require('./organizations') -const { createBadge } = require('./badges') +const { createBadge, patchBadge} = require('./badges') const { getUserTeamProfile, @@ -136,6 +136,11 @@ function manageRouter (nextApp) { can('organization:edit'), createBadge ) + router.patch( + '/api/organizations/:id/badge/:badgeId', + can('organization:edit'), + patchBadge + ) /** * List, Create, Read, Update, Delete operations on profiles diff --git a/app/tests/api/badges-api.test.js b/app/tests/api/badges-api.test.js index d2fc3af3..97aaaf3b 100644 --- a/app/tests/api/badges-api.test.js +++ b/app/tests/api/badges-api.test.js @@ -17,15 +17,35 @@ const migrationsDirectory = path.join( let app let dbClient -let orgAdminAgent +let org1 +let orgTeam1 +let orgOwner = { + id: 1 +} +let orgManager = { + id: 2 +} +let orgTeamMember = { + id: 3 +} +let notOrgMember = { + id: 4 +} +let badge1 + let introspectStub = sinon.stub(hydra, 'introspect') async function createUserAgent ({ id }) { + // Add user to db await dbClient('users').insert({ id }) + + // Mock hydra auth introspectStub.withArgs(`user${id}`).returns({ active: true, sub: `${id}` }) + + // Return agent with auth token return require('supertest') .agent(app) .set('Authorization', `Bearer user${id}`) @@ -44,9 +64,38 @@ test.before(async () => { console.log('Starting server...') app = await require('../../index')() - // seed + // Create user agents console.log('Creating agents...') - orgAdminAgent = await createUserAgent({ id: 1 }) + orgOwner.agent = await createUserAgent(orgOwner) + orgManager.agent = await createUserAgent(orgManager) + orgTeamMember.agent = await createUserAgent(orgTeamMember) + notOrgMember.agent = await createUserAgent(notOrgMember) + + // Create organization + org1 = ( + await orgOwner.agent + .post('/api/organizations') + .send({ name: 'Organization 1' }) + .expect(200) + ).body + + // Create a team + orgTeam1 = ( + await orgOwner.agent + .post(`/api/organizations/${org1.id}/teams`) + .send({ name: 'Organization 1 - Team 1' }) + .expect(200) + ).body + + // Add team member + await orgOwner.agent + .put(`/api/team/${orgTeam1.id}/${orgTeamMember.id}`) + .expect(200) + + // Add manager + await orgOwner.agent + .put(`/api/organizations/${org1.id}/addManager/${orgManager.id}`) + .expect(200) }) test.after.always(async () => { @@ -54,25 +103,79 @@ test.after.always(async () => { }) /** - * Test create an organization + * CREATE BADGE */ test('Add badge to organization', async (t) => { - const { body: org1 } = await orgAdminAgent - .post('/api/organizations') - .send({ name: 'create an organization' }) - .expect(200) - - const { body: badge1 } = await orgAdminAgent - .post(`/api/organizations/${org1.id}/badge`) - .send({ name: 'badge 1', color: 'red' }) - .expect(200) + // Owners can create badges + badge1 = ( + await orgOwner.agent + .post(`/api/organizations/${org1.id}/badge`) + .send({ name: 'badge 1', color: 'red' }) + .expect(200) + ).body t.deepEqual(badge1, { id: 1, - organization_id: org1.id, + organization_id: 1, name: 'badge 1', color: 'red' }) + + // Manager are not allowed + await orgManager.agent + .post(`/api/organizations/${org1.id}/badge`) + .send({ name: 'badge 1', color: 'red' }) + .expect(401) + + // Org Team Members are not allowed + await orgTeamMember.agent + .post(`/api/organizations/${org1.id}/badge`) + .send({ name: 'badge 1', color: 'red' }) + .expect(401) + + // Non-members are not-allowed + await notOrgMember.agent + .post(`/api/organizations/${org1.id}/badge`) + .send({ name: 'badge 1', color: 'red' }) + .expect(401) +}) + +/** + * PATCH BADGE + */ +test('Patch badge', async (t) => { + // Allow owners + let patchedBadge = ( + await orgOwner.agent + .patch(`/api/organizations/${org1.id}/badge/${badge1.id}`) + .send({ name: 'badge number 1', color: 'blue' }) + .expect(200) + ).body + + t.deepEqual(patchedBadge, { + id: 1, + organization_id: 1, + name: 'badge number 1', + color: 'blue' + }) + + // Disallow managers + await orgManager.agent + .patch(`/api/organizations/${org1.id}/badge/${badge1.id}`) + .send({ name: 'badge 1', color: 'red' }) + .expect(401) + + // Disallow org team Members + await orgManager.agent + .patch(`/api/organizations/${org1.id}/badge/${badge1.id}`) + .send({ name: 'badge 1', color: 'red' }) + .expect(401) + + // Disallow non-members + await notOrgMember.agent + .patch(`/api/organizations/${org1.id}/badge/${badge1.id}`) + .send({ name: 'badge 1', color: 'red' }) + .expect(401) }) // Badge creation From 5fe4afa894c5b113d3dbb4b19045e51043268511 Mon Sep 17 00:00:00 2001 From: Vitor George Date: Wed, 2 Mar 2022 09:51:23 +0000 Subject: [PATCH 03/11] Add patch route --- app/manage/badges.js | 29 +++++++++++++++++++++++++++++ app/tests/api/badges-api.test.js | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/app/manage/badges.js b/app/manage/badges.js index 6bea6a28..3981a5a4 100644 --- a/app/manage/badges.js +++ b/app/manage/badges.js @@ -49,5 +49,34 @@ module.exports = { return reply.boom.badRequest(err.message) } } + }), + patchBadge: route({ + validate: { + params: yup + .object({ + id: yup.number().required().positive().integer(), + badgeId: yup.number().required().positive().integer() + }) + .required(), + body: yup + .object({ + name: yup.string().optional(), + color: yup.string().optional() + }) + .required() + }, + handler: async function (req, reply) { + try { + const conn = await db() + const [badge] = await conn('organization_badge') + .update(req.body) + .where('id', req.params.badgeId) + .returning('*') + reply.send(badge) + } catch (err) { + console.log(err) + return reply.boom.badRequest(err.message) + } + } }) } diff --git a/app/tests/api/badges-api.test.js b/app/tests/api/badges-api.test.js index 97aaaf3b..0c135050 100644 --- a/app/tests/api/badges-api.test.js +++ b/app/tests/api/badges-api.test.js @@ -105,7 +105,7 @@ test.after.always(async () => { /** * CREATE BADGE */ -test('Add badge to organization', async (t) => { +test('Create badge', async (t) => { // Owners can create badges badge1 = ( await orgOwner.agent From d8ccc709e2279c804cbef6850572c2accb850673 Mon Sep 17 00:00:00 2001 From: Vitor George Date: Wed, 2 Mar 2022 10:12:23 +0000 Subject: [PATCH 04/11] Add delete and list routes --- app/manage/badges.js | 42 +++++++++++++++ app/manage/index.js | 16 +++++- app/tests/api/badges-api.test.js | 88 ++++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 2 deletions(-) diff --git a/app/manage/badges.js b/app/manage/badges.js index 3981a5a4..d9d7b948 100644 --- a/app/manage/badges.js +++ b/app/manage/badges.js @@ -20,6 +20,27 @@ function route ({ validate, handler }) { } module.exports = { + listBadges: route({ + validate: { + params: yup + .object({ + id: yup.number().required().positive().integer() + }) + .required() + }, + handler: async function (req, reply) { + try { + const conn = await db() + const badges = await conn('organization_badge') + .select('*') + .where('organization_id', req.params.id) + reply.send(badges) + } catch (err) { + console.log(err) + return reply.boom.badRequest(err.message) + } + } + }), createBadge: route({ validate: { params: yup @@ -78,5 +99,26 @@ module.exports = { return reply.boom.badRequest(err.message) } } + }), + deleteBadge: route({ + validate: { + params: yup + .object({ + badgeId: yup.number().required().positive().integer() + }) + .required() + }, + handler: async function (req, reply) { + try { + const conn = await db() + await conn('organization_badge') + .delete() + .where('id', req.params.badgeId) + return reply.sendStatus(200) + } catch (err) { + console.log(err) + return reply.boom.badRequest(err.message) + } + } }) } diff --git a/app/manage/index.js b/app/manage/index.js index e36ec987..6aa5dc6b 100644 --- a/app/manage/index.js +++ b/app/manage/index.js @@ -39,7 +39,7 @@ const { getOrgStaff } = require('./organizations') -const { createBadge, patchBadge} = require('./badges') +const { createBadge, patchBadge, deleteBadge, listBadges } = require('./badges') const { getUserTeamProfile, @@ -130,7 +130,14 @@ function manageRouter (nextApp) { router.post('/api/organizations/:id/teams', can('organization:create-team'), createOrgTeam) router.get('/api/organizations/:id/teams', getOrgTeams) - // Organization badges + /** + * Badges routes + */ + router.get( + '/api/organizations/:id/badge', + can('organization:edit'), + listBadges + ) router.post( '/api/organizations/:id/badge', can('organization:edit'), @@ -141,6 +148,11 @@ function manageRouter (nextApp) { can('organization:edit'), patchBadge ) + router.delete( + '/api/organizations/:id/badge/:badgeId', + can('organization:edit'), + deleteBadge + ) /** * List, Create, Read, Update, Delete operations on profiles diff --git a/app/tests/api/badges-api.test.js b/app/tests/api/badges-api.test.js index 0c135050..b2bc5747 100644 --- a/app/tests/api/badges-api.test.js +++ b/app/tests/api/badges-api.test.js @@ -178,6 +178,94 @@ test('Patch badge', async (t) => { .expect(401) }) +/** + * LIST BADGES + */ +test('List badges', async (t) => { + // Add more badges + await orgOwner.agent + .post(`/api/organizations/${org1.id}/badge`) + .send({ name: 'badge number 2', color: 'green' }) + .expect(200) + + // Add more badges + await orgOwner.agent + .post(`/api/organizations/${org1.id}/badge`) + .send({ name: 'badge number 3', color: 'yellow' }) + .expect(200) + + // Owners can list badges + const badgesList = ( + await orgOwner.agent.get(`/api/organizations/${org1.id}/badge`).expect(200) + ).body + + t.deepEqual(badgesList, [ + { + id: 1, + organization_id: 1, + name: 'badge number 1', + color: 'blue' + }, + { + id: 2, + organization_id: 1, + name: 'badge number 2', + color: 'green' + }, + { + id: 3, + organization_id: 1, + name: 'badge number 3', + color: 'yellow' + } + ]) +}) + +/** + * DELETE BADGE + */ +test('Delete badge', async (t) => { + // Disallow managers + await orgManager.agent + .delete(`/api/organizations/${org1.id}/badge/${badge1.id}`) + .expect(401) + + // Disallow org team Members + await orgManager.agent + .delete(`/api/organizations/${org1.id}/badge/${badge1.id}`) + .expect(401) + + // Disallow non-members + await notOrgMember.agent + .delete(`/api/organizations/${org1.id}/badge/${badge1.id}`) + .expect(401) + + // Allow owners + await orgOwner.agent + .delete(`/api/organizations/${org1.id}/badge/${badge1.id}`) + .expect(200) + + // Check if badge list has changed + const badgesList = ( + await orgOwner.agent.get(`/api/organizations/${org1.id}/badge`).expect(200) + ).body + + t.deepEqual(badgesList, [ + { + id: 2, + organization_id: 1, + name: 'badge number 2', + color: 'green' + }, + { + id: 3, + organization_id: 1, + name: 'badge number 3', + color: 'yellow' + } + ]) +}) + // Badge creation // - Only org admins can create badges // - Only org admins can edit badges From fef23a4fdfbc998bfed81dde4a335a8d7e79047a Mon Sep 17 00:00:00 2001 From: Vitor George Date: Wed, 2 Mar 2022 10:35:20 +0000 Subject: [PATCH 05/11] Rename route --- app/manage/index.js | 10 +++++----- app/tests/api/badges-api.test.js | 32 ++++++++++++++++---------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/app/manage/index.js b/app/manage/index.js index 6aa5dc6b..3111ed29 100644 --- a/app/manage/index.js +++ b/app/manage/index.js @@ -131,25 +131,25 @@ function manageRouter (nextApp) { router.get('/api/organizations/:id/teams', getOrgTeams) /** - * Badges routes + * Manage organization badges */ router.get( - '/api/organizations/:id/badge', + '/api/organizations/:id/badges', can('organization:edit'), listBadges ) router.post( - '/api/organizations/:id/badge', + '/api/organizations/:id/badges', can('organization:edit'), createBadge ) router.patch( - '/api/organizations/:id/badge/:badgeId', + '/api/organizations/:id/badges/:badgeId', can('organization:edit'), patchBadge ) router.delete( - '/api/organizations/:id/badge/:badgeId', + '/api/organizations/:id/badges/:badgeId', can('organization:edit'), deleteBadge ) diff --git a/app/tests/api/badges-api.test.js b/app/tests/api/badges-api.test.js index b2bc5747..afe0674c 100644 --- a/app/tests/api/badges-api.test.js +++ b/app/tests/api/badges-api.test.js @@ -109,7 +109,7 @@ test('Create badge', async (t) => { // Owners can create badges badge1 = ( await orgOwner.agent - .post(`/api/organizations/${org1.id}/badge`) + .post(`/api/organizations/${org1.id}/badges`) .send({ name: 'badge 1', color: 'red' }) .expect(200) ).body @@ -123,19 +123,19 @@ test('Create badge', async (t) => { // Manager are not allowed await orgManager.agent - .post(`/api/organizations/${org1.id}/badge`) + .post(`/api/organizations/${org1.id}/badges`) .send({ name: 'badge 1', color: 'red' }) .expect(401) // Org Team Members are not allowed await orgTeamMember.agent - .post(`/api/organizations/${org1.id}/badge`) + .post(`/api/organizations/${org1.id}/badges`) .send({ name: 'badge 1', color: 'red' }) .expect(401) // Non-members are not-allowed await notOrgMember.agent - .post(`/api/organizations/${org1.id}/badge`) + .post(`/api/organizations/${org1.id}/badges`) .send({ name: 'badge 1', color: 'red' }) .expect(401) }) @@ -147,7 +147,7 @@ test('Patch badge', async (t) => { // Allow owners let patchedBadge = ( await orgOwner.agent - .patch(`/api/organizations/${org1.id}/badge/${badge1.id}`) + .patch(`/api/organizations/${org1.id}/badges/${badge1.id}`) .send({ name: 'badge number 1', color: 'blue' }) .expect(200) ).body @@ -161,19 +161,19 @@ test('Patch badge', async (t) => { // Disallow managers await orgManager.agent - .patch(`/api/organizations/${org1.id}/badge/${badge1.id}`) + .patch(`/api/organizations/${org1.id}/badges/${badge1.id}`) .send({ name: 'badge 1', color: 'red' }) .expect(401) // Disallow org team Members await orgManager.agent - .patch(`/api/organizations/${org1.id}/badge/${badge1.id}`) + .patch(`/api/organizations/${org1.id}/badges/${badge1.id}`) .send({ name: 'badge 1', color: 'red' }) .expect(401) // Disallow non-members await notOrgMember.agent - .patch(`/api/organizations/${org1.id}/badge/${badge1.id}`) + .patch(`/api/organizations/${org1.id}/badges/${badge1.id}`) .send({ name: 'badge 1', color: 'red' }) .expect(401) }) @@ -184,19 +184,19 @@ test('Patch badge', async (t) => { test('List badges', async (t) => { // Add more badges await orgOwner.agent - .post(`/api/organizations/${org1.id}/badge`) + .post(`/api/organizations/${org1.id}/badges`) .send({ name: 'badge number 2', color: 'green' }) .expect(200) // Add more badges await orgOwner.agent - .post(`/api/organizations/${org1.id}/badge`) + .post(`/api/organizations/${org1.id}/badges`) .send({ name: 'badge number 3', color: 'yellow' }) .expect(200) // Owners can list badges const badgesList = ( - await orgOwner.agent.get(`/api/organizations/${org1.id}/badge`).expect(200) + await orgOwner.agent.get(`/api/organizations/${org1.id}/badges`).expect(200) ).body t.deepEqual(badgesList, [ @@ -227,27 +227,27 @@ test('List badges', async (t) => { test('Delete badge', async (t) => { // Disallow managers await orgManager.agent - .delete(`/api/organizations/${org1.id}/badge/${badge1.id}`) + .delete(`/api/organizations/${org1.id}/badges/${badge1.id}`) .expect(401) // Disallow org team Members await orgManager.agent - .delete(`/api/organizations/${org1.id}/badge/${badge1.id}`) + .delete(`/api/organizations/${org1.id}/badges/${badge1.id}`) .expect(401) // Disallow non-members await notOrgMember.agent - .delete(`/api/organizations/${org1.id}/badge/${badge1.id}`) + .delete(`/api/organizations/${org1.id}/badges/${badge1.id}`) .expect(401) // Allow owners await orgOwner.agent - .delete(`/api/organizations/${org1.id}/badge/${badge1.id}`) + .delete(`/api/organizations/${org1.id}/badges/${badge1.id}`) .expect(200) // Check if badge list has changed const badgesList = ( - await orgOwner.agent.get(`/api/organizations/${org1.id}/badge`).expect(200) + await orgOwner.agent.get(`/api/organizations/${org1.id}/badges`).expect(200) ).body t.deepEqual(badgesList, [ From 21ef4e33b6e5b22aa86a5099ecc78491e2d646fa Mon Sep 17 00:00:00 2001 From: Vitor George Date: Wed, 2 Mar 2022 11:34:01 +0000 Subject: [PATCH 06/11] Add "assign badge" route --- .../20220302104250_add_user_badges.js | 20 +++++++++ app/manage/badges.js | 33 ++++++++++++++ app/manage/index.js | 21 ++++++++- app/tests/api/badges-api.test.js | 43 +++++++++++++++---- 4 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 app/db/migrations/20220302104250_add_user_badges.js diff --git a/app/db/migrations/20220302104250_add_user_badges.js b/app/db/migrations/20220302104250_add_user_badges.js new file mode 100644 index 00000000..1b44f605 --- /dev/null +++ b/app/db/migrations/20220302104250_add_user_badges.js @@ -0,0 +1,20 @@ +exports.up = async function (knex) { + await knex.schema.createTable('user_badge', (table) => { + table + .integer('badge_id') + .references('id') + .inTable('organization_badge') + .onDelete('CASCADE') + table + .integer('user_id') + .references('id') + .inTable('organization_badge') + .onDelete('CASCADE') + table.date('assigned_at').defaultTo(knex.fn.now()) + table.date('valid_until') + }) +} + +exports.down = async function (knex) { + await knex.schema.dropTable('user_badge') +} diff --git a/app/manage/badges.js b/app/manage/badges.js index d9d7b948..132ff5ce 100644 --- a/app/manage/badges.js +++ b/app/manage/badges.js @@ -1,5 +1,6 @@ const db = require('../db') const yup = require('yup') +const organization = require('../lib/organization') function route ({ validate, handler }) { return async (req, reply) => { @@ -120,5 +121,37 @@ module.exports = { return reply.boom.badRequest(err.message) } } + }), + assignUserBadge: route({ + validate: { + params: yup + .object({ + id: yup.number().required().positive().integer(), + badgeId: yup.number().required().positive().integer(), + userId: yup.number().required().positive().integer() + }) + .required() + }, + handler: async function (req, reply) { + try { + const conn = await db() + + // user is member + await organization.isMember(req.params.id, req.params.userId) + + // assign badge + const [badge] = await conn('user_badge') + .insert({ + user_id: req.params.userId, + badge_id: req.params.badgeId + }) + .returning('*') + + reply.send(badge) + } catch (err) { + console.log(err) + return reply.boom.badRequest(err.message) + } + } }) } diff --git a/app/manage/index.js b/app/manage/index.js index 3111ed29..0a6777a9 100644 --- a/app/manage/index.js +++ b/app/manage/index.js @@ -39,7 +39,7 @@ const { getOrgStaff } = require('./organizations') -const { createBadge, patchBadge, deleteBadge, listBadges } = require('./badges') +const { createBadge, patchBadge, deleteBadge, listBadges, assignUserBadge } = require('./badges') const { getUserTeamProfile, @@ -154,6 +154,25 @@ function manageRouter (nextApp) { deleteBadge ) + /** + * Manage organization member badges + */ + router.post( + '/api/organizations/:id/badges/:badgeId/assign/:userId', + can('organization:edit'), + assignUserBadge + ) + // router.patch( + // '/api/organizations/:id/badges/:badgeId/assign/:userId', + // can('organization:edit'), + // updateUserBadge + // ) + // router.delete( + // '/api/organizations/:id/badges/:badgeId/remove/:userId', + // can('organization:edit'), + // removeUserBadge + // ) + /** * List, Create, Read, Update, Delete operations on profiles */ diff --git a/app/tests/api/badges-api.test.js b/app/tests/api/badges-api.test.js index afe0674c..c3f6e790 100644 --- a/app/tests/api/badges-api.test.js +++ b/app/tests/api/badges-api.test.js @@ -31,7 +31,7 @@ let orgTeamMember = { let notOrgMember = { id: 4 } -let badge1 +let badge1, badge2 let introspectStub = sinon.stub(hydra, 'introspect') @@ -183,10 +183,10 @@ test('Patch badge', async (t) => { */ test('List badges', async (t) => { // Add more badges - await orgOwner.agent + badge2 = (await orgOwner.agent .post(`/api/organizations/${org1.id}/badges`) .send({ name: 'badge number 2', color: 'green' }) - .expect(200) + .expect(200)).body // Add more badges await orgOwner.agent @@ -266,8 +266,35 @@ test('Delete badge', async (t) => { ]) }) -// Badge creation -// - Only org admins can create badges -// - Only org admins can edit badges -// - Moderators can assign/edit/remove badges -// - Badges are include in user profile +/** + * ASSIGN BADGE + */ +test('Assign badge', async (t) => { + const assignBadgeRoute = `/api/organizations/${org1.id}/badges/${badge2.id}/assign/${orgTeamMember.id}` + + // Disallow managers + await orgManager.agent + .post(assignBadgeRoute) + .expect(401) + + // Disallow org team Members + await orgManager.agent + .post(assignBadgeRoute) + .expect(401) + + // Disallow non-members + await notOrgMember.agent + .post(assignBadgeRoute) + .expect(401) + + // Allow owners + const badgeAssignment = (await orgOwner.agent + .post(`/api/organizations/${org1.id}/badges/${badge2.id}/assign/${orgTeamMember.id}`) + .expect(200)).body + + t.like(badgeAssignment, { + badge_id: badge2.id, + user_id: orgTeamMember.id + }) + t.falsy(badgeAssignment.assignedAt) +}) From 7f183a1272adc6d31d284f803a345ef40b88092c Mon Sep 17 00:00:00 2001 From: Vitor George Date: Tue, 8 Mar 2022 09:15:50 +0000 Subject: [PATCH 07/11] List user badges --- .../20220302104250_add_user_badges.js | 4 +- app/lib/profile.js | 22 ++++++- app/manage/badges.js | 30 ++++++++- app/manage/index.js | 7 ++- app/tests/api/badges-api.test.js | 62 ++++++++++++++++++- 5 files changed, 116 insertions(+), 9 deletions(-) diff --git a/app/db/migrations/20220302104250_add_user_badges.js b/app/db/migrations/20220302104250_add_user_badges.js index 1b44f605..df91e74a 100644 --- a/app/db/migrations/20220302104250_add_user_badges.js +++ b/app/db/migrations/20220302104250_add_user_badges.js @@ -10,8 +10,8 @@ exports.up = async function (knex) { .references('id') .inTable('organization_badge') .onDelete('CASCADE') - table.date('assigned_at').defaultTo(knex.fn.now()) - table.date('valid_until') + table.datetime('assigned_at').defaultTo(knex.fn.now()) + table.datetime('valid_until') }) } diff --git a/app/lib/profile.js b/app/lib/profile.js index 8ec5f42c..e0a16d7b 100644 --- a/app/lib/profile.js +++ b/app/lib/profile.js @@ -254,6 +254,25 @@ async function getUserManageToken (id) { return unpack(conn('users').select('manageToken').where('id', id).debug()) } +async function getUserBadges (id) { + const conn = await db() + return conn('user_badge') + .select([ + 'id', + 'assigned_at', + 'valid_until', + 'organization_id', + 'name', + 'color' + ]) + .leftJoin( + 'organization_badge', + 'user_badge.badge_id', + 'organization_badge.id' + ) + .where('user_badge.user_id', id) +} + module.exports = { addProfileKeys, modifyProfileKey, @@ -263,5 +282,6 @@ module.exports = { setProfile, getProfile, getTableForProfileType, - getUserManageToken + getUserManageToken, + getUserBadges } diff --git a/app/manage/badges.js b/app/manage/badges.js index 132ff5ce..fcbd4da0 100644 --- a/app/manage/badges.js +++ b/app/manage/badges.js @@ -1,6 +1,7 @@ const db = require('../db') const yup = require('yup') const organization = require('../lib/organization') +const profile = require('../lib/profile') function route ({ validate, handler }) { return async (req, reply) => { @@ -130,7 +131,12 @@ module.exports = { badgeId: yup.number().required().positive().integer(), userId: yup.number().required().positive().integer() }) - .required() + .required(), + body: yup + .object({ + assigned_at: yup.date().optional(), + valid_until: yup.date().optional() + }) }, handler: async function (req, reply) { try { @@ -143,7 +149,9 @@ module.exports = { const [badge] = await conn('user_badge') .insert({ user_id: req.params.userId, - badge_id: req.params.badgeId + badge_id: req.params.badgeId, + assigned_at: req.body.assigned_at, + valid_until: req.body.valid_until }) .returning('*') @@ -153,5 +161,23 @@ module.exports = { return reply.boom.badRequest(err.message) } } + }), + listUserBadges: route({ + validate: { + params: yup + .object({ + userId: yup.number().required().positive().integer() + }) + .required() + }, + handler: async function (req, reply) { + try { + const badges = await profile.getUserBadges(req.params.userId) + reply.send({ badges }) + } catch (err) { + console.log(err) + return reply.boom.badRequest(err.message) + } + } }) } diff --git a/app/manage/index.js b/app/manage/index.js index 0a6777a9..042b811b 100644 --- a/app/manage/index.js +++ b/app/manage/index.js @@ -39,7 +39,7 @@ const { getOrgStaff } = require('./organizations') -const { createBadge, patchBadge, deleteBadge, listBadges, assignUserBadge } = require('./badges') +const { createBadge, patchBadge, deleteBadge, listBadges, assignUserBadge, listUserBadges } = require('./badges') const { getUserTeamProfile, @@ -162,6 +162,11 @@ function manageRouter (nextApp) { can('organization:edit'), assignUserBadge ) + router.get( + '/api/user/:userId/badges', + can('public:authenticated'), + listUserBadges + ) // router.patch( // '/api/organizations/:id/badges/:badgeId/assign/:userId', // can('organization:edit'), diff --git a/app/tests/api/badges-api.test.js b/app/tests/api/badges-api.test.js index c3f6e790..3c47bb4a 100644 --- a/app/tests/api/badges-api.test.js +++ b/app/tests/api/badges-api.test.js @@ -31,7 +31,7 @@ let orgTeamMember = { let notOrgMember = { id: 4 } -let badge1, badge2 +let badge1, badge2, badge3 let introspectStub = sinon.stub(hydra, 'introspect') @@ -189,9 +189,15 @@ test('List badges', async (t) => { .expect(200)).body // Add more badges - await orgOwner.agent + badge3 = (await orgOwner.agent .post(`/api/organizations/${org1.id}/badges`) .send({ name: 'badge number 3', color: 'yellow' }) + .expect(200)).body + + // Add more badges + await orgOwner.agent + .post(`/api/organizations/${org1.id}/badges`) + .send({ name: 'badge number 4', color: 'pink' }) .expect(200) // Owners can list badges @@ -217,6 +223,12 @@ test('List badges', async (t) => { organization_id: 1, name: 'badge number 3', color: 'yellow' + }, + { + id: 4, + organization_id: 1, + name: 'badge number 4', + color: 'pink' } ]) }) @@ -262,6 +274,12 @@ test('Delete badge', async (t) => { organization_id: 1, name: 'badge number 3', color: 'yellow' + }, + { + id: 4, + organization_id: 1, + name: 'badge number 4', + color: 'pink' } ]) }) @@ -290,11 +308,49 @@ test('Assign badge', async (t) => { // Allow owners const badgeAssignment = (await orgOwner.agent .post(`/api/organizations/${org1.id}/badges/${badge2.id}/assign/${orgTeamMember.id}`) + .send({ assigned_at: '2020-02-02T00:00:00.000Z', valid_until: '2020-05-05T00:00:00.000Z' }) .expect(200)).body t.like(badgeAssignment, { badge_id: badge2.id, - user_id: orgTeamMember.id + user_id: orgTeamMember.id, + assigned_at: '2020-02-02T00:00:00.000Z', + valid_until: '2020-05-05T00:00:00.000Z' }) t.falsy(badgeAssignment.assignedAt) }) + +/** + * LIST USER BADGES + */ +test('List user badges', async (t) => { + // Assign badge 3 + await orgOwner.agent + .post(`/api/organizations/${org1.id}/badges/${badge3.id}/assign/${orgTeamMember.id}`) + .send({ assigned_at: '2020-07-07T00:00:00.000Z', valid_until: '2020-08-08T00:00:00.000Z' }) + .expect(200) + + const badges = (await orgManager.agent + .get(`/api/user/${orgTeamMember.id}/badges`) + .expect(200)).body + + t.like(badges, { badges: [ + { + id: 2, + organization_id: 1, + name: 'badge number 2', + color: 'green', + assigned_at: '2020-02-02T00:00:00.000Z', + valid_until: '2020-05-05T00:00:00.000Z' + }, + { + id: 3, + organization_id: 1, + name: 'badge number 3', + color: 'yellow', + assigned_at: '2020-07-07T00:00:00.000Z', + valid_until: '2020-08-08T00:00:00.000Z' + } + ] }) +}) + From 85aca1e8c8a3508e49a1b78ecff77c94c751285a Mon Sep 17 00:00:00 2001 From: Vitor George Date: Tue, 8 Mar 2022 10:08:53 +0000 Subject: [PATCH 08/11] Update badge --- app/manage/badges.js | 37 ++++++++++++++++++++++++++++++++ app/manage/index.js | 18 ++++++++++++++-- app/tests/api/badges-api.test.js | 26 +++++++++++++++++++--- 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/app/manage/badges.js b/app/manage/badges.js index fcbd4da0..0c9059b9 100644 --- a/app/manage/badges.js +++ b/app/manage/badges.js @@ -179,5 +179,42 @@ module.exports = { return reply.boom.badRequest(err.message) } } + }), + updateUserBadge: route({ + validate: { + params: yup + .object({ + badgeId: yup.number().required().positive().integer(), + userId: yup.number().required().positive().integer() + }) + .required(), + body: yup + .object({ + assigned_at: yup.date().optional(), + valid_until: yup.date().optional() + }) + }, + handler: async function (req, reply) { + try { + const conn = await db() + + // assign badge + const [badge] = await conn('user_badge') + .update({ + assigned_at: req.body.assigned_at, + valid_until: req.body.valid_until + }) + .where({ + user_id: req.params.userId, + badge_id: req.params.badgeId + }) + .returning('*') + + reply.send(badge) + } catch (err) { + console.log(err) + return reply.boom.badRequest(err.message) + } + } }) } diff --git a/app/manage/index.js b/app/manage/index.js index 042b811b..d06ea03d 100644 --- a/app/manage/index.js +++ b/app/manage/index.js @@ -39,7 +39,15 @@ const { getOrgStaff } = require('./organizations') -const { createBadge, patchBadge, deleteBadge, listBadges, assignUserBadge, listUserBadges } = require('./badges') +const { + createBadge, + patchBadge, + deleteBadge, + listBadges, + assignUserBadge, + listUserBadges, + updateUserBadge +} = require('./badges') const { getUserTeamProfile, @@ -155,7 +163,7 @@ function manageRouter (nextApp) { ) /** - * Manage organization member badges + * Manage user badges */ router.post( '/api/organizations/:id/badges/:badgeId/assign/:userId', @@ -167,6 +175,12 @@ function manageRouter (nextApp) { can('public:authenticated'), listUserBadges ) + router.patch( + `/api/organizations/:id/member/:userId/badge/:badgeId`, + can('organization:edit'), + updateUserBadge + ) + // router.patch( // '/api/organizations/:id/badges/:badgeId/assign/:userId', // can('organization:edit'), diff --git a/app/tests/api/badges-api.test.js b/app/tests/api/badges-api.test.js index 3c47bb4a..2799e5df 100644 --- a/app/tests/api/badges-api.test.js +++ b/app/tests/api/badges-api.test.js @@ -166,7 +166,7 @@ test('Patch badge', async (t) => { .expect(401) // Disallow org team Members - await orgManager.agent + await orgTeamMember.agent .patch(`/api/organizations/${org1.id}/badges/${badge1.id}`) .send({ name: 'badge 1', color: 'red' }) .expect(401) @@ -243,7 +243,7 @@ test('Delete badge', async (t) => { .expect(401) // Disallow org team Members - await orgManager.agent + await orgTeamMember.agent .delete(`/api/organizations/${org1.id}/badges/${badge1.id}`) .expect(401) @@ -296,7 +296,7 @@ test('Assign badge', async (t) => { .expect(401) // Disallow org team Members - await orgManager.agent + await orgTeamMember.agent .post(assignBadgeRoute) .expect(401) @@ -354,3 +354,23 @@ test('List user badges', async (t) => { ] }) }) +/** + * UPDATE BADGE + */ +test('Update badge', async (t) => { + const updateBadgeRoute = `/api/organizations/${org1.id}/member/${orgTeamMember.id}/badge/${badge2.id}` + + // Allow owners + const badgeAssignment = (await orgOwner.agent + .patch(updateBadgeRoute) + .send({ + valid_until: '2021-01-01Z' + }) + .expect(200)).body + + t.like(badgeAssignment, { + badge_id: badge2.id, + user_id: orgTeamMember.id, + valid_until: '2021-01-01T00:00:00.000Z' + }) +}) From 4f670d3c4d89de6b678b351688c499a06a5b671b Mon Sep 17 00:00:00 2001 From: Vitor George Date: Tue, 8 Mar 2022 10:20:04 +0000 Subject: [PATCH 09/11] Remove user badge --- app/manage/badges.js | 28 ++++++++++++++++ app/manage/index.js | 18 ++++------ app/tests/api/badges-api.test.js | 57 ++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 11 deletions(-) diff --git a/app/manage/badges.js b/app/manage/badges.js index 0c9059b9..fa11579c 100644 --- a/app/manage/badges.js +++ b/app/manage/badges.js @@ -216,5 +216,33 @@ module.exports = { return reply.boom.badRequest(err.message) } } + }), + removeUserBadge: route({ + validate: { + params: yup + .object({ + badgeId: yup.number().required().positive().integer(), + userId: yup.number().required().positive().integer() + }) + .required() + }, + handler: async function (req, reply) { + try { + const conn = await db() + + // delete user badge + await conn('user_badge') + .delete() + .where({ + user_id: req.params.userId, + badge_id: req.params.badgeId + }) + + reply.sendStatus(200) + } catch (err) { + console.log(err) + return reply.boom.badRequest(err.message) + } + } }) } diff --git a/app/manage/index.js b/app/manage/index.js index d06ea03d..e8500d5a 100644 --- a/app/manage/index.js +++ b/app/manage/index.js @@ -46,7 +46,8 @@ const { listBadges, assignUserBadge, listUserBadges, - updateUserBadge + updateUserBadge, + removeUserBadge } = require('./badges') const { @@ -181,16 +182,11 @@ function manageRouter (nextApp) { updateUserBadge ) - // router.patch( - // '/api/organizations/:id/badges/:badgeId/assign/:userId', - // can('organization:edit'), - // updateUserBadge - // ) - // router.delete( - // '/api/organizations/:id/badges/:badgeId/remove/:userId', - // can('organization:edit'), - // removeUserBadge - // ) + router.delete( + `/api/organizations/:id/member/:userId/badge/:badgeId`, + can('organization:edit'), + removeUserBadge + ) /** * List, Create, Read, Update, Delete operations on profiles diff --git a/app/tests/api/badges-api.test.js b/app/tests/api/badges-api.test.js index 2799e5df..fc053303 100644 --- a/app/tests/api/badges-api.test.js +++ b/app/tests/api/badges-api.test.js @@ -360,6 +360,21 @@ test('List user badges', async (t) => { test('Update badge', async (t) => { const updateBadgeRoute = `/api/organizations/${org1.id}/member/${orgTeamMember.id}/badge/${badge2.id}` + // Disallow managers + await orgManager.agent + .patch(updateBadgeRoute) + .expect(401) + + // Disallow org team Members + await orgTeamMember.agent + .patch(updateBadgeRoute) + .expect(401) + + // Disallow non-members + await notOrgMember.agent + .patch(updateBadgeRoute) + .expect(401) + // Allow owners const badgeAssignment = (await orgOwner.agent .patch(updateBadgeRoute) @@ -374,3 +389,45 @@ test('Update badge', async (t) => { valid_until: '2021-01-01T00:00:00.000Z' }) }) + +/** + * REMOVE BADGE + */ +test('Remove badge', async (t) => { + const removeBadgeRoute = `/api/organizations/${org1.id}/member/${orgTeamMember.id}/badge/${badge2.id}` + + // Disallow managers + await orgManager.agent + .delete(removeBadgeRoute) + .expect(401) + + // Disallow org team Members + await orgTeamMember.agent + .delete(removeBadgeRoute) + .expect(401) + + // Disallow non-members + await notOrgMember.agent + .delete(removeBadgeRoute) + .expect(401) + + // Allow owners + await orgOwner.agent + .delete(removeBadgeRoute) + .expect(200) + + const badges = (await orgManager.agent + .get(`/api/user/${orgTeamMember.id}/badges`) + .expect(200)).body + + t.like(badges, { badges: [ + { + id: 3, + organization_id: 1, + name: 'badge number 3', + color: 'yellow', + assigned_at: '2020-07-07T00:00:00.000Z', + valid_until: '2020-08-08T00:00:00.000Z' + } + ] }) +}) From 4dc498f7fa750ea86a7f59c9a420e9cf2ea9ced4 Mon Sep 17 00:00:00 2001 From: Vitor George Date: Tue, 8 Mar 2022 10:51:56 +0000 Subject: [PATCH 10/11] Drop tables and migrate before testing, not after --- app/tests/api/badges-api.test.js | 17 ++--------------- app/tests/api/organization-api.test.js | 13 +++---------- app/tests/api/organization-model.test.js | 13 +++---------- app/tests/api/profile-api.test.js | 13 +++---------- app/tests/api/profile-model.test.js | 14 +++----------- app/tests/api/team-api.test.js | 13 +++---------- app/tests/api/team-model.test.js | 13 +++---------- app/tests/utils.js | 17 +++++++++++++++-- 8 files changed, 35 insertions(+), 78 deletions(-) diff --git a/app/tests/api/badges-api.test.js b/app/tests/api/badges-api.test.js index fc053303..0329224c 100644 --- a/app/tests/api/badges-api.test.js +++ b/app/tests/api/badges-api.test.js @@ -1,19 +1,10 @@ -const path = require('path') const test = require('ava') const sinon = require('sinon') const db = require('../../db') const hydra = require('../../lib/hydra') -const { dropTables } = require('../utils') - -const migrationsDirectory = path.join( - __dirname, - '..', - '..', - 'db', - 'migrations' -) +const { resetDb } = require('../utils') let app let dbClient @@ -55,11 +46,7 @@ test.before(async () => { console.log('Connecting to test database...') dbClient = await db() - console.log('Dropping tables...') - await dropTables(dbClient) - - console.log('Migrating...') - await dbClient.migrate.latest({ directory: migrationsDirectory }) + await resetDb(dbClient) console.log('Starting server...') app = await require('../../index')() diff --git a/app/tests/api/organization-api.test.js b/app/tests/api/organization-api.test.js index f6d88400..41388338 100644 --- a/app/tests/api/organization-api.test.js +++ b/app/tests/api/organization-api.test.js @@ -1,19 +1,18 @@ -const path = require('path') const test = require('ava') const sinon = require('sinon') const db = require('../../db') +const { resetDb } = require('../utils') const team = require('../../lib/team') const organization = require('../../lib/organization') const permissions = require('../../manage/permissions') -const migrationsDirectory = path.join(__dirname, '..', '..', 'db', 'migrations') - let agent test.before(async () => { const conn = await db() - await conn.migrate.latest({ directory: migrationsDirectory }) + + await resetDb(conn) // seed await conn('users').insert({ id: 1 }) @@ -36,12 +35,6 @@ test.before(async () => { agent = require('supertest').agent(await require('../../index')()) }) -test.after.always(async () => { - const conn = await db() - await conn.migrate.rollback({ directory: migrationsDirectory }) - conn.destroy() -}) - /** * Test create an organization */ diff --git a/app/tests/api/organization-model.test.js b/app/tests/api/organization-model.test.js index 913e842b..372acfde 100644 --- a/app/tests/api/organization-model.test.js +++ b/app/tests/api/organization-model.test.js @@ -1,15 +1,14 @@ -const path = require('path') const test = require('ava') const { prop, map, contains } = require('ramda') const db = require('../../db') const organization = require('../../lib/organization') const team = require('../../lib/team') - -const migrationsDirectory = path.join(__dirname, '..', '..', 'db', 'migrations') +const { resetDb } = require('../utils') test.before(async () => { const conn = await db() - await conn.migrate.latest({ directory: migrationsDirectory }) + + await resetDb(conn) // seed await conn('users').insert({ id: 1 }) @@ -18,12 +17,6 @@ test.before(async () => { await conn('users').insert({ id: 4 }) }) -test.after.always(async () => { - const conn = await db() - await conn.migrate.rollback({ directory: migrationsDirectory }) - conn.destroy() -}) - /** * Test organization creation * An organization is created by a user. diff --git a/app/tests/api/profile-api.test.js b/app/tests/api/profile-api.test.js index 2be3ff1d..e1f386b6 100644 --- a/app/tests/api/profile-api.test.js +++ b/app/tests/api/profile-api.test.js @@ -1,4 +1,3 @@ -const path = require('path') const test = require('ava') const sinon = require('sinon') @@ -7,16 +6,16 @@ const team = require('../../lib/team') const org = require('../../lib/organization') const permissions = require('../../manage/permissions') const profile = require('../../lib/profile') +const { resetDb } = require('../utils') const { prop, concat, includes, propEq, find } = require('ramda') -const migrationsDirectory = path.join(__dirname, '..', '..', 'db', 'migrations') - let agent test.before(async () => { const conn = await db() - await conn.migrate.latest({ directory: migrationsDirectory }) + + await resetDb(conn) // seed await conn('users').insert({ id: 1 }) @@ -39,12 +38,6 @@ test.before(async () => { agent = require('supertest').agent(await require('../../index')()) }) -test.after.always(async () => { - const conn = await db() - await conn.migrate.rollback({ directory: migrationsDirectory }) - conn.destroy() -}) - /** * Get a team user profile with correct visibility */ diff --git a/app/tests/api/profile-model.test.js b/app/tests/api/profile-model.test.js index 9cea4307..6449fd40 100644 --- a/app/tests/api/profile-model.test.js +++ b/app/tests/api/profile-model.test.js @@ -1,18 +1,16 @@ - -const path = require('path') const { range, map, contains, prop, propEq, find, includes } = require('ramda') const test = require('ava') const db = require('../../db') const organization = require('../../lib/organization') const team = require('../../lib/team') const profile = require('../../lib/profile') +const { resetDb } = require('../utils') const { ValidationError, PropertyRequiredError } = require('../../lib/utils') -const migrationsDirectory = path.join(__dirname, '..', '..', 'db', 'migrations') - test.before(async () => { const conn = await db() - await conn.migrate.latest({ directory: migrationsDirectory }) + + await resetDb(conn) // seed await conn('users').insert({ id: 1 }) @@ -26,12 +24,6 @@ test.before(async () => { await conn('users').insert({ id: 9 }) }) -test.after.always(async () => { - const conn = await db() - await conn.migrate.rollback({ directory: migrationsDirectory }) - conn.destroy() -}) - test('add attributes for a public user profile', async (t) => { const name = 'Age' const visibility = 'public' diff --git a/app/tests/api/team-api.test.js b/app/tests/api/team-api.test.js index aed3475a..59364047 100644 --- a/app/tests/api/team-api.test.js +++ b/app/tests/api/team-api.test.js @@ -1,4 +1,3 @@ -const path = require('path') const test = require('ava') const sinon = require('sinon') const { any } = require('ramda') @@ -6,13 +5,13 @@ const { any } = require('ramda') const db = require('../../db') const team = require('../../lib/team') const permissions = require('../../manage/permissions') - -const migrationsDirectory = path.join(__dirname, '..', '..', 'db', 'migrations') +const { resetDb } = require('../utils') let agent test.before(async () => { const conn = await db() - await conn.migrate.latest({ directory: migrationsDirectory }) + + await resetDb(conn) // seed await conn('users').insert({ id: 1 }) @@ -39,12 +38,6 @@ test.before(async () => { agent = require('supertest').agent(await require('../../index')()) }) -test.after.always(async () => { - const conn = await db() - await conn.migrate.rollback({ directory: migrationsDirectory }) - conn.destroy() -}) - test('create a team', async t => { let res = await agent.post('/api/teams') .send({ name: 'road team 1' }) diff --git a/app/tests/api/team-model.test.js b/app/tests/api/team-model.test.js index faaebd81..faccf33e 100644 --- a/app/tests/api/team-model.test.js +++ b/app/tests/api/team-model.test.js @@ -1,14 +1,13 @@ -const path = require('path') const test = require('ava') const db = require('../../db') const team = require('../../lib/team') const { prop } = require('ramda') - -const migrationsDirectory = path.join(__dirname, '..', '..', 'db', 'migrations') +const { resetDb } = require('../utils') test.before(async () => { const conn = await db() - await conn.migrate.latest({ directory: migrationsDirectory }) + + await resetDb(conn) // seed await conn('users').insert({ id: 1 }) @@ -17,12 +16,6 @@ test.before(async () => { await conn('users').insert({ id: 4 }) }) -test.after.always(async () => { - const conn = await db() - await conn.migrate.rollback({ directory: migrationsDirectory }) - conn.destroy() -}) - test('create a team', async (t) => { const data = await team.create({ name: 'map team 1' }, 1) const members = await team.getMembers(data.id) diff --git a/app/tests/utils.js b/app/tests/utils.js index 33496596..763e5733 100644 --- a/app/tests/utils.js +++ b/app/tests/utils.js @@ -1,4 +1,14 @@ -async function dropTables (db) { +const path = require('path') + +const migrationsDirectory = path.join( + __dirname, + '..', + 'db', + 'migrations' +) + +async function resetDb (db) { + console.log('Dropping tables...') const pgres = await db.raw(` SELECT 'drop table "' || tablename || '" cascade;' AS drop @@ -12,8 +22,11 @@ async function dropTables (db) { for (const r of pgres.rows) { await db.raw(r.drop) } + + console.log('Migrating...') + await db.migrate.latest({ directory: migrationsDirectory }) } module.exports = { - dropTables + resetDb } From 9d82c27819c8b2275697192fe9049197fd5efe86 Mon Sep 17 00:00:00 2001 From: Vitor George Date: Mon, 14 Mar 2022 11:24:14 +0000 Subject: [PATCH 11/11] Move route wrapper to utils file, add documentation header --- app/manage/badges.js | 475 ++++++++++++++++++++++--------------------- app/manage/utils.js | 30 ++- 2 files changed, 277 insertions(+), 228 deletions(-) diff --git a/app/manage/badges.js b/app/manage/badges.js index fa11579c..47eee591 100644 --- a/app/manage/badges.js +++ b/app/manage/badges.js @@ -2,247 +2,268 @@ const db = require('../db') const yup = require('yup') const organization = require('../lib/organization') const profile = require('../lib/profile') +const { routeWrapper } = require('./utils') -function route ({ validate, handler }) { - return async (req, reply) => { +/** + * Get the list of badges of an organization + */ +const listBadges = routeWrapper({ + validate: { + params: yup + .object({ + id: yup.number().required().positive().integer() + }) + .required() + }, + handler: async function (req, reply) { try { - if (validate.params) { - req.params = await validate.params.validate(req.params) - } - - if (validate.body) { - req.body = await validate.body.validate(req.body) - } - } catch (error) { - console.log(error) - reply.boom.badRequest(error) + const conn = await db() + const badges = await conn('organization_badge') + .select('*') + .where('organization_id', req.params.id) + reply.send(badges) + } catch (err) { + console.log(err) + return reply.boom.badRequest(err.message) } - await handler(req, reply) } -} +}) -module.exports = { - listBadges: route({ - validate: { - params: yup - .object({ - id: yup.number().required().positive().integer() - }) - .required() - }, - handler: async function (req, reply) { - try { - const conn = await db() - const badges = await conn('organization_badge') - .select('*') - .where('organization_id', req.params.id) - reply.send(badges) - } catch (err) { - console.log(err) - return reply.boom.badRequest(err.message) - } - } - }), - createBadge: route({ - validate: { - params: yup - .object({ - id: yup.number().required().positive().integer() - }) - .required(), - body: yup - .object({ - name: yup.string().required(), - color: yup.string().required() +/** + * Create organization badge + */ +const createBadge = routeWrapper({ + validate: { + params: yup + .object({ + id: yup.number().required().positive().integer() + }) + .required(), + body: yup + .object({ + name: yup.string().required(), + color: yup.string().required() + }) + .required() + }, + handler: async function (req, reply) { + try { + const conn = await db() + const [badge] = await conn('organization_badge') + .insert({ + organization_id: req.params.id, + ...req.body }) - .required() - }, - handler: async function (req, reply) { - try { - const conn = await db() - const [badge] = await conn('organization_badge') - .insert({ - organization_id: req.params.id, - ...req.body - }) - .returning('*') - reply.send(badge) - } catch (err) { - console.log(err) - return reply.boom.badRequest(err.message) - } + .returning('*') + reply.send(badge) + } catch (err) { + console.log(err) + return reply.boom.badRequest(err.message) } - }), - patchBadge: route({ - validate: { - params: yup - .object({ - id: yup.number().required().positive().integer(), - badgeId: yup.number().required().positive().integer() - }) - .required(), - body: yup - .object({ - name: yup.string().optional(), - color: yup.string().optional() - }) - .required() - }, - handler: async function (req, reply) { - try { - const conn = await db() - const [badge] = await conn('organization_badge') - .update(req.body) - .where('id', req.params.badgeId) - .returning('*') - reply.send(badge) - } catch (err) { - console.log(err) - return reply.boom.badRequest(err.message) - } + } +}) + +/** + * Edit organization badge + */ +const patchBadge = routeWrapper({ + validate: { + params: yup + .object({ + id: yup.number().required().positive().integer(), + badgeId: yup.number().required().positive().integer() + }) + .required(), + body: yup + .object({ + name: yup.string().optional(), + color: yup.string().optional() + }) + .required() + }, + handler: async function (req, reply) { + try { + const conn = await db() + const [badge] = await conn('organization_badge') + .update(req.body) + .where('id', req.params.badgeId) + .returning('*') + reply.send(badge) + } catch (err) { + console.log(err) + return reply.boom.badRequest(err.message) } - }), - deleteBadge: route({ - validate: { - params: yup - .object({ - badgeId: yup.number().required().positive().integer() - }) - .required() - }, - handler: async function (req, reply) { - try { - const conn = await db() - await conn('organization_badge') - .delete() - .where('id', req.params.badgeId) - return reply.sendStatus(200) - } catch (err) { - console.log(err) - return reply.boom.badRequest(err.message) - } + } +}) + +/** + * Delete organization badge + */ +const deleteBadge = routeWrapper({ + validate: { + params: yup + .object({ + badgeId: yup.number().required().positive().integer() + }) + .required() + }, + handler: async function (req, reply) { + try { + const conn = await db() + await conn('organization_badge') + .delete() + .where('id', req.params.badgeId) + return reply.sendStatus(200) + } catch (err) { + console.log(err) + return reply.boom.badRequest(err.message) } - }), - assignUserBadge: route({ - validate: { - params: yup - .object({ - id: yup.number().required().positive().integer(), - badgeId: yup.number().required().positive().integer(), - userId: yup.number().required().positive().integer() - }) - .required(), - body: yup - .object({ - assigned_at: yup.date().optional(), - valid_until: yup.date().optional() + } +}) + +/** + * Assign organization badge to an user + */ +const assignUserBadge = routeWrapper({ + validate: { + params: yup + .object({ + id: yup.number().required().positive().integer(), + badgeId: yup.number().required().positive().integer(), + userId: yup.number().required().positive().integer() + }) + .required(), + body: yup + .object({ + assigned_at: yup.date().optional(), + valid_until: yup.date().optional() + }) + }, + handler: async function (req, reply) { + try { + const conn = await db() + + // user is member + await organization.isMember(req.params.id, req.params.userId) + + // assign badge + const [badge] = await conn('user_badge') + .insert({ + user_id: req.params.userId, + badge_id: req.params.badgeId, + assigned_at: req.body.assigned_at, + valid_until: req.body.valid_until }) - }, - handler: async function (req, reply) { - try { - const conn = await db() - - // user is member - await organization.isMember(req.params.id, req.params.userId) - - // assign badge - const [badge] = await conn('user_badge') - .insert({ - user_id: req.params.userId, - badge_id: req.params.badgeId, - assigned_at: req.body.assigned_at, - valid_until: req.body.valid_until - }) - .returning('*') - - reply.send(badge) - } catch (err) { - console.log(err) - return reply.boom.badRequest(err.message) - } + .returning('*') + + reply.send(badge) + } catch (err) { + console.log(err) + return reply.boom.badRequest(err.message) } - }), - listUserBadges: route({ - validate: { - params: yup - .object({ - userId: yup.number().required().positive().integer() - }) - .required() - }, - handler: async function (req, reply) { - try { - const badges = await profile.getUserBadges(req.params.userId) - reply.send({ badges }) - } catch (err) { - console.log(err) - return reply.boom.badRequest(err.message) - } + } +}) + +/** + * List badges of an user + */ +const listUserBadges = routeWrapper({ + validate: { + params: yup + .object({ + userId: yup.number().required().positive().integer() + }) + .required() + }, + handler: async function (req, reply) { + try { + const badges = await profile.getUserBadges(req.params.userId) + reply.send({ badges }) + } catch (err) { + console.log(err) + return reply.boom.badRequest(err.message) } - }), - updateUserBadge: route({ - validate: { - params: yup - .object({ - badgeId: yup.number().required().positive().integer(), - userId: yup.number().required().positive().integer() + } +}) + +/** + * Update a badge assigned to an user + */ +const updateUserBadge = routeWrapper({ + validate: { + params: yup + .object({ + badgeId: yup.number().required().positive().integer(), + userId: yup.number().required().positive().integer() + }) + .required(), + body: yup + .object({ + assigned_at: yup.date().optional(), + valid_until: yup.date().optional() + }) + }, + handler: async function (req, reply) { + try { + const conn = await db() + + // assign badge + const [badge] = await conn('user_badge') + .update({ + assigned_at: req.body.assigned_at, + valid_until: req.body.valid_until }) - .required(), - body: yup - .object({ - assigned_at: yup.date().optional(), - valid_until: yup.date().optional() + .where({ + user_id: req.params.userId, + badge_id: req.params.badgeId }) - }, - handler: async function (req, reply) { - try { - const conn = await db() - - // assign badge - const [badge] = await conn('user_badge') - .update({ - assigned_at: req.body.assigned_at, - valid_until: req.body.valid_until - }) - .where({ - user_id: req.params.userId, - badge_id: req.params.badgeId - }) - .returning('*') - - reply.send(badge) - } catch (err) { - console.log(err) - return reply.boom.badRequest(err.message) - } + .returning('*') + + reply.send(badge) + } catch (err) { + console.log(err) + return reply.boom.badRequest(err.message) } - }), - removeUserBadge: route({ - validate: { - params: yup - .object({ - badgeId: yup.number().required().positive().integer(), - userId: yup.number().required().positive().integer() - }) - .required() - }, - handler: async function (req, reply) { - try { - const conn = await db() - - // delete user badge - await conn('user_badge') - .delete() - .where({ - user_id: req.params.userId, - badge_id: req.params.badgeId - }) - - reply.sendStatus(200) - } catch (err) { - console.log(err) - return reply.boom.badRequest(err.message) - } + } +}) + +/** + * Remove badge assign to an user + */ +const removeUserBadge = routeWrapper({ + validate: { + params: yup + .object({ + badgeId: yup.number().required().positive().integer(), + userId: yup.number().required().positive().integer() + }) + .required() + }, + handler: async function (req, reply) { + try { + const conn = await db() + + // delete user badge + await conn('user_badge').delete().where({ + user_id: req.params.userId, + badge_id: req.params.badgeId + }) + + reply.sendStatus(200) + } catch (err) { + console.log(err) + return reply.boom.badRequest(err.message) } - }) + } +}) + +module.exports = { + listBadges, + createBadge, + patchBadge, + deleteBadge, + assignUserBadge, + listUserBadges, + updateUserBadge, + removeUserBadge } diff --git a/app/manage/utils.js b/app/manage/utils.js index 28eab9b1..6f144bf0 100644 --- a/app/manage/utils.js +++ b/app/manage/utils.js @@ -28,6 +28,34 @@ async function teamsMembersModeratorsHelper (teamsData) { }) } +/** + * Route wrapper to perform validation before processing + * the request. + * @param {function} config.validate Yup validation schema + * @param {function} config.handler Handler to execute if validation pass + * + * @returns {function} Route middleware function + */ +function routeWrapper (config) { + const { validate, handler } = config + return async (req, reply) => { + try { + if (validate.params) { + req.params = await validate.params.validate(req.params) + } + + if (validate.body) { + req.body = await validate.body.validate(req.body) + } + } catch (error) { + console.log(error) + reply.boom.badRequest(error) + } + await handler(req, reply) + } +} + module.exports = { - teamsMembersModeratorsHelper + teamsMembersModeratorsHelper, + routeWrapper }