jsharmony-cms
Version:
745 lines (669 loc) • 25.9 kB
JavaScript
/*
Copyright 2020 apHarmony
This file is part of jsHarmony.
jsHarmony is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
jsHarmony is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this package. If not, see <http://www.gnu.org/licenses/>.
*/
// Test files must be run from an application with a configured database
// e.g. "mocha node_modules/jsharmony-cms/test --reporter spec"
var assert = require('assert');
var jsHarmonyCMS = require('../jsHarmonyCMS.js');
var DB = require('jsharmony-db');
var async = require('async');
var _ = require('lodash');
var path = require('path');
var fs = require('fs');
// -------------------- merge result assertions -----------------
// D - values from destination
// S - values from source
// Si - current id of source
// So - orig id of source
// XY - id from X (S/D), orig id from Y
var notExist = function(s, d) {
return null;
};
// all properties same as source
var src = function(s, d) {
if (!s) return s;
return {
id: s.id,
orig_id: s.orig_id,
action: s.action,
};
};
// all properties same as destiation
var dst = function(s, d) {
if (!d) return d;
return {
id: d.id,
orig_id: d.orig_id,
action: d.action,
};
};
// update id with value from source
var srcId = function(s, d) {
return {
id: s.id,
orig_id: d.orig_id,
action: d.action,
};
};
// no action, with source id
var noneS = function(s, d) {
return {
id: s.id,
orig_id: s.id,
action: null,
};
};
var addS = function(s, d) {
return {
id: s.id,
orig_id: null,
action: 'ADD',
};
};
var addD = function(s, d) {
return {
id: d.id,
orig_id: null,
action: 'ADD',
};
};
var updateSD = function(s, d) {
return {
id: s.id,
orig_id: d.orig_id,
action: 'UPDATE',
};
};
var updateSS = function(s, d) {
return {
id: s.id,
orig_id: s.orig_id,
action: 'UPDATE',
};
};
var updateDS = function(s, d) {
return {
id: d.id,
orig_id: s.id,
action: 'UPDATE',
};
};
var deleteD = function(s, d) {
return {
id: null,
orig_id: d.orig_id,
action: 'DELETE',
};
};
var deleteSi = function(s, d) {
return {
id: null,
orig_id: s.id,
action: 'DELETE',
};
};
var deleteSo = function(s, d) {
return {
id: null,
orig_id: s.orig_id,
action: 'DELETE',
};
};
var setupMissing = function(id, orig_id) {
return null;
};
setupMissing.label = 'missing';
var setupUnchanged = function(id, orig_id) {
return {
id: orig_id,
orig_id: orig_id,
action: null,
};
};
setupUnchanged.label = 'unchanged';
var setupAdd = function(id, orig_id) {
return {
id: id,
orig_id: null,
action: 'ADD',
};
};
setupAdd.label = 'ADD';
var setupUpdate = function(id, orig_id) {
return {
id: id,
orig_id: orig_id,
action: 'UPDATE',
};
};
setupUpdate.label = 'UPDATE';
var setupDelete = function(id, orig_id) {
return {
id: null,
orig_id: orig_id,
action: 'DELETE',
};
};
setupDelete.label = 'DELETE';
var mergeCases = [
setupMissing,
setupUnchanged,
setupAdd,
setupUpdate,
setupDelete,
];
var operations = {
overwrite: [
// missing, unchanged, ADD, UPDATE, DELETE (src)
/*missing */ [ notExist, src, src, src, src ],
/*unchanged*/ [ notExist, src, src, src, src ],
/*ADD */ [ notExist, src, src, src, src ],
/*UPDATE */ [ notExist, src, src, src, src ],
/*DELETE */ [ notExist, src, src, src, src ],
],
apply: [
// missing, unchanged, ADD, UPDATE, DELETE (src)
/*missing */ [ dst, dst, noneS, noneS, notExist ],
/*unchanged*/ [ dst, dst, srcId, srcId, notExist ],
/*ADD */ [ dst, dst, srcId, srcId, notExist ],
/*UPDATE */ [ dst, dst, srcId, srcId, notExist ],
/*DELETE */ [ dst, dst, updateSD, updateSD, notExist ],
],
changes: [
// missing, unchanged, ADD, UPDATE, DELETE (src)
/*missing */ [ dst, dst, src, src, src ],
/*unchanged*/ [ dst, dst, updateSD, updateSD, deleteD ],
/*ADD */ [ dst, dst, addS, updateSS, deleteSo ],
/*UPDATE */ [ dst, dst, updateSD, updateSD, deleteD ],
/*DELETE */ [ dst, dst, updateSD, updateSD, deleteD ],
],
rebase: [
// missing, unchanged, ADD, UPDATE, DELETE (src)
/*missing */ [ dst, noneS, noneS, noneS, dst ],
/*unchanged*/ [ notExist, noneS, noneS, noneS, notExist ],
/*ADD */ [ dst, updateDS, updateDS, updateDS, dst ],
/*UPDATE */ [ addD, updateDS, updateDS, updateDS, addD ],
/*DELETE */ [ notExist, deleteSi, deleteSi, deleteSi, notExist ],
],
};
// -------------------- conflict matrix result assertions -----------------
// A, B - arbitrary values, what matters is "same, not same" between the two branches
// XY - id from X (A/B), orig id from Y
var setupNoneA = function(a, b) {
return {
id: a,
orig_id: a,
action: null,
};
};
setupNoneA.label = 'noneA';
var setupNoneB = function(a, b) {
return {
id: b,
orig_id: b,
action: null,
};
};
setupNoneB.label = 'noneB';
var setupAddA = function(a, b) {
return {
id: a,
orig_id: null,
action: 'ADD',
};
};
setupAddA.label = 'addA';
var setupAddB = function(a, b) {
return {
id: b,
orig_id: null,
action: 'ADD',
};
};
setupAddB.label = 'addB';
var setupUpdateAA = function(a, b) {
return {
id: a,
orig_id: a,
action: 'UPDATE',
};
};
setupUpdateAA.label = 'updateAA';
var setupUpdateAB = function(a, b) {
return {
id: a,
orig_id: b,
action: 'UPDATE',
};
};
setupUpdateAB.label = 'updateAB';
var setupUpdateBA = function(a, b) {
return {
id: b,
orig_id: a,
action: 'UPDATE',
};
};
setupUpdateBA.label = 'updateBA';
var setupUpdateBB = function(a, b) {
return {
id: b,
orig_id: b,
action: 'UPDATE',
};
};
setupUpdateBB.label = 'updateBB';
var setupDeleteA = function(a, b) {
return {
id: null,
orig_id: a,
action: 'DELETE',
};
};
setupDeleteA.label = 'deleteA';
var setupDeleteB = function(a, b) {
return {
id: null,
orig_id: b,
action: 'DELETE',
};
};
setupDeleteB.label = 'deleteB';
var conflictCases = [
setupMissing,
setupNoneA,
setupNoneB,
setupAddA,
setupAddB,
setupUpdateAA,
setupUpdateAB,
setupUpdateBA,
setupUpdateBB,
setupDeleteA,
setupDeleteB,
];
var nc = 'no conflict';
var CONFLICT = 'CONFLICT';
// Two things are slightly unusual because this only uses two values to control combinatorical explosion. (sort of three when null)
// updateAA/BB are atypical, but can exist under special circumstances; e.g. merging changes into branch that already has those changes.
// updateAB vs updateBA is surprising. I don't see how this would actually occur in editing, unless there were some sort of "revert" and that got merged with the original edit.
// similarly deleteA vs addA. though the sequence add, then delete may occur normally.
var conflicts = [
// missing , noneA , noneB , addA , addB , updateAA , updateAB , updateBA , updateBB , deleteA , deleteB (src)
/*missing*/ [ nc , nc , nc , nc , nc , nc , nc , nc , nc , nc , nc ],
/*noneA*/ [ nc , nc , nc , nc , CONFLICT , nc , nc , nc , CONFLICT , nc , CONFLICT ],
/*noneB*/ [ nc , nc , nc , CONFLICT , nc , CONFLICT , nc , nc , nc , CONFLICT , nc ],
/*addA*/ [ nc , nc , CONFLICT , nc , CONFLICT , nc , nc , CONFLICT , CONFLICT , nc , CONFLICT ],
/*addB*/ [ nc , CONFLICT , nc , CONFLICT , nc , CONFLICT , CONFLICT , nc , nc , CONFLICT , nc ],
/*updateAA*/ [ nc , nc , CONFLICT , nc , CONFLICT , nc , nc , CONFLICT , CONFLICT , CONFLICT , CONFLICT ],
/*updateAB*/ [ nc , nc , nc , nc , CONFLICT , nc , nc , nc , CONFLICT , CONFLICT , CONFLICT ],
/*updateBA*/ [ nc , nc , nc , CONFLICT , nc , CONFLICT , nc , nc , nc , CONFLICT , CONFLICT ],
/*updateBB*/ [ nc , CONFLICT , nc , CONFLICT , nc , CONFLICT , CONFLICT , nc , nc , CONFLICT , CONFLICT ],
/*deleteA*/ [ nc , nc , CONFLICT , nc , CONFLICT , CONFLICT , CONFLICT , CONFLICT , CONFLICT , nc , nc ],
/*deleteB*/ [ nc , CONFLICT , nc , CONFLICT , nc , CONFLICT , CONFLICT , CONFLICT , CONFLICT , nc , nc ],
];
// -------------------- conflict manual result assertions (three-four pages) -----------------
// provided premade updateAB and updateCD
// cases with more then two "sames" are often degenerate (may not really be an update) and should be covered above
var sameNothing = function(srcPage, dstPage) {};
sameNothing.label = 'same nothing';
var sameId = function(srcPage, dstPage) {
srcPage.id = dstPage.id;
};
sameId.label = 'same id';
var sameOrig = function(srcPage, dstPage) {
srcPage.orig_id = dstPage.orig_id;
};
sameOrig.label = 'same orig_id';
var srcEditsDst = function(srcPage, dstPage) {
srcPage.orig_id = dstPage.id;
};
srcEditsDst.label = 'src edits dst';
var dstEditsSrc = function(srcPage, dstPage) {
dstPage.orig_id = srcPage.id;
};
dstEditsSrc.label = 'dst edits src';
var manualConflicts = [
// four pages
[CONFLICT, sameNothing],
// three pages
[nc , sameId],
[CONFLICT, sameOrig],
[CONFLICT, srcEditsDst],
[CONFLICT, dstEditsSrc],
// <= two pages: see matrix
];
/*
Update A-B on no-change C will happen given any two concurrent edits.
So, lead Developer David recently got a departure notice from Developer Danny, and makes a branch to add a job description to the careers page.
Meanwhile, HR Henry makes a branch to update all the job descriptions with new work-from-home policies.
David merges the change, taking the careers page from 1->2. After merge to master and clearing orig, all we see is 2.
Henry submits his changes, with update 1->3. During the conflict check, we see 1->3 : 2. No matching ids.
If there is no conflict notice, the job posting gets lost.
*/
var cmsPath = '';
var dataPath = '';
describe('Merges - Matrix', function() {
this.timeout(10000);
var jsh = new jsHarmonyCMS.Application();
jsh.Config.appbasepath = process.cwd();
jsh.Config.silentStart = true;
jsh.Config.interactive = true;
jsh.Config.onConfigLoaded.push(function(cb){
jsh.Config.system_settings.automatic_schema = false;
cmsPath = jsh.Config.modules['jsHarmonyCMS'].moduledir;
dataPath = jsh.Config.datadir;
return cb();
});
var dbconfig;
var db;
var dbtypes = DB.types;
var testDstBranchId = -1;
var testSrcBranchId = -1;
var testPages = [];
var testSrcPageId = -1;
var testSrcOrigPageId = -1;
var testDstPageId = -1;
var testDstOrigPageId = -1;
var testPageIdA = -1;
var testPageIdB = -1;
var testPageIdGrandparent = -1;
var testPageIdParent = -1;
var testPageIdChild = -1;
var testPageKey = -1;
var setupBranchPage = function(branchId, key, page){
return function(cb) {
var sql, sql_params, sql_ptypes;
if (page) {
// if the page should exist, ensure it exists, and update with the correct values.
sql = 'update cms.branch_page set page_id = @page_id, page_orig_id = @page_orig_id, branch_page_action = @branch_page_action where page_key = @page_key and branch_id = @branch_id;\
insert into cms.branch_page(branch_id, page_key, page_id, page_orig_id, branch_page_action) select @branch_id, @page_key, @page_id, @page_orig_id, @branch_page_action from cms.branch where branch_id = @branch_id and @page_key not in (select page_key from cms.branch_page where branch_id = @branch_id);';
sql_params = {
'branch_id': branchId,
'page_key': key,
'page_id': page.id,
'page_orig_id': page.orig_id,
'branch_page_action': page.action,
};
sql_ptypes = [
dbtypes.BigInt,
dbtypes.BigInt,
dbtypes.BigInt,
dbtypes.BigInt,
dbtypes.VarChar(8),
];
} else {
// page shouldn't exist for this test, get rid if it present.
sql = 'delete from cms.branch_page where page_key = @page_key and branch_id = @branch_id;';
sql_params = {
'branch_id': branchId,
'page_key': key,
};
sql_ptypes = [
dbtypes.BigInt,
dbtypes.BigInt,
];
}
db.Command('S1', sql, sql_ptypes, sql_params, function(err, dbrslt, stats) {
assert.ifError(err);
cb(err);
});
};
};
// clear flags from last test to ensure branch is updatable
var resetBranch = function(branchId) {
return function(cb) {
var sql = 'update cms.branch set branch_merge_id=null, branch_merge_type=null where branch_id=@dst_branch_id;';
var sql_params = {
'dst_branch_id': branchId,
};
var sql_ptypes = [
dbtypes.BigInt,
];
db.Command('S1', sql, sql_ptypes, sql_params, function(err, dbrslt, stats) {
assert.ifError(err);
cb(err);
});
};
};
// pull page state as JS object and compare values
// can compare multiple entries, but we only use one here
function assertBranchState(branch_id, state, label, done) {
var sql = 'select branch_page_action as action, page_id id, page_orig_id orig_id from cms.branch_page where branch_id='+branch_id+'';
db.Recordset('', sql, [], {}, function (err, dbrslt, stats) {
// log: async test exceptions don't appear under the right test heading
// providing a message to deepStrictEqual doesn't print the diff.
console.log(label, 'results');
assert.ifError(err);
try{
assert.deepStrictEqual(dbrslt, state);
}
catch(ex){
console.log('Found');
console.log('-----');
console.log(JSON.stringify(dbrslt,null,4));
console.log('Expected');
console.log('--------');
console.log(JSON.stringify(state,null,4));
throw(ex);
}
done(err);
});
}
function clearTestData(cb){
/* Clear previous branchs */
var sql = "\
delete from cms.branch_page where branch_id in (select branch_id from cms.branch where branch_name like 'Merge Test Data:%');\
delete from cms.page where page_path like 'Merge Test Data:%';\
delete from cms.branch_media where branch_id in (select branch_id from cms.branch where branch_name like 'Merge Test Data:%');\
delete from cms.branch_menu where branch_id in (select branch_id from cms.branch where branch_name like 'Merge Test Data:%');\
delete from cms.branch_redirect where branch_id in (select branch_id from cms.branch where branch_name like 'Merge Test Data:%');\
delete from cms.branch where branch_name like 'Merge Test Data:%';";
db.Command('S1', sql, [], {}, function(err, dbrslt, stats) {
assert.ifError(err);
cb();
});
}
before(function(done) {
this.timeout(0);
jsh.Init(function(){
var dbid = 'default';
db = jsh.DB[dbid];
dbconfig = jsh.DBConfig[dbid];
if(dbconfig.admin_user){
dbconfig = _.extend({}, dbconfig);
dbconfig.user = dbconfig.admin_user;
dbconfig.password = dbconfig.admin_password;
}
async.waterfall([
clearTestData,
function(cb){
// create test site
var sql = "insert into cms.site(site_name) select 'Merge Test Data' where ('Merge Test Data' not in (select site_name from cms.site));";
db.Command('S1', sql, [], {}, function(err, dbrslt, stats) {
assert.ifError(err);
cb();
});
},
function(cb){
// create source branch
var sql = "insert into cms.v_my_current_branch(branch_type, branch_name, site_id) values('USER', 'Merge Test Data: Source Branch', (select site_id from cms.site where site_name='Merge Test Data'));\
select branch_id from cms.v_my_current_branch;";
db.Scalar('S1', sql, [], {}, function(err, dbrslt, stats) {
assert.ifError(err);
testSrcBranchId = dbrslt;
cb();
});
},
function(cb){
// create destination branch
var sql = "insert into cms.v_my_current_branch(branch_type, branch_name, site_id) values('USER', 'Merge Test Data: Destination Branch', (select site_id from cms.site where site_name='Merge Test Data'));\
select branch_id from cms.v_my_current_branch;";
db.Scalar('S1', sql, [], {}, function(err, dbrslt, stats) {
assert.ifError(err);
testDstBranchId = dbrslt;
cb();
});
},
function(cb){
// create test pages
var sql =
"insert into cms.page(site_id,page_path) values((select site_id from cms.site where site_name='Merge Test Data'), 'Merge Test Data: src orig');\
insert into cms.page(site_id,page_key, page_path) select (select site_id from cms.site where site_name='Merge Test Data'), page_key, 'Merge Test Data: dst orig' from cms.page where page_path = 'Merge Test Data: src orig';\
insert into cms.page(site_id,page_key, page_path, page_orig_id) select (select site_id from cms.site where site_name='Merge Test Data'), page_key, 'Merge Test Data: src current', page_id from cms.page where page_path = 'Merge Test Data: src orig';\
insert into cms.page(site_id,page_key, page_path, page_orig_id) select (select site_id from cms.site where site_name='Merge Test Data'), page_key, 'Merge Test Data: dst current', page_id from cms.page where page_path = 'Merge Test Data: dst orig';\
insert into cms.page(site_id,page_key, page_path, page_orig_id) select (select site_id from cms.site where site_name='Merge Test Data'), page_key, 'Merge Test Data: child', page_id from cms.page where page_path = 'Merge Test Data: src current';\
select page_key, page_id, page_orig_id from cms.page where page_path like 'Merge Test Data:%' order by page_id;";
db.Recordset('S1', sql, [], {}, function(err, dbrslt, stats) {
assert.ifError(err);
testPageKey = dbrslt[0].page_key;
testPages = dbrslt.map(function(rec) {return rec.page_id;});
testSrcOrigPageId = testPages[0];
testDstOrigPageId = testPages[1];
testSrcPageId = testPages[2];
testDstPageId = testPages[3];
testPageIdA = testPages[0];
testPageIdB = testPages[1];
testPageIdGrandparent = testPages[0];
testPageIdParent = testPages[2];
testPageIdChild = testPages[4];
async.eachSeries(testPages, function(page_id, file_cb){
fs.copyFile(
path.join(cmsPath, 'models/sql/objects/data_files/page_sample.json'),
path.join(dataPath, 'page/'+page_id+'.json'),
file_cb);
}, cb);
});
},
], function(err){
if(err) console.log(err);
done();
});
});
});
after(function(done) {
clearTestData(function(err){
if(err) console.log(err);
db.Close();
done();
});
});
Object.keys(operations).forEach(function(operation){
describe(operation, function() {
operations[operation].forEach(function(row, dstIndex) {
row.forEach(function(test, srcIndex) {
var label = 'dst ' + mergeCases[dstIndex].label + ' + src ' + mergeCases[srcIndex].label;
it(label, function(done) {
var srcPage = mergeCases[srcIndex](testSrcPageId, testSrcOrigPageId);
var dstPage = mergeCases[dstIndex](testDstPageId, testDstOrigPageId);
var resultPage = test(srcPage, dstPage);
async.waterfall([
resetBranch(testDstBranchId),
setupBranchPage(testSrcBranchId, testPageKey, srcPage),
setupBranchPage(testDstBranchId, testPageKey, dstPage),
function(cb) {
jsh.Modules.jsHarmonyCMS.funcs.merge_begin_merge('S1', {dst_branch_id: testDstBranchId, src_branch_id: testSrcBranchId, merge_type: operation.toUpperCase()}, cb);
},
function(cb) {
//console.log('merge for ', label);
jsh.Modules.jsHarmonyCMS.funcs['merge_'+operation]('S1', {dst_branch_id: testDstBranchId, src_branch_id: testSrcBranchId}, cb);
},
function(cb) {
//console.log('assert for ', label);
assertBranchState(testDstBranchId, resultPage ? [resultPage] : [], label, cb);
},
], function(err){
if(err) console.log(err);
done(err);
});
});
});
});
});
});
var conflictTest = function(label, srcPage, dstPage, status, done) {
async.waterfall([
setupBranchPage(testSrcBranchId, testPageKey, srcPage),
setupBranchPage(testDstBranchId, testPageKey, dstPage),
function(cb) {
console.log('conflicts for ', label);
jsh.Modules.jsHarmonyCMS.funcs.conflicts('S1', '', testSrcBranchId, testDstBranchId, function(err, results) {
if(err) return cb(err);
assert(!_.isEmpty(results.branch_conflicts));
var resultStatus = results.branch_conflicts.page.length > 0 ? CONFLICT : nc;
if(resultStatus != status){
console.log(testSrcBranchId);
console.log(srcPage);
console.log(testDstBranchId);
console.log(dstPage);
console.log('Actual Result: ' + JSON.stringify(results.branch_conflicts.page));
}
assert.strictEqual(resultStatus, status);
cb();
});
},
], function(err){
if(err) console.log(err);
done(err);
});
};
describe('Conflicts', function() {
before(function(done) {
resetBranch(testDstBranchId)(done);
});
describe('Matrix', function() {
conflicts.forEach(function(row, dstIndex) {
row.forEach(function(status, srcIndex) {
var label = 'dst ' + conflictCases[dstIndex].label + ' + src ' + conflictCases[srcIndex].label;
// can't include the "it" in the function because testPageId isn't defined until `before` runs.
it(label, function(done) {
var srcPage = conflictCases[srcIndex](testPageIdA, testPageIdB);
var dstPage = conflictCases[dstIndex](testPageIdA, testPageIdB);
conflictTest(label, srcPage, dstPage, status, done);
});
});
});
});
describe('Manual', function() {
manualConflicts.forEach(function(row) {
var status = row.shift();
var label = row.map(function(f) {return f.label;}).join(' ');
// can't include the "it" in the function because testPageId isn't defined until `before` runs.
it(label, function(done) {
var srcPage = setupUpdate(testSrcPageId, testSrcOrigPageId);
var dstPage = setupUpdate(testDstPageId, testDstOrigPageId);
row.forEach(function(f) {f(srcPage, dstPage);});
conflictTest(label, srcPage, dstPage, status, done);
});
});
});
describe('Direct Descedant', function() {
it('Unrelated pages', function(done) {
var srcPage = setupUpdate(testSrcPageId, testSrcOrigPageId);
var dstPage = setupUnchanged(testDstPageId, testDstPageId);
conflictTest('Unrelated pages', srcPage, dstPage, CONFLICT, done);
});
it('Source Descendant of Destination', function(done) {
var srcPage = setupUpdate(testPageIdChild, testPageIdParent);
var dstPage = setupUnchanged(testPageIdGrandparent, testPageIdGrandparent);
conflictTest('Source Descendant of Destination', srcPage, dstPage, nc, done);
});
it('Destination Descendant of Source', function(done) {
var srcPage = setupUpdate(testPageIdParent, testPageIdGrandparent);
var dstPage = setupUpdate(testPageIdChild, testPageIdParent);
conflictTest('Destination Descendant of Source', srcPage, dstPage, CONFLICT, done);
});
});
});
});