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/db/migrations/20220302104250_add_user_badges.js b/app/db/migrations/20220302104250_add_user_badges.js new file mode 100644 index 00000000..df91e74a --- /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.datetime('assigned_at').defaultTo(knex.fn.now()) + table.datetime('valid_until') + }) +} + +exports.down = async function (knex) { + await knex.schema.dropTable('user_badge') +} 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 new file mode 100644 index 00000000..47eee591 --- /dev/null +++ b/app/manage/badges.js @@ -0,0 +1,269 @@ +const db = require('../db') +const yup = require('yup') +const organization = require('../lib/organization') +const profile = require('../lib/profile') +const { routeWrapper } = require('./utils') + +/** + * 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 { + 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) + } + } +}) + +/** + * 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 + }) + .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) + } + } +}) + +/** + * 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) + } + } +}) + +/** + * 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 + }) + .returning('*') + + reply.send(badge) + } 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) + } + } +}) + +/** + * 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 + }) + .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) + } + } +}) + +/** + * 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/index.js b/app/manage/index.js index 3e517e03..e8500d5a 100644 --- a/app/manage/index.js +++ b/app/manage/index.js @@ -39,6 +39,17 @@ const { getOrgStaff } = require('./organizations') +const { + createBadge, + patchBadge, + deleteBadge, + listBadges, + assignUserBadge, + listUserBadges, + updateUserBadge, + removeUserBadge +} = require('./badges') + const { getUserTeamProfile, createProfileKeys, @@ -128,6 +139,55 @@ function manageRouter (nextApp) { router.post('/api/organizations/:id/teams', can('organization:create-team'), createOrgTeam) router.get('/api/organizations/:id/teams', getOrgTeams) + /** + * Manage organization badges + */ + router.get( + '/api/organizations/:id/badges', + can('organization:edit'), + listBadges + ) + router.post( + '/api/organizations/:id/badges', + can('organization:edit'), + createBadge + ) + router.patch( + '/api/organizations/:id/badges/:badgeId', + can('organization:edit'), + patchBadge + ) + router.delete( + '/api/organizations/:id/badges/:badgeId', + can('organization:edit'), + deleteBadge + ) + + /** + * Manage user badges + */ + router.post( + '/api/organizations/:id/badges/:badgeId/assign/:userId', + can('organization:edit'), + assignUserBadge + ) + router.get( + '/api/user/:userId/badges', + can('public:authenticated'), + listUserBadges + ) + router.patch( + `/api/organizations/:id/member/:userId/badge/:badgeId`, + can('organization:edit'), + updateUserBadge + ) + + 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/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 } diff --git a/app/tests/api/badges-api.test.js b/app/tests/api/badges-api.test.js new file mode 100644 index 00000000..0329224c --- /dev/null +++ b/app/tests/api/badges-api.test.js @@ -0,0 +1,420 @@ +const test = require('ava') +const sinon = require('sinon') + +const db = require('../../db') +const hydra = require('../../lib/hydra') + +const { resetDb } = require('../utils') + +let app +let dbClient +let org1 +let orgTeam1 +let orgOwner = { + id: 1 +} +let orgManager = { + id: 2 +} +let orgTeamMember = { + id: 3 +} +let notOrgMember = { + id: 4 +} +let badge1, badge2, badge3 + +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}`) +} + +test.before(async () => { + console.log('Connecting to test database...') + dbClient = await db() + + await resetDb(dbClient) + + console.log('Starting server...') + app = await require('../../index')() + + // Create user agents + console.log('Creating agents...') + 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 () => { + dbClient.destroy() +}) + +/** + * CREATE BADGE + */ +test('Create badge', async (t) => { + // Owners can create badges + badge1 = ( + await orgOwner.agent + .post(`/api/organizations/${org1.id}/badges`) + .send({ name: 'badge 1', color: 'red' }) + .expect(200) + ).body + + t.deepEqual(badge1, { + id: 1, + organization_id: 1, + name: 'badge 1', + color: 'red' + }) + + // Manager are not allowed + await orgManager.agent + .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}/badges`) + .send({ name: 'badge 1', color: 'red' }) + .expect(401) + + // Non-members are not-allowed + await notOrgMember.agent + .post(`/api/organizations/${org1.id}/badges`) + .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}/badges/${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}/badges/${badge1.id}`) + .send({ name: 'badge 1', color: 'red' }) + .expect(401) + + // Disallow org team Members + await orgTeamMember.agent + .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}/badges/${badge1.id}`) + .send({ name: 'badge 1', color: 'red' }) + .expect(401) +}) + +/** + * LIST BADGES + */ +test('List badges', async (t) => { + // Add more badges + badge2 = (await orgOwner.agent + .post(`/api/organizations/${org1.id}/badges`) + .send({ name: 'badge number 2', color: 'green' }) + .expect(200)).body + + // Add more badges + 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 + const badgesList = ( + await orgOwner.agent.get(`/api/organizations/${org1.id}/badges`).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' + }, + { + id: 4, + organization_id: 1, + name: 'badge number 4', + color: 'pink' + } + ]) +}) + +/** + * DELETE BADGE + */ +test('Delete badge', async (t) => { + // Disallow managers + await orgManager.agent + .delete(`/api/organizations/${org1.id}/badges/${badge1.id}`) + .expect(401) + + // Disallow org team Members + await orgTeamMember.agent + .delete(`/api/organizations/${org1.id}/badges/${badge1.id}`) + .expect(401) + + // Disallow non-members + await notOrgMember.agent + .delete(`/api/organizations/${org1.id}/badges/${badge1.id}`) + .expect(401) + + // Allow owners + await orgOwner.agent + .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}/badges`).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' + }, + { + id: 4, + organization_id: 1, + name: 'badge number 4', + color: 'pink' + } + ]) +}) + +/** + * 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 orgTeamMember.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}`) + .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, + 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' + } + ] }) +}) + +/** + * UPDATE BADGE + */ +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) + .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' + }) +}) + +/** + * 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' + } + ] }) +}) 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 new file mode 100644 index 00000000..763e5733 --- /dev/null +++ b/app/tests/utils.js @@ -0,0 +1,32 @@ +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 + FROM + pg_tables + WHERE + schemaname = 'public' + AND tablename != 'spatial_ref_sys' + `) + + for (const r of pgres.rows) { + await db.raw(r.drop) + } + + console.log('Migrating...') + await db.migrate.latest({ directory: migrationsDirectory }) +} + +module.exports = { + resetDb +} diff --git a/package.json b/package.json index 01d15da1..167ba0ca 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,8 @@ "url-parse": "^1.5.9", "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 70fb9453..8712cf59 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" @@ -7199,6 +7211,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" @@ -8446,6 +8463,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" @@ -10202,6 +10224,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" @@ -10990,6 +11017,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"