From d7e4694dfa327c25e6e5a75223e6443bf7ce8ea5 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 25 May 2020 12:02:38 +0200 Subject: [PATCH] feat: Write integration test for users. --- .../containers/Authentication/InviteAccept.js | 3 +- server/src/http/controllers/Authentication.js | 2 +- server/src/http/controllers/InviteUsers.js | 38 ++- server/src/models/TenantUser.js | 12 +- server/src/system/models/SystemUser.js | 6 +- server/tests/routes/inviteUsers.test.js | 259 ++++++++++++++++++ 6 files changed, 303 insertions(+), 17 deletions(-) create mode 100644 server/tests/routes/inviteUsers.test.js diff --git a/client/src/containers/Authentication/InviteAccept.js b/client/src/containers/Authentication/InviteAccept.js index cb746408a..4a4321428 100644 --- a/client/src/containers/Authentication/InviteAccept.js +++ b/client/src/containers/Authentication/InviteAccept.js @@ -37,7 +37,8 @@ function Invite({ requestInviteAccept, requestInviteMetaByToken }) { last_name: Yup.string().required().label(formatMessage({id:'last_name_'})), phone_number: Yup.string() .matches() - .required().label(formatMessage({id:''})), + .required() + .label(formatMessage({id:'phone_number'})), password: Yup.string() .min(4) .required().label(formatMessage({id:'password'})) diff --git a/server/src/http/controllers/Authentication.js b/server/src/http/controllers/Authentication.js index 1cb22316b..9dfa1b78f 100644 --- a/server/src/http/controllers/Authentication.js +++ b/server/src/http/controllers/Authentication.js @@ -1,4 +1,3 @@ - import express from 'express'; import { check, validationResult } from 'express-validator'; import path from 'path'; @@ -176,6 +175,7 @@ export default { await TenantUser.bindKnex(tenantDb).query().insert({ ...userInsert, + invite_accepted_at: moment().format('YYYY/MM/DD HH:mm:ss'), }); Logger.log('info', 'New tenant has been created.', { organizationId }); diff --git a/server/src/http/controllers/InviteUsers.js b/server/src/http/controllers/InviteUsers.js index 472f7af66..8d741a75d 100644 --- a/server/src/http/controllers/InviteUsers.js +++ b/server/src/http/controllers/InviteUsers.js @@ -9,6 +9,7 @@ import { import path from 'path'; import fs from 'fs'; import Mustache from 'mustache'; +import moment from 'moment'; import mail from '@/services/mail'; import { hashPassword } from '@/utils'; import SystemUser from '@/system/models/SystemUser'; @@ -64,10 +65,10 @@ export default { }); } const form = { ...req.body }; - const foundUser = await SystemUser.query() - .where('email', form.email).first(); - const { user } = req; + const { TenantUser } = req.models; + const foundUser = await SystemUser.query() + .where('email', form.email).first(); if (foundUser) { return res.status(400).send({ @@ -80,6 +81,10 @@ export default { tenant_id: user.tenantId, token, }); + const tenantUser = await TenantUser.query().insert({ + first_name: form.email, + email: form.email, + }); const { Option } = req.models; const organizationOptions = await Option.query() .where('key', 'organization_name'); @@ -117,11 +122,18 @@ export default { check('first_name').exists().trim().escape(), check('last_name').exists().trim().escape(), check('phone_number').exists().trim().escape(), - // check('language').exists().isIn(['ar', 'en']), check('password').exists().trim().escape(), param('token').exists().trim().escape(), ], async handler(req, res) { + const validationErrors = validationResult(req); + + if (!validationErrors.isEmpty()) { + return res.boom.badData(null, { + code: 'validation_error', ...validationErrors, + }); + } + const { token } = req.params; const inviteToken = await Invite.query() .where('token', token).first(); @@ -160,7 +172,7 @@ export default { const userForm = { first_name: form.first_name, last_name: form.last_name, - email: form.email, + email: inviteToken.email, phone_number: form.phone_number, language: form.language, active: 1, @@ -175,19 +187,29 @@ export default { errors: [{ type: 'PHONE_NUMBER.ALREADY.EXISTS', code: 400 }], }); } + const insertUserOper = TenantUser.bindKnex(tenantDb) - .query().insert({ ...userForm }); + .query() + .where('email', userForm.email) + .patch({ + ...userForm, + invite_accepted_at: moment().format('YYYY/MM/DD'), + }); + const insertSysUserOper = SystemUser.query().insert({ ...userForm, password: hashedPassword, + tenant_id: inviteToken.tenantId, }); + const deleteInviteTokenOper = Invite.query() + .where('token', inviteToken.token).delete(); + await Promise.all([ insertUserOper, insertSysUserOper, + deleteInviteTokenOper, ]); - await Invite.query().where('token', token).delete(); - return res.status(200).send(); }, }, diff --git a/server/src/models/TenantUser.js b/server/src/models/TenantUser.js index 783d7a0f0..48bdc3be7 100644 --- a/server/src/models/TenantUser.js +++ b/server/src/models/TenantUser.js @@ -1,11 +1,13 @@ import bcrypt from 'bcryptjs'; -import { Model } from 'objection'; +import { Model, mixin } from 'objection'; import TenantModel from '@/models/TenantModel'; +import DateSession from '@/models/DateSession'; // import PermissionsService from '@/services/PermissionsService'; -export default class TenantUser extends TenantModel { - // ...PermissionsService - +export default class TenantUser extends mixin(TenantModel, [DateSession]) { + /** + * Virtual attributes. + */ static get virtualAttributes() { return ['fullName']; } @@ -49,6 +51,6 @@ export default class TenantUser extends TenantModel { } fullName() { - return `${this.firstName} ${this.lastName}`; + return `${this.firstName} ${this.lastName || ''}`; } } \ No newline at end of file diff --git a/server/src/system/models/SystemUser.js b/server/src/system/models/SystemUser.js index 4f470907b..57b494e84 100644 --- a/server/src/system/models/SystemUser.js +++ b/server/src/system/models/SystemUser.js @@ -1,8 +1,10 @@ -import { Model } from 'objection'; +import { Model, mixin } from 'objection'; import bcrypt from 'bcryptjs'; import SystemModel from '@/system/models/SystemModel'; +import DateSession from '@/models/DateSession'; -export default class SystemUser extends SystemModel { + +export default class SystemUser extends mixin(SystemModel, [DateSession]) { /** * Table name. */ diff --git a/server/tests/routes/inviteUsers.test.js b/server/tests/routes/inviteUsers.test.js new file mode 100644 index 000000000..89fdc5821 --- /dev/null +++ b/server/tests/routes/inviteUsers.test.js @@ -0,0 +1,259 @@ +import knex from '@/database/knex'; +import { + request, + expect, + createUser, +} from '~/testInit'; +import { + tenantWebsite, + tenantFactory, + loginRes +} from '~/dbInit'; +import Invite from '@/system/models/Invite' +import TenantUser from '@/models/TenantUser'; +import SystemUser from '@/system/models/SystemUser'; + +describe('routes: `/api/invite_users`', () => { + describe('POST: `/api/invite_users/send`', () => { + it('Should response unauthorized if the user was not authorized.', async () => { + const res = await request().get('/api/invite_users/send'); + + expect(res.status).equals(401); + expect(res.body.message).equals('Unauthorized'); + }); + + it('Should email be required.', async () => { + const res = await request() + .post('/api/invite/send') + .set('x-access-token', loginRes.body.token) + .set('organization-id', tenantWebsite.organizationId) + .send(); + + expect(res.status).equals(422); + expect(res.body.errors).include.something.deep.equals({ + msg: 'Invalid value', param: 'email', location: 'body', + }); + }); + + it('Should email not be already registered in the system database.', async () => { + const user = await createUser(tenantWebsite, { + active: false, + email: 'admin@admin.com', + }); + + const res = await request() + .post('/api/invite/send') + .set('x-access-token', loginRes.body.token) + .set('organization-id', tenantWebsite.organizationId) + .send({ + email: 'admin@admin.com', + }); + + expect(res.status).equals(400); + expect(res.body.errors).include.something.deep.equals({ + type: 'USER.EMAIL.ALREADY.REGISTERED', code: 100, + }); + }); + + it('Should invite token be inserted to the master database.', async () => { + const res = await request() + .post('/api/invite/send') + .set('x-access-token', loginRes.body.token) + .set('organization-id', tenantWebsite.organizationId) + .send({ + email: 'admin@admin.com' + }); + + const foundInviteToken = await Invite.query() + .where('email', 'admin@admin.com').first(); + + expect(foundInviteToken).is.not.null; + expect(foundInviteToken.token).is.not.null; + }); + + it('Should invite email be insereted to users tenant database.', async () => { + const res = await request() + .post('/api/invite/send') + .set('x-access-token', loginRes.body.token) + .set('organization-id', tenantWebsite.organizationId) + .send({ + email: 'admin@admin.com' + }); + + const foundTenantUser = await TenantUser.tenant().query() + .where('email', 'admin@admin.com').first(); + + expect(foundTenantUser).is.not.null; + expect(foundTenantUser.email).equals('admin@admin.com'); + expect(foundTenantUser.firstName).equals('admin@admin.com'); + expect(foundTenantUser.createdAt).is.not.null; + }); + }); + + describe('POST: `/api/invite/accept/:token`', () => { + let sendInviteRes; + let inviteUser; + + beforeEach(async () => { + sendInviteRes = await request() + .post('/api/invite/send') + .set('x-access-token', loginRes.body.token) + .set('organization-id', tenantWebsite.organizationId) + .send({ + email: 'admin@admin.com' + }); + + inviteUser = await Invite.query() + .where('email', 'admin@admin.com') + .first(); + }); + + it('Should the given token be valid.', async () => { + const res = await request() + .post('/api/invite/accept/invalid_token') + .send({ + first_name: 'Ahmed', + last_name: 'Bouhuolia', + password: 'hard-password', + phone_number: '0927918381', + }); + + expect(res.status).equals(404); + expect(res.body.errors).include.something.deep.equals({ + type: 'INVITE.TOKEN.NOT.FOUND', code: 300, + }); + }); + + it('Should first_name be required.', async () => { + const res = await request() + .post(`/api/invite/accept/${inviteUser.token}`) + .send(); + + expect(res.status).equals(422); + expect(res.body.errors).include.something.deep.equals({ + msg: 'Invalid value', param: 'first_name', location: 'body' + }); + }); + + it('Should last_name be required.', async () => { + const res = await request() + .post(`/api/invite/accept/${inviteUser.token}`) + .send(); + + expect(res.status).equals(422); + expect(res.body.errors).include.something.deep.equals({ + msg: 'Invalid value', param: 'last_name', location: 'body' + }); + }); + + it('Should phone_number be required.', async () => { + const res = await request() + .post(`/api/invite/accept/${inviteUser.token}`) + .send(); + + expect(res.status).equals(422); + expect(res.body.errors).include.something.deep.equals({ + msg: 'Invalid value', param: 'phone_number', location: 'body' + }); + }); + + it('Should password be required.', async () => { + const res = await request() + .post(`/api/invite/accept/${inviteUser.token}`) + .send(); + + expect(res.status).equals(422); + expect(res.body.errors).include.something.deep.equals({ + msg: 'Invalid value', param: 'password', location: 'body' + }); + }); + + it('Should phone number not be already registered.', async () => { + const user = await createUser(tenantWebsite); + const res = await request() + .post(`/api/invite/accept/${inviteUser.token}`) + .send({ + first_name: 'Ahmed', + last_name: 'Bouhuolia', + password: 'hard-password', + phone_number: user.phone_number, + }) + + expect(res.status).equals(400); + expect(res.body.errors).include.something.deep.equals({ + type: 'PHONE_MUMNER.ALREADY.EXISTS', code: 400, + }); + }); + + it('Should tenant user details updated after invite accept.', async () => { + const user = await createUser(tenantWebsite); + const res = await request() + .post(`/api/invite/accept/${inviteUser.token}`) + .send({ + first_name: 'Ahmed', + last_name: 'Bouhuolia', + password: 'hard-password', + phone_number: '0927918381', + }); + + const foundTenantUser = await TenantUser.tenant().query() + .where('email', 'admin@admin.com').first(); + + expect(foundTenantUser).is.not.null; + expect(foundTenantUser.id).is.not.null; + expect(foundTenantUser.email).equals('admin@admin.com'); + expect(foundTenantUser.firstName).equals('Ahmed'); + expect(foundTenantUser.lastName).equals('Bouhuolia'); + expect(foundTenantUser.active).equals(1); + expect(foundTenantUser.inviteAcceptedAt).is.not.null; + expect(foundTenantUser.createdAt).is.not.null; + expect(foundTenantUser.updatedAt).is.not.null; + }); + + it('Should user details be insereted to the system database', async () => { + const user = await createUser(tenantWebsite); + const res = await request() + .post(`/api/invite/accept/${inviteUser.token}`) + .send({ + first_name: 'Ahmed', + last_name: 'Bouhuolia', + password: 'hard-password', + phone_number: '0927918381', + }); + + const foundSystemUser = await SystemUser.query() + .where('email', 'admin@admin.com').first(); + + expect(foundSystemUser).is.not.null; + expect(foundSystemUser.id).is.not.null; + expect(foundSystemUser.tenantId).equals(inviteUser.tenantId); + expect(foundSystemUser.email).equals('admin@admin.com'); + expect(foundSystemUser.firstName).equals('Ahmed'); + expect(foundSystemUser.lastName).equals('Bouhuolia'); + expect(foundSystemUser.active).equals(1); + expect(foundSystemUser.lastLoginAt).is.null; + expect(foundSystemUser.createdAt).is.not.null; + expect(foundSystemUser.updatedAt).is.null; + }); + + it('Should invite token be deleted after invite accept.', async () => { + const res = await request() + .post(`/api/invite/accept/${inviteUser.token}`) + .send({ + first_name: 'Ahmed', + last_name: 'Bouhuolia', + password: 'hard-password', + phone_number: '0927918381', + }); + + const foundInviteToken = await Invite.query().where('token', inviteUser.token); + expect(foundInviteToken.length).equals(0); + }); + }); + + describe('GET: `/api/invite_users/:token`', () => { + it('Should response token invalid.', () => { + + }); + }); +}); \ No newline at end of file