523 lines
17 KiB
JavaScript
523 lines
17 KiB
JavaScript
// Copyright IBM Corp. 2013,2016. All Rights Reserved.
|
|
// Node module: loopback-datasource-juggler
|
|
// This file is licensed under the MIT License.
|
|
// License text available at https://opensource.org/licenses/MIT
|
|
|
|
var should = require('./init.js');
|
|
var utils = require('../lib/utils');
|
|
var fieldsToArray = utils.fieldsToArray;
|
|
var removeUndefined = utils.removeUndefined;
|
|
var mergeSettings = utils.mergeSettings;
|
|
var mergeIncludes = utils.mergeIncludes;
|
|
var sortObjectsByIds = utils.sortObjectsByIds;
|
|
var uniq = utils.uniq;
|
|
|
|
describe('util.fieldsToArray', function() {
|
|
function sample(fields, excludeUnknown) {
|
|
var properties = ['foo', 'bar', 'bat', 'baz'];
|
|
return {
|
|
expect: function(arr) {
|
|
should.deepEqual(fieldsToArray(fields, properties, excludeUnknown), arr);
|
|
},
|
|
};
|
|
}
|
|
|
|
it('Turn objects and strings into an array of fields' +
|
|
' to include when finding models', function() {
|
|
|
|
sample(false).expect(undefined);
|
|
sample(null).expect(undefined);
|
|
sample({}).expect(undefined);
|
|
sample('foo').expect(['foo']);
|
|
sample(['foo']).expect(['foo']);
|
|
sample({ 'foo': 1 }).expect(['foo']);
|
|
sample({ 'bat': true }).expect(['bat']);
|
|
sample({ 'bat': 0 }).expect(['foo', 'bar', 'baz']);
|
|
sample({ 'bat': false }).expect(['foo', 'bar', 'baz']);
|
|
});
|
|
|
|
it('should exclude unknown properties', function() {
|
|
|
|
sample(false, true).expect(undefined);
|
|
sample(null, true).expect(undefined);
|
|
sample({}, true).expect(undefined);
|
|
sample('foo', true).expect(['foo']);
|
|
sample(['foo', 'unknown'], true).expect(['foo']);
|
|
sample({ 'foo': 1, unknown: 1 }, true).expect(['foo']);
|
|
sample({ 'bat': true, unknown: true }, true).expect(['bat']);
|
|
sample({ 'bat': 0 }, true).expect(['foo', 'bar', 'baz']);
|
|
sample({ 'bat': false }, true).expect(['foo', 'bar', 'baz']);
|
|
});
|
|
});
|
|
|
|
describe('util.removeUndefined', function() {
|
|
it('Remove undefined values from the query object', function() {
|
|
var q1 = { where: { x: 1, y: undefined }};
|
|
should.deepEqual(removeUndefined(q1), { where: { x: 1 }});
|
|
|
|
var q2 = { where: { x: 1, y: 2 }};
|
|
should.deepEqual(removeUndefined(q2), { where: { x: 1, y: 2 }});
|
|
|
|
var q3 = { where: { x: 1, y: { in: [2, undefined] }}};
|
|
should.deepEqual(removeUndefined(q3), { where: { x: 1, y: { in: [2] }}});
|
|
|
|
should.equal(removeUndefined(null), null);
|
|
|
|
should.equal(removeUndefined(undefined), undefined);
|
|
|
|
should.equal(removeUndefined('x'), 'x');
|
|
|
|
var date = new Date();
|
|
var q4 = { where: { x: 1, y: date }};
|
|
should.deepEqual(removeUndefined(q4), { where: { x: 1, y: date }});
|
|
|
|
// test handling of undefined
|
|
var q5 = { where: { x: 1, y: undefined }};
|
|
should.deepEqual(removeUndefined(q5, 'nullify'), { where: { x: 1, y: null }});
|
|
|
|
var q6 = { where: { x: 1, y: undefined }};
|
|
(function() { removeUndefined(q6, 'throw'); }).should.throw(/`undefined` in query/);
|
|
|
|
});
|
|
});
|
|
|
|
describe('util.parseSettings', function() {
|
|
it('Parse a full url into a settings object', function() {
|
|
var url = 'mongodb://x:y@localhost:27017/mydb?w=2';
|
|
var settings = utils.parseSettings(url);
|
|
should.equal(settings.hostname, 'localhost');
|
|
should.equal(settings.port, 27017);
|
|
should.equal(settings.host, 'localhost');
|
|
should.equal(settings.user, 'x');
|
|
should.equal(settings.password, 'y');
|
|
should.equal(settings.database, 'mydb');
|
|
should.equal(settings.connector, 'mongodb');
|
|
should.equal(settings.w, '2');
|
|
should.equal(settings.url, 'mongodb://x:y@localhost:27017/mydb?w=2');
|
|
|
|
});
|
|
|
|
it('Parse a url without auth into a settings object', function() {
|
|
var url = 'mongodb://localhost:27017/mydb/abc?w=2';
|
|
var settings = utils.parseSettings(url);
|
|
should.equal(settings.hostname, 'localhost');
|
|
should.equal(settings.port, 27017);
|
|
should.equal(settings.host, 'localhost');
|
|
should.equal(settings.user, undefined);
|
|
should.equal(settings.password, undefined);
|
|
should.equal(settings.database, 'mydb');
|
|
should.equal(settings.connector, 'mongodb');
|
|
should.equal(settings.w, '2');
|
|
should.equal(settings.url, 'mongodb://localhost:27017/mydb/abc?w=2');
|
|
|
|
});
|
|
|
|
it('Parse a url with complex query into a settings object', function() {
|
|
var url = 'mysql://127.0.0.1:3306/mydb?x[a]=1&x[b]=2&engine=InnoDB';
|
|
var settings = utils.parseSettings(url);
|
|
should.equal(settings.hostname, '127.0.0.1');
|
|
should.equal(settings.port, 3306);
|
|
should.equal(settings.host, '127.0.0.1');
|
|
should.equal(settings.user, undefined);
|
|
should.equal(settings.password, undefined);
|
|
should.equal(settings.database, 'mydb');
|
|
should.equal(settings.connector, 'mysql');
|
|
should.equal(settings.x.a, '1');
|
|
should.equal(settings.x.b, '2');
|
|
should.equal(settings.engine, 'InnoDB');
|
|
should.equal(settings.url, 'mysql://127.0.0.1:3306/mydb?x[a]=1&x[b]=2&engine=InnoDB');
|
|
|
|
});
|
|
|
|
it('Parse a url without auth into a settings object', function() {
|
|
var url = 'memory://?x=1';
|
|
var settings = utils.parseSettings(url);
|
|
should.equal(settings.hostname, '');
|
|
should.equal(settings.user, undefined);
|
|
should.equal(settings.password, undefined);
|
|
should.equal(settings.database, undefined);
|
|
should.equal(settings.connector, 'memory');
|
|
should.equal(settings.x, '1');
|
|
should.equal(settings.url, 'memory://?x=1');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
describe('mergeSettings', function() {
|
|
it('should merge settings correctly', function() {
|
|
var src = { base: 'User',
|
|
relations: { accessTokens: { model: 'accessToken', type: 'hasMany',
|
|
foreignKey: 'userId' },
|
|
account: { model: 'account', type: 'belongsTo' }},
|
|
acls: [
|
|
{ accessType: '*',
|
|
permission: 'DENY',
|
|
principalType: 'ROLE',
|
|
principalId: '$everyone' },
|
|
{ accessType: '*',
|
|
permission: 'ALLOW',
|
|
principalType: 'ROLE',
|
|
property: 'login',
|
|
principalId: '$everyone' },
|
|
{ permission: 'ALLOW',
|
|
property: 'findById',
|
|
principalType: 'ROLE',
|
|
principalId: '$owner' },
|
|
] };
|
|
var tgt = { strict: false,
|
|
acls: [
|
|
{ principalType: 'ROLE',
|
|
principalId: '$everyone',
|
|
permission: 'ALLOW',
|
|
property: 'create' },
|
|
{ principalType: 'ROLE',
|
|
principalId: '$owner',
|
|
permission: 'ALLOW',
|
|
property: 'removeById' },
|
|
],
|
|
maxTTL: 31556926,
|
|
ttl: 1209600 };
|
|
|
|
var dst = mergeSettings(tgt, src);
|
|
|
|
var expected = { strict: false,
|
|
acls: [
|
|
{ principalType: 'ROLE',
|
|
principalId: '$everyone',
|
|
permission: 'ALLOW',
|
|
property: 'create' },
|
|
{ principalType: 'ROLE',
|
|
principalId: '$owner',
|
|
permission: 'ALLOW',
|
|
property: 'removeById' },
|
|
{ accessType: '*',
|
|
permission: 'DENY',
|
|
principalType: 'ROLE',
|
|
principalId: '$everyone' },
|
|
{ accessType: '*',
|
|
permission: 'ALLOW',
|
|
principalType: 'ROLE',
|
|
property: 'login',
|
|
principalId: '$everyone' },
|
|
{ permission: 'ALLOW',
|
|
property: 'findById',
|
|
principalType: 'ROLE',
|
|
principalId: '$owner' },
|
|
],
|
|
maxTTL: 31556926,
|
|
ttl: 1209600,
|
|
base: 'User',
|
|
relations: { accessTokens: { model: 'accessToken', type: 'hasMany',
|
|
foreignKey: 'userId' },
|
|
account: { model: 'account', type: 'belongsTo' }}};
|
|
|
|
should.deepEqual(dst.acls, expected.acls, 'Merged settings should match the expectation');
|
|
});
|
|
});
|
|
|
|
describe('sortObjectsByIds', function() {
|
|
|
|
var items = [
|
|
{ id: 1, name: 'a' },
|
|
{ id: 2, name: 'b' },
|
|
{ id: 3, name: 'c' },
|
|
{ id: 4, name: 'd' },
|
|
{ id: 5, name: 'e' },
|
|
{ id: 6, name: 'f' },
|
|
];
|
|
|
|
it('should sort', function() {
|
|
var sorted = sortObjectsByIds('id', [6, 5, 4, 3, 2, 1], items);
|
|
var names = sorted.map(function(u) { return u.name; });
|
|
should.deepEqual(names, ['f', 'e', 'd', 'c', 'b', 'a']);
|
|
});
|
|
|
|
it('should sort - partial ids', function() {
|
|
var sorted = sortObjectsByIds('id', [5, 3, 2], items);
|
|
var names = sorted.map(function(u) { return u.name; });
|
|
should.deepEqual(names, ['e', 'c', 'b', 'a', 'd', 'f']);
|
|
});
|
|
|
|
it('should sort - strict', function() {
|
|
var sorted = sortObjectsByIds('id', [5, 3, 2], items, true);
|
|
var names = sorted.map(function(u) { return u.name; });
|
|
should.deepEqual(names, ['e', 'c', 'b']);
|
|
});
|
|
|
|
});
|
|
|
|
describe('util.mergeIncludes', function() {
|
|
|
|
function checkInputOutput(baseInclude, updateInclude, expectedInclude) {
|
|
var mergedInclude = mergeIncludes(baseInclude, updateInclude);
|
|
should.deepEqual(mergedInclude, expectedInclude,
|
|
'Merged include should match the expectation');
|
|
}
|
|
|
|
it('Merge string values to object', function() {
|
|
var baseInclude = 'relation1';
|
|
var updateInclude = 'relation2';
|
|
var expectedInclude = [
|
|
{ relation2: true },
|
|
{ relation1: true },
|
|
];
|
|
checkInputOutput(baseInclude, updateInclude, expectedInclude);
|
|
});
|
|
|
|
it('Merge string & array values to object', function() {
|
|
var baseInclude = 'relation1';
|
|
var updateInclude = ['relation2'];
|
|
var expectedInclude = [
|
|
{ relation2: true },
|
|
{ relation1: true },
|
|
];
|
|
checkInputOutput(baseInclude, updateInclude, expectedInclude);
|
|
});
|
|
|
|
it('Merge string & object values to object', function() {
|
|
var baseInclude = ['relation1'];
|
|
var updateInclude = { relation2: 'relation2Include' };
|
|
var expectedInclude = [
|
|
{ relation2: 'relation2Include' },
|
|
{ relation1: true },
|
|
];
|
|
checkInputOutput(baseInclude, updateInclude, expectedInclude);
|
|
});
|
|
|
|
it('Merge array & array values to object', function() {
|
|
var baseInclude = ['relation1'];
|
|
var updateInclude = ['relation2'];
|
|
var expectedInclude = [
|
|
{ relation2: true },
|
|
{ relation1: true },
|
|
];
|
|
checkInputOutput(baseInclude, updateInclude, expectedInclude);
|
|
});
|
|
|
|
it('Merge array & object values to object', function() {
|
|
var baseInclude = ['relation1'];
|
|
var updateInclude = { relation2: 'relation2Include' };
|
|
var expectedInclude = [
|
|
{ relation2: 'relation2Include' },
|
|
{ relation1: true },
|
|
];
|
|
checkInputOutput(baseInclude, updateInclude, expectedInclude);
|
|
});
|
|
|
|
it('Merge object & object values to object', function() {
|
|
var baseInclude = { relation1: 'relation1Include' };
|
|
var updateInclude = { relation2: 'relation2Include' };
|
|
var expectedInclude = [
|
|
{ relation2: 'relation2Include' },
|
|
{ relation1: 'relation1Include' },
|
|
];
|
|
checkInputOutput(baseInclude, updateInclude, expectedInclude);
|
|
});
|
|
|
|
it('Override property collision with update value', function() {
|
|
var baseInclude = { relation1: 'baseValue' };
|
|
var updateInclude = { relation1: 'updateValue' };
|
|
var expectedInclude = [
|
|
{ relation1: 'updateValue' },
|
|
];
|
|
checkInputOutput(baseInclude, updateInclude, expectedInclude);
|
|
});
|
|
|
|
it('Merge string includes & include with relation syntax properly',
|
|
function() {
|
|
var baseInclude = 'relation1';
|
|
var updateInclude = { relation: 'relation1' };
|
|
var expectedInclude = [
|
|
{ relation: 'relation1' },
|
|
];
|
|
checkInputOutput(baseInclude, updateInclude, expectedInclude);
|
|
});
|
|
|
|
it('Merge string includes & include with scope properly', function() {
|
|
var baseInclude = 'relation1';
|
|
var updateInclude = {
|
|
relation: 'relation1',
|
|
scope: { include: 'relation2' },
|
|
};
|
|
var expectedInclude = [
|
|
{ relation: 'relation1', scope: { include: 'relation2' }},
|
|
];
|
|
checkInputOutput(baseInclude, updateInclude, expectedInclude);
|
|
});
|
|
|
|
it('Merge includes with and without relation syntax properly',
|
|
function() {
|
|
//w & w/o relation syntax - no collision
|
|
var baseInclude = ['relation2'];
|
|
var updateInclude = {
|
|
relation: 'relation1',
|
|
scope: { include: 'relation2' },
|
|
};
|
|
var expectedInclude = [{
|
|
relation: 'relation1',
|
|
scope: { include: 'relation2' },
|
|
}, { relation2: true }];
|
|
checkInputOutput(baseInclude, updateInclude, expectedInclude);
|
|
|
|
//w & w/o relation syntax - collision
|
|
baseInclude = ['relation1'];
|
|
updateInclude = { relation: 'relation1', scope: { include: 'relation2' }};
|
|
expectedInclude =
|
|
[{ relation: 'relation1', scope: { include: 'relation2' }}];
|
|
checkInputOutput(baseInclude, updateInclude, expectedInclude);
|
|
|
|
//w & w/o relation syntax - collision
|
|
baseInclude = { relation: 'relation1', scope: { include: 'relation2' }};
|
|
updateInclude = ['relation1'];
|
|
expectedInclude = [{ relation1: true }];
|
|
checkInputOutput(baseInclude, updateInclude, expectedInclude);
|
|
});
|
|
|
|
it('Merge includes with mixture of strings, arrays & objects properly', function() {
|
|
var baseInclude = ['relation1', { relation2: true },
|
|
{ relation: 'relation3', scope: { where: { id: 'some id' }}},
|
|
{ relation: 'relation5', scope: { where: { id: 'some id' }}},
|
|
];
|
|
var updateInclude = ['relation4', { relation3: true },
|
|
{ relation: 'relation2', scope: { where: { id: 'some id' }}}];
|
|
var expectedInclude = [{ relation4: true }, { relation3: true },
|
|
{ relation: 'relation2', scope: { where: { id: 'some id' }}},
|
|
{ relation1: true },
|
|
{ relation: 'relation5', scope: { where: { id: 'some id' }}}];
|
|
checkInputOutput(baseInclude, updateInclude, expectedInclude);
|
|
});
|
|
});
|
|
|
|
describe('util.uniq', function() {
|
|
|
|
it('should dedupe an array with duplicate number entries', function() {
|
|
var a = [1, 2, 1, 3];
|
|
var b = uniq(a);
|
|
b.should.eql([1, 2, 3]);
|
|
});
|
|
|
|
it('should dedupe an array with duplicate string entries', function() {
|
|
var a = ['a', 'a', 'b', 'a'];
|
|
var b = uniq(a);
|
|
b.should.eql(['a', 'b']);
|
|
});
|
|
|
|
it('should dedupe an array without duplicate number entries', function() {
|
|
var a = [1, 3, 2];
|
|
var b = uniq(a);
|
|
b.should.eql([1, 3, 2]);
|
|
});
|
|
|
|
it('should dedupe an array without duplicate string entries', function() {
|
|
var a = ['a', 'c', 'b'];
|
|
var b = uniq(a);
|
|
b.should.eql(['a', 'c', 'b']);
|
|
});
|
|
|
|
it('should allow null/undefined array', function() {
|
|
var a = null;
|
|
var b = uniq(a);
|
|
b.should.eql([]);
|
|
});
|
|
|
|
it('should report error for non-array arg', function() {
|
|
var a = '1';
|
|
try {
|
|
var b = uniq(a);
|
|
throw new Error('The test should have thrown an error');
|
|
} catch (err) {
|
|
err.should.be.instanceof(Error);
|
|
}
|
|
});
|
|
|
|
});
|
|
|
|
describe('util.toRegExp', function() {
|
|
var invalidDataTypes;
|
|
var validDataTypes;
|
|
|
|
before(function() {
|
|
invalidDataTypes = [0, true, {}, [], Function, null];
|
|
validDataTypes = ['string', /^regex/, new RegExp(/^regex/)];
|
|
});
|
|
|
|
it('should not accept invalid data types', function() {
|
|
invalidDataTypes.forEach(function(invalid) {
|
|
utils.toRegExp(invalid).should.be.an.Error;
|
|
});
|
|
});
|
|
|
|
it('should accept valid data types', function() {
|
|
validDataTypes.forEach(function(valid) {
|
|
utils.toRegExp(valid).should.not.be.an.Error;
|
|
});
|
|
});
|
|
|
|
context('with a regex string', function() {
|
|
it('should return a RegExp object when no regex flags are provided',
|
|
function() {
|
|
utils.toRegExp('^regex$').should.be.an.instanceOf(RegExp);
|
|
});
|
|
|
|
it('should throw an error when invalid regex flags are provided',
|
|
function() {
|
|
utils.toRegExp('^regex$/abc').should.be.an.Error;
|
|
});
|
|
|
|
it('should return a RegExp object when valid flags are provided',
|
|
function() {
|
|
utils.toRegExp('regex/igm').should.be.an.instanceOf(RegExp);
|
|
});
|
|
});
|
|
|
|
context('with a regex literal', function() {
|
|
it('should return a RegExp object', function() {
|
|
utils.toRegExp(/^regex$/igm).should.be.an.instanceOf(RegExp);
|
|
});
|
|
});
|
|
|
|
context('with a regex object', function() {
|
|
it('should return a RegExp object', function() {
|
|
utils.toRegExp(new RegExp('^regex$', 'igm')).should.be.an.instanceOf(RegExp);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('util.hasRegExpFlags', function() {
|
|
context('with a regex string', function() {
|
|
it('should be true when the regex has invalid flags', function() {
|
|
utils.hasRegExpFlags('^regex$/abc').should.be.ok;
|
|
});
|
|
|
|
it('should be true when the regex has valid flags', function() {
|
|
utils.hasRegExpFlags('^regex$/igm').should.be.ok;
|
|
});
|
|
|
|
it('should be false when the regex has no flags', function() {
|
|
utils.hasRegExpFlags('^regex$').should.not.be.ok;
|
|
utils.hasRegExpFlags('^regex$/').should.not.be.ok;
|
|
});
|
|
});
|
|
|
|
context('with a regex literal', function() {
|
|
it('should be true when the regex has valid flags', function() {
|
|
utils.hasRegExpFlags(/^regex$/igm).should.be.ok;
|
|
});
|
|
|
|
it('should be false when the regex has no flags', function() {
|
|
utils.hasRegExpFlags(/^regex$/).should.not.be.ok;
|
|
});
|
|
});
|
|
|
|
context('with a regex object', function() {
|
|
it('should be true when the regex has valid flags', function() {
|
|
utils.hasRegExpFlags(new RegExp(/^regex$/igm)).should.be.ok;
|
|
});
|
|
|
|
it('should be false when the regex has no flags', function() {
|
|
utils.hasRegExpFlags(new RegExp(/^regex$/)).should.not.be.ok;
|
|
});
|
|
});
|
|
});
|