const should = require('should'); const sinon = require('sinon'); const utils = require('../../../../core/server/data/migrations/utils'); class Deferred { constructor() { this.promise = new Promise((resolve, reject) => { this.resolve = async (val) => { resolve(val); return this.promise; }; this.reject = async (val) => { reject(val); return this.promise; }; }); } } describe('migrations/utils', function () { describe('createTransactionalMigration', function () { it('Accepts two functions and creates a transactional migration from them', function () { const config = { transacting: {} }; const up = sinon.spy(); const down = sinon.spy(); const migration = utils.createTransactionalMigration(up, down); const upResult = migration.up(config); const downResult = migration.down(config); should.ok(migration.config.transaction, 'Migration config should set transaction to true'); should.ok(upResult instanceof Promise, 'Migration up method should return a Promise'); should.ok(downResult instanceof Promise, 'Migration down method should return a Promise'); should.ok(up.calledOnceWith(config.transacting), 'up function should be called with transacting'); should.ok(down.calledOnceWith(config.transacting), 'down function should be called with transacting'); }); }); describe('combineTransactionalMigrations', function () { it('Accepts a number of migrations and returns a single migration', async function () { const config = { transacting: {} }; const upA = sinon.stub().resolves(); const downA = sinon.stub().resolves(); const upB = sinon.stub().resolves(); const downB = sinon.stub().resolves(); const migrationA = utils.createTransactionalMigration(upA, downA); const migrationB = utils.createTransactionalMigration(upB, downB); const combinedMigration = utils.combineTransactionalMigrations(migrationA, migrationB); should.ok(combinedMigration.config.transaction, 'Migration config should set transaction to true'); const upResult = combinedMigration.up(config); should.ok(upResult instanceof Promise, 'Migration up method should return a Promise'); await upResult; should.ok(upA.calledOnceWith(config.transacting), 'first up fn should be called with transacting'); should.ok(upB.calledOnceWith(config.transacting), 'second up fn should be called with transacting'); should.ok(upA.calledBefore(upB), 'Migration up method should call child up methods in order'); const downResult = combinedMigration.down(config); should.ok(downResult instanceof Promise, 'Migration down method should return a Promise'); await downResult; should.ok(downA.calledOnceWith(config.transacting), 'first down fn should be called with transacting'); should.ok(downB.calledOnceWith(config.transacting), 'second down fn should be called with transacting'); should.ok(downB.calledBefore(downA), 'Migration down method should call child down methods in reverse order'); }); it('Waits for each migration to finish before executing the next', async function () { const config = { transacting: {} }; const upDeferredA = new Deferred(); const upDeferredB = new Deferred(); const downDeferredA = new Deferred(); const downDeferredB = new Deferred(); const upA = sinon.stub().returns(upDeferredA.promise); const downA = sinon.stub().returns(downDeferredA.promise); const upB = sinon.stub().returns(upDeferredB.promise); const downB = sinon.stub().returns(downDeferredB.promise); const migrationA = utils.createTransactionalMigration(upA, downA); const migrationB = utils.createTransactionalMigration(upB, downB); const combinedMigration = utils.combineTransactionalMigrations(migrationA, migrationB); combinedMigration.up(config); should.ok(!upB.called, 'second up fn should not be called until first up fn has resolved'); await upDeferredA.resolve(); should.ok(upB.calledOnce, 'second up fn should be called once first up fn has resolved'); combinedMigration.down(config); should.ok(!downA.called, 'penultimate down fn should not be called until last down fn has resolved'); await downDeferredB.resolve(); should.ok(downA.calledOnce, 'penultimate down fn should be called once last down fn has resolved'); }); it('Stops execution if a migration errors, and propagates error', async function () { const config = { transacting: {} }; const upDeferredA = new Deferred(); const upDeferredB = new Deferred(); const downDeferredA = new Deferred(); const downDeferredB = new Deferred(); const upA = sinon.stub().returns(upDeferredA.promise); const downA = sinon.stub().returns(downDeferredA.promise); const upB = sinon.stub().returns(upDeferredB.promise); const downB = sinon.stub().returns(downDeferredB.promise); const migrationA = utils.createTransactionalMigration(upA, downA); const migrationB = utils.createTransactionalMigration(upB, downB); const combinedMigration = utils.combineTransactionalMigrations(migrationA, migrationB); const upResult = combinedMigration.up(config); should.ok(!upB.called, 'second up fn should not be called until first up fn has resolved'); try { await upDeferredA.reject(new Error('some reason')); } catch (err) { // } finally { should.ok(!upB.called, 'second up fn should not be called if first up fn has rejected'); } let upError = null; try { await upResult; } catch (err) { upError = true; } finally { should.ok(upError, 'Migration up should error if child errors'); } const downResult = combinedMigration.down(config); should.ok(!downA.called, 'penultimate down fn should not be called until last down fn has resolved'); try { await downDeferredB.reject(new Error('some reason')); } catch (err) { // } finally { should.ok(!downA.called, 'penultimate down fn should not be called if last down fn has rejected'); } let downErr = null; try { await downResult; } catch (err) { downErr = err; } finally { should.ok(downErr, 'Migration down should error if child errors'); } }); }); }); const Knex = require('knex'); const ObjectId = require('bson-objectid').default; async function setupDb() { const knex = Knex({ client: 'sqlite', connection: { filename: ':memory:' }, // Suppress warnings from knex useNullAsDefault: true }); await knex.raw(` CREATE TABLE \`permissions\` ( \`id\` varchar(24) not null, \`name\` varchar(50) not null, \`object_type\` varchar(50) not null, \`action_type\` varchar(50) not null, \`object_id\` varchar(24) null, \`created_at\` datetime not null, \`created_by\` varchar(24) not null, \`updated_at\` datetime null, \`updated_by\` varchar(24) null, primary key (\`id\`) ); CREATE UNIQUE INDEX \`permissions_name_unique\` on \`permissions\` (\`name\`);\`); `); await knex.raw(` CREATE TABLE \`permissions_roles\` ( \`id\` varchar(24) not null, \`role_id\` varchar(24) not null, \`permission_id\` varchar(24) not null, primary key (\`id\`) ); `); await knex.raw(` CREATE TABLE \`roles\` ( \`id\` varchar(24) not null, \`name\` varchar(50) not null, \`description\` varchar(2000) null, \`created_at\` datetime not null, \`created_by\` varchar(24) not null, \`updated_at\` datetime null, \`updated_by\` varchar(24) null, primary key (\`id\`) ); CREATE UNIQUE INDEX \`roles_name_unique\` on \`roles\` (\`name\`); `); const date = knex.raw('CURRENT_TIMESTAMP'); await knex('roles').insert({ id: ObjectId().toHexString(), name: 'Role Name', description: 'Role description', created_at: date, created_by: utils.meta.MIGRATION_USER, updated_at: date, updated_by: utils.meta.MIGRATION_USER }); await knex('roles').insert({ id: ObjectId().toHexString(), name: 'Other Role Name', description: 'Other Role description', created_at: date, created_by: utils.meta.MIGRATION_USER, updated_at: date, updated_by: utils.meta.MIGRATION_USER }); await knex('permissions').insert({ id: ObjectId().toHexString(), name: 'Permission Name', action_type: 'action', object_type: 'object', created_at: date, created_by: utils.meta.MIGRATION_USER, updated_at: date, updated_by: utils.meta.MIGRATION_USER }); return knex; } async function runUpMigration(knex, migration) { { const transacting = await knex.transaction(); await migration.up({transacting}); transacting.commit(); } return async function runDownMigration() { const transacting = await knex.transaction(); await migration.down({transacting}); transacting.commit(); }; } describe('migrations/utils/permissions', function () { describe('addPermission', function () { it('Accepts a name, action and object and returns a migration which adds a permission to the database', async function () { const knex = await setupDb(); const migration = utils.addPermission({ name: 'scarface', object: 'my little friend', action: 'say hello' }); should.ok(migration.config.transaction, 'addPermission creates a transactional migration'); const runDownMigration = await runUpMigration(knex, migration); const allPermissionsAfterUp = await knex('permissions').select(); const insertedPermissionsAfterUp = allPermissionsAfterUp.filter((row) => { return row.name === 'scarface'; }); should.ok(insertedPermissionsAfterUp.length === 1, 'The permission was inserted into the database'); await runDownMigration(); const allPermissionsAfterDown = await knex('permissions').select(); const insertedPermissionsAfterDown = allPermissionsAfterDown.filter((row) => { return row.name === 'scarface'; }); should.ok(insertedPermissionsAfterDown.length === 0, 'The permission was removed'); }); }); describe('addPermissionToRole', function () { it('Accepts a permisson name and role name and returns a migration which adds a permission to the database', async function () { const knex = await setupDb(); const migration = utils.addPermissionToRole({ permission: 'Permission Name', role: 'Role Name' }); const runDownMigration = await runUpMigration(knex, migration); const allPermissionsForRoleAfterUp = await knex.raw(` SELECT p.name FROM permissions p INNER JOIN permissions_roles pr ON pr.permission_id = p.id INNER JOIN roles r ON pr.role_id = r.id WHERE r.name = 'Role Name'; `); const attachedPermissionAfterUp = allPermissionsForRoleAfterUp.find((row) => { return row.name === 'Permission Name'; }); should.ok(attachedPermissionAfterUp, 'The permission was attached to the role.'); await runDownMigration(); const allPermissionsForRoleAfterDown = await knex.raw(` SELECT p.name FROM permissions p INNER JOIN permissions_roles pr ON pr.permission_id = p.id INNER JOIN roles r ON pr.role_id = r.id WHERE r.name = 'Role Name'; `); const attachedPermissionAfterDown = allPermissionsForRoleAfterDown.find((row) => { return row.name === 'Permission Name'; }); should.ok(!attachedPermissionAfterDown, 'The permission was removed from the role.'); }); }); describe('addPermissionWithRoles', function () { it('Accepts addPermission config and a list of roles, and creates the permission, linking it to roles', async function () { const knex = await setupDb(); const migration = utils.addPermissionWithRoles({ name: 'scarface', object: 'my little friend', action: 'say hello' }, [ 'Role Name', 'Other Role Name' ]); const runDownMigration = await runUpMigration(knex, migration); const allPermissionsAfterUp = await knex('permissions').select(); const insertedPermissionsAfterUp = allPermissionsAfterUp.filter((row) => { return row.name === 'scarface'; }); should.ok(insertedPermissionsAfterUp.length === 1, 'The permission was inserted into the database'); const allPermissionsForRoleAfterUp = await knex.raw(` SELECT p.name FROM permissions p INNER JOIN permissions_roles pr ON pr.permission_id = p.id INNER JOIN roles r ON pr.role_id = r.id WHERE r.name = 'Role Name'; `); const permissionAttachedToRoleAfterUp = allPermissionsForRoleAfterUp.find((row) => { return row.name === 'scarface'; }); should.ok(permissionAttachedToRoleAfterUp, 'The permission was attached to the role.'); const allPermissionsForOtherRoleAfterUp = await knex.raw(` SELECT p.name FROM permissions p INNER JOIN permissions_roles pr ON pr.permission_id = p.id INNER JOIN roles r ON pr.role_id = r.id WHERE r.name = 'Other Role Name'; `); const permissionAttachedToOtherRoleAfterUp = allPermissionsForRoleAfterUp.find((row) => { return row.name === 'scarface'; }); should.ok(permissionAttachedToOtherRoleAfterUp, 'The permission was attached to the other role.'); await runDownMigration(); const allPermissionsAfterDown = await knex('permissions').select(); const insertedPermissionsAfterDown = allPermissionsAfterDown.filter((row) => { return row.name === 'scarface'; }); should.ok(insertedPermissionsAfterDown.length === 0, 'The permission was removed from the database'); const allPermissionsForRoleAfterDown = await knex.raw(` SELECT p.name FROM permissions p INNER JOIN permissions_roles pr ON pr.permission_id = p.id INNER JOIN roles r ON pr.role_id = r.id WHERE r.name = 'Role Name'; `); const permissionAttachedToRoleAfterDown = allPermissionsForRoleAfterDown.find((row) => { return row.name === 'scarface'; }); should.ok(!permissionAttachedToRoleAfterDown, 'The permission was removed from the role.'); const allPermissionsForOtherRoleAfterDown = await knex.raw(` SELECT p.name FROM permissions p INNER JOIN permissions_roles pr ON pr.permission_id = p.id INNER JOIN roles r ON pr.role_id = r.id WHERE r.name = 'Other Role Name'; `); const permissionAttachedToOtherRoleAfterDown = allPermissionsForOtherRoleAfterDown.find((row) => { return row.name === 'scarface'; }); should.ok(!permissionAttachedToOtherRoleAfterDown, 'The permission was removed from the other role.'); }); }); });