mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-22 18:31:57 +03:00
b36d0cc1c4
refs https://github.com/TryGhost/Team/issues/1178 The "up" migration that this util generates correctly throws if the pre-requisite data cannot be found in the database. The "down" migration however was incorrectly mirroring this behaviour of throwing - which meant that it wasn't idempotent, as it does not require a permission or role to existing if it wants to move relations between them.
559 lines
21 KiB
JavaScript
559 lines
21 KiB
JavaScript
const should = require('should');
|
|
const sinon = require('sinon');
|
|
const errors = require('@tryghost/errors');
|
|
|
|
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 permission 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('Throws errors', function () {
|
|
it('Throws when permission cannot be found in up migration', async function () {
|
|
const knex = await setupDb();
|
|
|
|
const migration = utils.addPermissionToRole({
|
|
permission: 'Unimaginable',
|
|
role: 'Not there'
|
|
});
|
|
|
|
let runDownMigration;
|
|
try {
|
|
runDownMigration = await runUpMigration(knex, migration);
|
|
should.fail('addPermissionToRole up migration did not throw');
|
|
} catch (err) {
|
|
should.equal(err instanceof errors.GhostError, true);
|
|
err.message.should.equal('Cannot add permission(Unimaginable) with role(Not there) - permission does not exist');
|
|
}
|
|
});
|
|
|
|
it('Does not throw when permission cannot be found in down migration', async function () {
|
|
const knex = await setupDb();
|
|
|
|
const migration = utils.addPermissionToRole({
|
|
permission: 'Permission Name',
|
|
role: 'Role Name'
|
|
});
|
|
|
|
const runDownMigration = await runUpMigration(knex, migration);
|
|
await knex('permissions')
|
|
.where('name', '=', 'Permission Name')
|
|
.del();
|
|
|
|
await runDownMigration(knex, migration);
|
|
});
|
|
|
|
it('Throws when role cannot be found', async function () {
|
|
const knex = await setupDb();
|
|
|
|
const migration = utils.addPermissionToRole({
|
|
permission: 'Permission Name',
|
|
role: 'Not there'
|
|
});
|
|
|
|
try {
|
|
await runUpMigration(knex, migration);
|
|
should.fail('addPermissionToRole did not throw');
|
|
} catch (err) {
|
|
should.equal(err instanceof errors.GhostError, true);
|
|
err.message.should.equal('Cannot add permission(Permission Name) with role(Not there) - role does not exist');
|
|
}
|
|
});
|
|
|
|
it('Does not throw when role cannot be found in down migration', async function () {
|
|
const knex = await setupDb();
|
|
|
|
const migration = utils.addPermissionToRole({
|
|
permission: 'Permission Name',
|
|
role: 'Role Name'
|
|
});
|
|
|
|
const runDownMigration = await runUpMigration(knex, migration);
|
|
await knex('roles')
|
|
.where('name', '=', 'Role Name')
|
|
.del();
|
|
|
|
await runDownMigration(knex, migration);
|
|
});
|
|
});
|
|
});
|
|
|
|
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.');
|
|
});
|
|
});
|
|
});
|