diff --git a/server/src/database/factories/index.js b/server/src/database/factories/index.js index fc884287a..7b4f4b77a 100644 --- a/server/src/database/factories/index.js +++ b/server/src/database/factories/index.js @@ -7,7 +7,6 @@ const factory = knexFactory(knex); factory.define('user', 'users', async () => { const hashedPassword = await hashPassword('admin'); - return { first_name: faker.name.firstName(), last_name: faker.name.lastName(), @@ -32,7 +31,6 @@ factory.define('account_type', 'account_types', async () => ({ factory.define('account', 'accounts', async () => { const accountType = await factory.create('account_type'); - return { name: faker.lorem.word(), account_type_id: accountType.id, @@ -59,7 +57,6 @@ factory.define('item_metadata', 'items_metadata', async () => { factory.define('item', 'items', async () => { const category = await factory.create('item_category'); const account = await factory.create('account'); - return { name: faker.lorem.word(), note: faker.lorem.paragraph(), @@ -73,7 +70,6 @@ factory.define('item', 'items', async () => { factory.define('setting', 'settings', async () => { const user = await factory.create('user'); - return { key: faker.lorem.slug(), user_id: user.id, @@ -83,4 +79,45 @@ factory.define('setting', 'settings', async () => { }; }); +factory.define('role', 'roles', async () => ({ + name: faker.lorem.word(), + description: faker.lorem.words(), + predefined: false, +})); + +factory.define('user_has_role', 'user_has_roles', async () => { + const user = await factory.create('user'); + const role = await factory.create('role'); + + return { + user_id: user.id, + role_id: role.id, + }; +}); + +factory.define('permission', 'permissions', async () => { + const permissions = ['create', 'edit', 'delete', 'view', 'owner']; + const randomPermission = permissions[Math.floor(Math.random() * permissions.length)]; + + return { + name: randomPermission, + }; +}); + +factory.define('role_has_permission', 'role_has_permissions', async () => { + const permission = await factory.create('permission'); + const role = await factory.create('role'); + const resource = await factory.create('resource'); + + return { + role_id: role.id, + permission_id: permission.id, + resource_id: resource.id, + }; +}); + +factory.define('resource', 'resources', () => ({ + name: faker.lorem.word(), +})); + export default factory; diff --git a/server/src/database/migrations/20190423085238_create_permissions_table.js b/server/src/database/migrations/20190423085238_create_permissions_table.js new file mode 100644 index 000000000..21826ece5 --- /dev/null +++ b/server/src/database/migrations/20190423085238_create_permissions_table.js @@ -0,0 +1,11 @@ + +exports.up = function (knex) { + return knex.schema.createTable('permissions', (table) => { + table.increments(); + table.string('name'); + }); +}; + +exports.down = function (knex) { + return knex.schema.dropTable('permissions'); +}; diff --git a/server/src/database/migrations/20190423085240_create_resources_table.js b/server/src/database/migrations/20190423085240_create_resources_table.js new file mode 100644 index 000000000..1f9b99d40 --- /dev/null +++ b/server/src/database/migrations/20190423085240_create_resources_table.js @@ -0,0 +1,11 @@ + +exports.up = function (knex) { + return knex.schema.createTable('resources', (table) => { + table.increments(); + table.string('name'); + }); +}; + +exports.down = function (knex) { + return knex.schema.dropTable('resources'); +}; diff --git a/server/src/database/migrations/20190822214242_create_users_table.js b/server/src/database/migrations/20190822214242_create_users_table.js index c7b3425be..dd4260bcc 100644 --- a/server/src/database/migrations/20190822214242_create_users_table.js +++ b/server/src/database/migrations/20190822214242_create_users_table.js @@ -1,5 +1,5 @@ -exports.up = function(knex) { +exports.up = function (knex) { return knex.schema.createTable('users', (table) => { table.increments(); table.string('first_name'); @@ -15,6 +15,6 @@ exports.up = function(knex) { }); }; -exports.down = function(knex) { +exports.down = function (knex) { return knex.schema.dropTableIfExists('users'); }; diff --git a/server/src/database/migrations/20190822214244_create_user_has_roles_table.js b/server/src/database/migrations/20190822214244_create_user_has_roles_table.js new file mode 100644 index 000000000..44f49755a --- /dev/null +++ b/server/src/database/migrations/20190822214244_create_user_has_roles_table.js @@ -0,0 +1,12 @@ + +exports.up = function (knex) { + return knex.schema.createTable('user_has_roles', (table) => { + table.increments(); + table.integer('user_id').unsigned().references('id').inTable('users'); + table.integer('role_id').unsigned().references('id').inTable('roles'); + }); +}; + +exports.down = function (knex) { + return knex.schema.dropTableIfExists('user_has_roles'); +}; diff --git a/server/src/database/migrations/20190822214247_create_oauth_tokens_table.js b/server/src/database/migrations/20190822214247_create_oauth_tokens_table.js index c238dcc94..f91d4a01a 100644 --- a/server/src/database/migrations/20190822214247_create_oauth_tokens_table.js +++ b/server/src/database/migrations/20190822214247_create_oauth_tokens_table.js @@ -1,6 +1,6 @@ exports.up = function(knex) { - return knex.schema.createTable('oauth_tokens', table => { + return knex.schema.createTable('oauth_tokens', (table) => { table.increments(); table.string('access_token'); table.date('access_token_expires_on'); diff --git a/server/src/database/migrations/20190822214905_create_role_has_accounts.js b/server/src/database/migrations/20190822214905_create_role_has_accounts.js new file mode 100644 index 000000000..e66e7df5e --- /dev/null +++ b/server/src/database/migrations/20190822214905_create_role_has_accounts.js @@ -0,0 +1,10 @@ + +exports.up = function (knex) { + return knex.schema.createTable('role_has_accounts', (table) => { + table.increments(); + table.integer('role_id').unsigned().references('id').inTable('roles'); + table.integer('account_id').unsigned().references('id').inTable('accounts'); + }); +}; + +exports.down = (knex) => knex.schema.dropTableIfExists('role_has_accounts'); diff --git a/server/src/database/migrations/20190822214905_create_role_has_permissions.js b/server/src/database/migrations/20190822214905_create_role_has_permissions.js new file mode 100644 index 000000000..26e19e8be --- /dev/null +++ b/server/src/database/migrations/20190822214905_create_role_has_permissions.js @@ -0,0 +1,11 @@ + +exports.up = function (knex) { + return knex.schema.createTable('role_has_permissions', (table) => { + table.increments(); + table.integer('role_id').unsigned().references('id').inTable('roles'); + table.integer('permission_id').unsigned().references('id').inTable('permissions'); + table.integer('resource_id').unsigned().references('id').inTable('resources'); + }); +}; + +exports.down = (knex) => knex.schema.dropTableIfExists('role_has_permissions'); diff --git a/server/src/http/controllers/Roles.js b/server/src/http/controllers/Roles.js new file mode 100644 index 000000000..3b1b6ccfd --- /dev/null +++ b/server/src/http/controllers/Roles.js @@ -0,0 +1,351 @@ +/* eslint-disable no-unused-vars */ +import express from 'express'; +import { check, validationResult } from 'express-validator'; +import { difference } from 'lodash'; +import asyncMiddleware from '@/http/middleware/asyncMiddleware'; +import Role from '@/models/Role'; +import Permission from '@/models/Permission'; +import Resource from '@/models/Resource'; +import knex from '@/database/knex'; + +const AccessControllSchema = [ + { + resource: 'items', + label: 'products_services', + permissions: ['create', 'edit', 'delete', 'view'], + fullAccess: true, + ownControl: true, + }, +]; + +// eslint-disable-next-line arrow-body-style +const getResourceSchema = (resource) => AccessControllSchema.find((schema) => { + return schema.resource === resource; +}); + +const getResourcePermissions = (resource) => { + const foundResource = getResourceSchema(resource); + return foundResource ? foundResource.permissions : []; +}; + +export default { + + findNotFoundResources(resourcesSlugs) { + const schemaResourcesSlugs = AccessControllSchema.map((s) => s.resource); + return difference(resourcesSlugs, schemaResourcesSlugs); + }, + + findNotFoundPermissions(permissions, resourceSlug) { + const schemaPermissions = getResourcePermissions(resourceSlug); + return difference(permissions, schemaPermissions); + }, + + /** + * Router constructor method. + */ + router() { + const router = express.Router(); + + router.post('/:id', + this.editRole.validation, + asyncMiddleware(this.editRole.handler.bind(this))); + + // router.post('/', + // this.newRole.validation, + // asyncMiddleware(this.newRole.handler)); + + router.delete('/:id', + this.deleteRole.validation, + asyncMiddleware(this.deleteRole.handler)); + + return router; + }, + + /** + * Creates a new role. + */ + newRole: { + validation: [ + check('name').exists().trim().escape(), + check('description').optional().trim().escape(), + check('permissions').isArray({ min: 0 }), + check('permissions.*.resource_slug').exists().whitelist('^[a-z0-9]+(?:-[a-z0-9]+)*$'), + check('permissions.*.permissions').isArray({ min: 1 }), + ], + async handler(req, res) { + const validationErrors = validationResult(req); + + if (!validationErrors.isEmpty()) { + return res.boom.badData(null, { + code: 'validation_error', ...validationErrors, + }); + } + + const { name, description, permissions } = req.body; + + const resourcesSlugs = permissions.map((perm) => perm.resource_slug); + const permissionsSlugs = []; + + const resourcesNotFound = this.findNotFoundResources(resourcesSlugs); + const errorReasons = []; + const notFoundPermissions = []; + + if (resourcesNotFound.length > 0) { + errorReasons.push({ + type: 'RESOURCE_SLUG_NOT_FOUND', + code: 100, + resources: resourcesNotFound, + }); + } + permissions.forEach((perm) => { + const abilities = perm.permissions.map((ability) => ability); + + // Gets the not found permissions in the schema. + const notFoundAbilities = this.findNotFoundPermissions(abilities, perm.resource_slug); + + if (notFoundAbilities.length > 0) { + notFoundPermissions.push({ + resource_slug: perm.resource_slug, permissions: notFoundAbilities, + }); + } else { + const perms = perm.permissions || []; + perms.forEach((permission) => { + if (perms.indexOf(permission) !== -1) { + permissionsSlugs.push(permission); + } + }); + } + }); + if (notFoundPermissions.length > 0) { + errorReasons.push({ + type: 'PERMISSIONS_SLUG_NOT_FOUND', + code: 200, + permissions: notFoundPermissions, + }); + } + if (errorReasons.length > 0) { + return res.boom.badRequest(null, { errors: errorReasons }); + } + // Permissions. + const [resourcesCollection, permsCollection] = await Promise.all([ + Resource.query((query) => { query.whereIn('name', resourcesSlugs); }).fetchAll(), + Permission.query((query) => { query.whereIn('name', permissionsSlugs); }).fetchAll(), + ]); + + const notStoredResources = difference( + resourcesSlugs, resourcesCollection.map((s) => s.name), + ); + const notStoredPermissions = difference( + permissionsSlugs, permsCollection.map((perm) => perm.slug), + ); + + const insertThread = []; + + if (notStoredResources.length > 0) { + insertThread.push(knex('resources').insert([ + ...notStoredResources.map((resource) => ({ name: resource })), + ])); + } + if (notStoredPermissions.length > 0) { + insertThread.push(knex('permissions').insert([ + ...notStoredPermissions.map((permission) => ({ name: permission })), + ])); + } + + await Promise.all(insertThread); + + const [storedPermissions, storedResources] = await Promise.all([ + Permission.query((q) => { q.whereIn('name', permissionsSlugs); }).fetchAll(), + Resource.query((q) => { q.whereIn('name', resourcesSlugs); }).fetchAll(), + ]); + + const storedResourcesSet = new Map(storedResources.map((resource) => [ + resource.attributes.name, resource.attributes.id, + ])); + const storedPermissionsSet = new Map(storedPermissions.map((perm) => [ + perm.attributes.name, perm.attributes.id, + ])); + const role = Role.forge({ name, description }); + + await role.save(); + + const roleHasPerms = permissions.map((resource) => resource.permissions.map((perm) => ({ + role_id: role.id, + resource_id: storedResourcesSet.get(resource.resource_slug), + permission_id: storedPermissionsSet.get(perm), + }))); + + if (roleHasPerms.length > 0) { + await knex('role_has_permissions').insert(roleHasPerms[0]); + } + return res.status(200).send({ id: role.get('id') }); + }, + }, + + /** + * Edit the give role. + */ + editRole: { + validation: [ + check('name').exists().trim().escape(), + check('description').optional().trim().escape(), + check('permissions').isArray({ min: 0 }), + check('permissions.*.resource_slug').exists().whitelist('^[a-z0-9]+(?:-[a-z0-9]+)*$'), + check('permissions.*.permissions').isArray({ min: 1 }), + ], + async handler(req, res) { + const validationErrors = validationResult(req); + + if (!validationErrors.isEmpty()) { + return res.boom.badData(null, { + code: 'validation_error', ...validationErrors, + }); + } + + const { id } = req.params; + const role = await Role.where('id', id).fetch(); + + if (!role) { + return res.boom.notFound(null, { + errors: [{ type: 'ROLE_NOT_FOUND', code: 100 }], + }); + } + + const { permissions } = req.body; + const errorReasons = []; + const permissionsSlugs = []; + const notFoundPermissions = []; + + const resourcesSlugs = permissions.map((perm) => perm.resource_slug); + const resourcesNotFound = this.findNotFoundResources(resourcesSlugs); + + if (resourcesNotFound.length > 0) { + errorReasons.push({ + type: 'RESOURCE_SLUG_NOT_FOUND', + code: 100, + resources: resourcesNotFound, + }); + } + + permissions.forEach((perm) => { + const abilities = perm.permissions.map((ability) => ability); + // Gets the not found permissions in the schema. + const notFoundAbilities = this.findNotFoundPermissions(abilities, perm.resource_slug); + + if (notFoundAbilities.length > 0) { + notFoundPermissions.push({ + resource_slug: perm.resource_slug, permissions: notFoundAbilities, + }); + } else { + const perms = perm.permissions || []; + perms.forEach((permission) => { + if (perms.indexOf(permission) !== -1) { + permissionsSlugs.push(permission); + } + }); + } + }); + + if (notFoundPermissions.length > 0) { + errorReasons.push({ + type: 'PERMISSIONS_SLUG_NOT_FOUND', + code: 200, + permissions: notFoundPermissions, + }); + } + if (errorReasons.length > 0) { + return res.boom.badRequest(null, { errors: errorReasons }); + } + + // Permissions. + const [resourcesCollection, permsCollection] = await Promise.all([ + Resource.query((query) => { query.whereIn('name', resourcesSlugs); }).fetchAll(), + Permission.query((query) => { query.whereIn('name', permissionsSlugs); }).fetchAll(), + ]); + + const notStoredResources = difference( + resourcesSlugs, resourcesCollection.map((s) => s.name), + ); + const notStoredPermissions = difference( + permissionsSlugs, permsCollection.map((perm) => perm.slug), + ); + const insertThread = []; + + if (notStoredResources.length > 0) { + insertThread.push(knex('resources').insert([ + ...notStoredResources.map((resource) => ({ name: resource })), + ])); + } + if (notStoredPermissions.length > 0) { + insertThread.push(knex('permissions').insert([ + ...notStoredPermissions.map((permission) => ({ name: permission })), + ])); + } + + await Promise.all(insertThread); + + const [storedPermissions, storedResources] = await Promise.all([ + Permission.query((q) => { q.whereIn('name', permissionsSlugs); }).fetchAll(), + Resource.query((q) => { q.whereIn('name', resourcesSlugs); }).fetchAll(), + ]); + + const storedResourcesSet = new Map(storedResources.map((resource) => [ + resource.attributes.name, resource.attributes.id, + ])); + const storedPermissionsSet = new Map(storedPermissions.map((perm) => [ + perm.attributes.name, perm.attributes.id, + ])); + + await role.save(); + + + const savedRoleHasPerms = await knex('role_has_permissions').where({ + role_id: role.id, + }); + + console.log(savedRoleHasPerms); + + // const roleHasPerms = permissions.map((resource) => resource.permissions.map((perm) => ({ + // role_id: role.id, + // resource_id: storedResourcesSet.get(resource.resource_slug), + // permission_id: storedPermissionsSet.get(perm), + // }))); + + // if (roleHasPerms.length > 0) { + // await knex('role_has_permissions').insert(roleHasPerms[0]); + // } + return res.status(200).send({ id: role.get('id') }); + }, + }, + + deleteRole: { + validation: [], + async handler(req, res) { + const { id } = req.params; + const role = await Role.where('id', id).fetch(); + + if (!role) { + return res.boom.notFound(); + } + if (role.attributes.predefined) { + return res.boom.badRequest(null, { + errors: [{ type: 'ROLE_PREDEFINED', code: 100 }], + }); + } + + await knex('role_has_permissions') + .where('role_id', role.id).delete({ require: false }); + + await role.destroy(); + + return res.status(200).send(); + }, + }, + + getRole: { + validation: [], + handler(req, res) { + return res.status(200).send(); + }, + }, +}; diff --git a/server/src/http/index.js b/server/src/http/index.js index 6f9f734a5..ec57e0503 100644 --- a/server/src/http/index.js +++ b/server/src/http/index.js @@ -1,6 +1,7 @@ // import OAuth2 from '@/http/controllers/OAuth2'; import Authentication from '@/http/controllers/Authentication'; import Users from '@/http/controllers/Users'; +import Roles from '@/http/controllers/Roles'; import Items from '@/http/controllers/Items'; import ItemCategories from '@/http/controllers/ItemCategories'; import Accounts from '@/http/controllers/Accounts'; @@ -10,6 +11,7 @@ export default (app) => { // app.use('/api/oauth2', OAuth2.router()); app.use('/api/auth', Authentication.router()); app.use('/api/users', Users.router()); + app.use('/api/roles', Roles.router()); app.use('/api/accounts', Accounts.router()); app.use('/api/accountOpeningBalance', AccountOpeningBalance.router()); app.use('/api/items', Items.router()); diff --git a/server/src/http/middleware/authorization.js b/server/src/http/middleware/authorization.js new file mode 100644 index 000000000..3caf12897 --- /dev/null +++ b/server/src/http/middleware/authorization.js @@ -0,0 +1,4 @@ + +const authorization = (req, res, next) => { + const { user } = req; +}; \ No newline at end of file diff --git a/server/src/models/Account.js b/server/src/models/Account.js index 49f0a9ed6..ca6d2f94a 100644 --- a/server/src/models/Account.js +++ b/server/src/models/Account.js @@ -20,7 +20,7 @@ const Account = bookshelf.Model.extend({ balances() { return this.hasMany('AccountBalance', 'accounnt_id'); - } + }, }); export default bookshelf.model('Account', Account); diff --git a/server/src/models/AccountBalance.js b/server/src/models/AccountBalance.js index 64dfb1b17..74d277865 100644 --- a/server/src/models/AccountBalance.js +++ b/server/src/models/AccountBalance.js @@ -12,9 +12,7 @@ const AccountBalance = bookshelf.Model.extend({ */ hasTimestamps: false, - /** - * - */ + account() { return this.belongsTo('Account', 'account_id'); }, diff --git a/server/src/models/Permission.js b/server/src/models/Permission.js new file mode 100644 index 000000000..33b674bec --- /dev/null +++ b/server/src/models/Permission.js @@ -0,0 +1,25 @@ +import bookshelf from './bookshelf'; + +const Permission = bookshelf.Model.extend({ + + /** + * Table name of Role model. + * @type {String} + */ + tableName: 'permissions', + + /** + * Timestamp columns. + */ + hasTimestamps: false, + + role() { + return this.belongsTo('Role', 'role_id'); + }, + + resource() { + return this.belongsTo('Resource', 'resource_id'); + }, +}); + +export default bookshelf.model('Permission', Permission); diff --git a/server/src/models/Resource.js b/server/src/models/Resource.js new file mode 100644 index 000000000..3df709899 --- /dev/null +++ b/server/src/models/Resource.js @@ -0,0 +1,21 @@ +import bookshelf from './bookshelf'; + +const Resource = bookshelf.Model.extend({ + /** + * Table name. + */ + tableName: 'resources', + + /** + * Timestamp columns. + */ + hasTimestamps: false, + + permissions() { + }, + + roles() { + }, +}); + +export default bookshelf.model('Resource', Resource); diff --git a/server/src/models/Role.js b/server/src/models/Role.js index a15807b4c..c9452e435 100644 --- a/server/src/models/Role.js +++ b/server/src/models/Role.js @@ -20,11 +20,18 @@ const Role = bookshelf.Model.extend({ return this.belongsToMany('Permission', 'role_has_permissions', 'role_id', 'permission_id'); }, + /** + * Role may has many resources. + */ + resources() { + return this.belongsToMany('Resource', 'role_has_permissions', 'role_id', 'resource_id'); + }, + /** * Role model may has many users. */ users() { - return this.belongsTo('User'); + return this.belongsToMany('User', 'user_has_roles'); }, }); diff --git a/server/src/models/User.js b/server/src/models/User.js index ae611cc36..575bda159 100644 --- a/server/src/models/User.js +++ b/server/src/models/User.js @@ -21,6 +21,13 @@ const User = bookshelf.Model.extend({ verifyPassword(password) { return bcrypt.compareSync(password, this.get('password')); }, + + /** + * User model may has many associated roles. + */ + roles() { + return this.belongsToMany('Role', 'user_has_roles', 'user_id', 'role_id'); + }, }); export default bookshelf.model('User', User); diff --git a/server/tests/models/AccountType.test.js b/server/tests/models/AccountType.test.js index 70f71ac3a..439f47a8c 100644 --- a/server/tests/models/AccountType.test.js +++ b/server/tests/models/AccountType.test.js @@ -2,7 +2,7 @@ import { create, expect } from '~/testInit'; import '@/models/Account'; import AccountType from '@/models/AccountType'; -describe.only('Model: AccountType', () => { +describe('Model: AccountType', () => { it('Shoud account type model has many associated accounts.', async () => { const accountType = await create('account_type'); await create('account', { account_type_id: accountType.id }); diff --git a/server/tests/models/Permission.test.js b/server/tests/models/Permission.test.js new file mode 100644 index 000000000..f0e7f81ee --- /dev/null +++ b/server/tests/models/Permission.test.js @@ -0,0 +1,21 @@ +import { create } from '~/testInit'; +import Resource from '@/models/Resource'; +import '@/models/Role'; + +describe('Model: Permission', () => { + it('Permission model may has associated role.', async () => { + const roleHasPermissions = await create('role_has_permission'); + const resourceModel = await Resource.where('id', roleHasPermissions.resource_id).fetch(); + const roleModel = await resourceModel.role().fetch(); + + console.log(roleModel); + }); + + it('Permission model may has associated resource.', async () => { + const roleHasPermissions = await create('role_has_permission'); + const resourceModel = await Resource.where('id', roleHasPermissions.resource_id).fetch(); + const permissionModel = await resourceModel.permission().fetch(); + + console.log(permissionModel); + }); +}); diff --git a/server/tests/models/Resource.test.js b/server/tests/models/Resource.test.js new file mode 100644 index 000000000..e69de29bb diff --git a/server/tests/models/Role.test.js b/server/tests/models/Role.test.js new file mode 100644 index 000000000..80f867218 --- /dev/null +++ b/server/tests/models/Role.test.js @@ -0,0 +1,34 @@ +import { expect, create } from '~/testInit'; +import Role from '@/models/Role'; +import '@/models/Permission'; +import '@/models/Resource'; + +describe('Model: Role', () => { + it('Role model may has many associated users', async () => { + const userHasRole = await create('user_has_role'); + await create('user_has_role', { role_id: userHasRole.role_id }); + + const roleModel = await Role.where('id', userHasRole.role_id).fetch(); + const roleUsers = await roleModel.users().fetch(); + + expect(roleUsers).to.have.lengthOf(2); + }); + + it('Role model may has many associated permissions.', async () => { + const roleHasPermissions = await create('role_has_permission'); + + const roleModel = await Role.where('id', roleHasPermissions.role_id).fetch(); + const rolePermissions = await roleModel.permissions().fetch(); + + expect(rolePermissions).to.have.lengthOf(1); + }); + + it('Role model may has many associated resources that has some or all permissions.', async () => { + const roleHasPermissions = await create('role_has_permission'); + + const roleModel = await Role.where('id', roleHasPermissions.role_id).fetch(); + const roleResources = await roleModel.resources().fetch(); + + expect(roleResources).to.have.lengthOf(1); + }); +}); diff --git a/server/tests/models/User.test.js b/server/tests/models/User.test.js new file mode 100644 index 000000000..cb80e09e5 --- /dev/null +++ b/server/tests/models/User.test.js @@ -0,0 +1,15 @@ +import { create, expect } from '~/testInit'; +import User from '@/models/User'; +import '@/models/Role'; + +describe('Model: User', () => { + it('User model may has many associated roles.', async () => { + const userHasRole = await create('user_has_role'); + await create('user_has_role', { user_id: userHasRole.user_id }); + + const userModel = await User.where('id', userHasRole.user_id).fetch(); + const userRoles = await userModel.roles().fetch(); + + expect(userRoles).to.have.lengthOf(2); + }); +}); diff --git a/server/tests/routes/accountOpeningBalance.test.js b/server/tests/routes/accountOpeningBalance.test.js index 6f03b9184..13eb004ab 100644 --- a/server/tests/routes/accountOpeningBalance.test.js +++ b/server/tests/routes/accountOpeningBalance.test.js @@ -1,6 +1,6 @@ import { request, expect } from '~/testInit'; -describe.only('routes: `/accountOpeningBalance`', () => { +describe('routes: `/accountOpeningBalance`', () => { describe('POST `/accountOpeningBalance`', () => { it('Should `accounts` be array type.', async () => { const res = await request().post('/api/accountOpeningBalance').send({ @@ -28,7 +28,7 @@ describe.only('routes: `/accountOpeningBalance`', () => { expect(res.status).equals(422); }); - it.only('Should `accounts.*.id` be exist in the storage.', async () => { + it('Should `accounts.*.id` be exist in the storage.', async () => { const res = await request().post('/api/accountOpeningBalance').send({ accounts: [ { id: 100, credit: 100, debit: 100 }, diff --git a/server/tests/routes/roles.test.js b/server/tests/routes/roles.test.js index 60173431f..e365c6c22 100644 --- a/server/tests/routes/roles.test.js +++ b/server/tests/routes/roles.test.js @@ -1,26 +1,276 @@ +import { request, expect, create } from '~/testInit'; +import knex from '@/database/knex'; -describe('routes: `/roles/`', () => { +describe.only('routes: `/roles/`', () => { describe('POST: `/roles/`', () => { - it('Should name be required.', () => { + it('Should `name` be required.', async () => { + const res = await request().post('/api/roles').send(); + expect(res.status).equals(422); + expect(res.body.code).equals('validation_error'); + + const foundNameParam = res.body.errors.find((err) => err.param === 'name'); + expect(!!foundNameParam).equals(true); }); - it('Should `permissions` be ', () => { + it('Should `permissions` be array.', async () => { + const res = await request().post('/api/roles').send({ + permissions: 'not_array', + }); + expect(res.status).equals(422); + expect(res.body.code).equals('validation_error'); + + const foundPermissionsPerm = res.body.errors.find((err) => err.param === 'permissions'); + expect(!!foundPermissionsPerm).equals(true); + }); + + it('Should `permissions.resource_slug` be slug.', async () => { + const res = await request().post('/api/roles').send({ + permissions: [{ slug: 'not slug' }], + }); + + expect(res.status).equals(422); + expect(res.body.code).equals('validation_error'); + + const foundPerm = res.body.errors.find((err) => err.param === 'permissions[0].resource_slug'); + expect(!!foundPerm).equals(true); + }); + + it('Should `permissions.permissions be array.`', async () => { + const res = await request().post('/api/roles').send({ + permissions: [{ permissions: 'not_array' }], + }); + + expect(res.status).equals(422); + expect(res.body.code).equals('validation_error'); + + const foundPerm = res.body.errors.find((err) => err.param === 'permissions[0].permissions'); + expect(!!foundPerm).equals(true); + }); + + it('Should response bad request in case the resource slug was invalid.', async () => { + const res = await request().post('/api/roles').send({ + name: 'name', + permissions: [{ resource_slug: 'invalid', permissions: ['item'] }], + }); + + expect(res.status).equals(400); + expect(res.body.errors).include.something.that.deep.equals({ + type: 'RESOURCE_SLUG_NOT_FOUND', + code: 100, + resources: ['invalid'], + }); + }); + + it('Should response bad request in case the permission type was invalid.', async () => { + const res = await request().post('/api/roles').send({ + name: 'name', + permissions: [{ resource_slug: 'items', permissions: ['item'] }], + }); + + expect(res.status).equals(400); + expect(res.body.errors).include.something.that.deep.equals({ + type: 'PERMISSIONS_SLUG_NOT_FOUND', + code: 200, + permissions: [{ resource_slug: 'items', permissions: ['item'] }], + }); + }); + + it('Should save the submit resources in the storage in case was not exist.', async () => { + await request().post('/api/roles').send({ + name: 'Role Name', + permissions: [{ resource_slug: 'items', permissions: ['create'] }], + }); + + const storedResources = await knex('resources'); + expect(storedResources).to.have.lengthOf(1); + }); + + it('Should save the submit permissions in the storage in case was not exist.', async () => { + await request().post('/api/roles').send({ + name: 'Role Name', + permissions: [{ resource_slug: 'items', permissions: ['create'] }], + }); + + const storedPermissions = await knex('permissions'); + expect(storedPermissions).to.have.lengthOf(1); + }); + + it('Should save the submit role in the storage with associated resource and permissions.', async () => { + await request().post('/api/roles').send({ + name: 'Role Name', + description: 'Role description', + permissions: [{ resource_slug: 'items', permissions: ['create', 'view'] }], + }); + + const storedRoles = await knex('roles'); + const storedResource = await knex('resources').where('name', 'items').first(); + const storedPermissions = await knex('permissions'); + const roleHasPermissions = await knex('role_has_permissions') + .where('role_id', storedRoles[0].id); + + expect(storedRoles).to.have.lengthOf(1); + expect(storedRoles[0].name).equals('Role Name'); + expect(storedRoles[0].description).equals('Role description'); + + expect(roleHasPermissions).to.have.lengthOf(2); + expect(roleHasPermissions[0].role_id).equals(storedRoles[0].id); + expect(roleHasPermissions[0].permission_id).equals(storedPermissions[0].id); + expect(roleHasPermissions[0].resource_id).equals(storedResource.id); }); it('Should response success with correct data format.', async () => { + const res = await request().post('/api/roles').send(); + expect(res.status).equals(422); }); - it('Should save the given role details in the storage.', () => { + it('Should save the given role details in the storage.', async () => { + const res = await request().post('/api/roles').send(); + expect(res.status).equals(422); + }); + }); + + describe('POST: `/roles/:id`', () => { + it('Should response not found in case role was not exist.', async () => { + const res = await request().post('/api/roles/10').send({ + name: 'Role Name', + description: 'Description', + permissions: [ + { resource_slug: 'items', permissions: ['create'] }, + ], + }); + + expect(res.status).equals(404); + }); + + it('Should `name` be required.', async () => { + const role = await create('role'); + const res = await request().post(`/api/roles/${role.id}`).send(); + + expect(res.status).equals(422); + expect(res.body.code).equals('validation_error'); + + const foundNameParam = res.body.errors.find((err) => err.param === 'name'); + expect(!!foundNameParam).equals(true); + }); + + it('Should `permissions` be array.', async () => { + const role = await create('role'); + const res = await request().post(`/api/roles/${role.id}`).send({ + permissions: 'not_array', + }); + + expect(res.status).equals(422); + expect(res.body.code).equals('validation_error'); + + const foundPermissionsPerm = res.body.errors.find((err) => err.param === 'permissions'); + expect(!!foundPermissionsPerm).equals(true); + }); + + it('Should `permissions.resource_slug` be slug.', async () => { + const role = await create('role'); + const res = await request().post(`/api/roles/${role.id}`).send({ + permissions: [{ slug: 'not slug' }], + }); + + expect(res.status).equals(422); + expect(res.body.code).equals('validation_error'); + + const foundPerm = res.body.errors.find((err) => err.param === 'permissions[0].resource_slug'); + expect(!!foundPerm).equals(true); + }); + + it('Should `permissions.permissions be array.`', async () => { + const role = await create('role'); + const res = await request().post(`/api/roles/${role.id}`).send({ + permissions: [{ permissions: 'not_array' }], + }); + + expect(res.status).equals(422); + expect(res.body.code).equals('validation_error'); + + const foundPerm = res.body.errors.find((err) => err.param === 'permissions[0].permissions'); + expect(!!foundPerm).equals(true); + }); + + it('Should response bad request in case the resource slug was invalid.', async () => { + const role = await create('role'); + const res = await request().post(`/api/roles/${role.id}`).send({ + name: 'name', + permissions: [{ resource_slug: 'invalid', permissions: ['item'] }], + }); + + expect(res.status).equals(400); + expect(res.body.errors).include.something.that.deep.equals({ + type: 'RESOURCE_SLUG_NOT_FOUND', + code: 100, + resources: ['invalid'], + }); + }); + + it('Should response bad request in case the permission type was invalid.', async () => { + const role = await create('role'); + const res = await request().post(`/api/roles/${role.id}`).send({ + name: 'name', + permissions: [{ resource_slug: 'items', permissions: ['item'] }], + }); + + expect(res.status).equals(400); + expect(res.body.errors).include.something.that.deep.equals({ + type: 'PERMISSIONS_SLUG_NOT_FOUND', + code: 200, + permissions: [{ resource_slug: 'items', permissions: ['item'] }], + }); + }); + + it('Should save the submit resources in the storage in case was not exist.', async () => { + const role = await create('role'); + await request().post(`/api/roles/${role.id}`).send({ + name: 'Role Name', + permissions: [{ resource_slug: 'items', permissions: ['create'] }], + }); + + const storedResources = await knex('resources'); + expect(storedResources).to.have.lengthOf(1); + }); + + it.only('Should save the submit permissions in the storage in case was not exist.', async () => { + const role = await create('role'); + await request().post(`/api/roles/${role.id}`).send({ + name: 'Role Name', + permissions: [{ resource_slug: 'items', permissions: ['create'] }], + }); + + const storedPermissions = await knex('permissions'); + expect(storedPermissions).to.have.lengthOf(1); }); }); describe('DELETE: `/roles/:id`', () => { - it('Should not delete the predefined role.', () => { + it('Should response not found in case the role was not exist.', async () => { + const res = await request().delete('/api/roles/100').send(); + expect(res.status).equals(404); + }); + + it('Should not delete the predefined role.', async () => { + const role = await create('role', { predefined: true }); + const res = await request().delete(`/api/roles/${role.id}`).send(); + + expect(res.status).equals(400); + }); + + it('Should delete the given role and its relations with permissions and resources.', async () => { + const role = await create('role'); + await create('role_has_permission', { role_id: role.id }); + + await request().delete(`/api/roles/${role.id}`).send(); + + const storedRole = await knex('roles').where('id', role.id).first(); + expect(storedRole).to.equals(undefined); }); }); });