@studiolabs/strong-remoting
Version:
StrongLoop Remoting Module
433 lines (382 loc) • 14.5 kB
JavaScript
// Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: strong-remoting
// This file is licensed under the Artistic License 2.0.
// License text available at https://opensource.org/licenses/Artistic-2.0
;
var assert = require('assert');
var extend = require('util')._extend;
var expect = require('./helpers/expect');
var Context = require('../lib/context-base');
var SharedMethod = require('../lib/shared-method');
var TypeRegistry = require('../lib/type-registry');
var factory = require('./helpers/shared-objects-factory.js');
var Promise = global.Promise || require('bluebird');
describe('SharedMethod', function() {
var STUB_CLASS = {};
var STUB_METHOD = function(cb) { cb(); };
describe('constructor', function() {
it('normalizes "array" type in "accepts" arguments', function() {
var sharedMethod = new SharedMethod(STUB_METHOD, 'a-name', STUB_CLASS, {
accepts: {arg: 'data', type: 'array'},
});
expect(sharedMethod.accepts).to.eql([
{arg: 'data', type: ['any']},
]);
});
it('normalizes "array" type in "returns" arguments', function() {
var sharedMethod = new SharedMethod(STUB_METHOD, 'a-name', STUB_CLASS, {
returns: {arg: 'data', type: 'array'},
});
expect(sharedMethod.returns).to.eql([
{arg: 'data', type: ['any']},
]);
});
it('passes along `documented` flag correctly', function() {
var sharedMethod = new SharedMethod(STUB_METHOD, 'a-name', STUB_CLASS, {
documented: false,
});
expect(sharedMethod.documented).to.eql(false);
});
});
describe('sharedMethod.isDelegateFor(suspect, [isStatic])', function() {
// stub function
function myFunction() {}
it('checks if the given function is going to be invoked', function() {
var mockSharedClass = {};
var sharedMethod = new SharedMethod(myFunction, 'myName', mockSharedClass);
assert.equal(sharedMethod.isDelegateFor(myFunction), true);
});
it('checks by name if a function is going to be invoked', function() {
var mockSharedClass = {prototype: {myName: myFunction}};
var sharedMethod = new SharedMethod(myFunction, 'myName', mockSharedClass);
assert.equal(sharedMethod.isDelegateFor('myName', false), true);
assert.equal(sharedMethod.isDelegateFor('myName', true), false);
assert.equal(sharedMethod.isDelegateFor('myName'), true);
});
it('checks by name if static function is going to be invoked', function() {
var mockSharedClass = {myName: myFunction};
var options = {isStatic: true};
var sharedMethod = new SharedMethod(myFunction, 'myName', mockSharedClass, options);
assert.equal(sharedMethod.isDelegateFor('myName', true), true);
assert.equal(sharedMethod.isDelegateFor('myName', false), false);
});
it('checks by alias if static function is going to be invoked', function() {
var mockSharedClass = {myName: myFunction};
var options = {isStatic: true, aliases: ['myAlias']};
var sharedMethod = new SharedMethod(myFunction, 'myName', mockSharedClass, options);
assert.equal(sharedMethod.isDelegateFor('myAlias', true), true);
assert.equal(sharedMethod.isDelegateFor('myAlias', false), false);
});
it('checks if the given name is a string', function() {
var mockSharedClass = {};
var err;
try {
var sharedMethod = new SharedMethod(myFunction, Number, mockSharedClass);
} catch (e) {
err = e;
}
assert(err);
});
});
describe('sharedMethod.isDelegateForName(suspect)', function() {
// stub function
function myFunction() {}
it('checks by name if static function is going to be invoked', function() {
var mockSharedClass = {myName: myFunction};
var options = {isStatic: true};
var sharedMethod = new SharedMethod(myFunction, 'myName', mockSharedClass, options);
assert.equal(sharedMethod.isDelegateForName('myName'), true);
});
it('checks by alias if static function is going to be invoked', function() {
var mockSharedClass = {myName: myFunction};
var options = {isStatic: true, aliases: ['myAlias']};
var sharedMethod = new SharedMethod(myFunction, 'myName', mockSharedClass, options);
assert.equal(sharedMethod.isDelegateForName('myAlias'), true);
});
it('checks by name if prototype function is going to be invoked', function() {
var mockSharedClass = {myName: myFunction};
var options = {isStatic: false};
var sharedMethod = new SharedMethod(myFunction, 'myName', mockSharedClass, options);
assert.equal(sharedMethod.isDelegateForName('prototype.myName'), true);
});
it('checks by alias if prototype function is going to be invoked', function() {
var mockSharedClass = {myName: myFunction};
var options = {isStatic: false, aliases: ['myAlias']};
var sharedMethod = new SharedMethod(myFunction, 'myName', mockSharedClass, options);
assert.equal(sharedMethod.isDelegateForName('prototype.myAlias'), true);
});
it('checks if the given name is a string', function() {
var mockSharedClass = {};
var err;
var sharedMethod = new SharedMethod(myFunction, 'myName', mockSharedClass);
expect(function() { sharedMethod.isDelegateForName(myFunction); }).to.throw(/argument.*string/);
});
});
describe('sharedMethod.invoke', function() {
it('returns 400 when number argument is `NaN`', function(done) {
var method = givenSharedMethod({
accepts: {arg: 'num', type: 'number'},
});
method.invoke('ctx', {num: NaN}, {}, ctx(method), function(err) {
setImmediate(function() {
expect(err).to.exist();
expect(err.message).to.contain('not a number');
expect(err.statusCode).to.equal(400);
done();
});
});
});
describe('data type: integer', function() {
describe('SharedMethod.getType - determine actual type based on value', function() {
it('returns type: number for decimal value & integer target type',
function() {
expect(SharedMethod.getType(15.2, 'integer')).to.equal('number');
});
it('returns type:integer for intiger value & integer target type',
function() {
expect(SharedMethod.getType(14, 'integer')).to.equal('integer');
});
});
it('returns 400 when integer argument is a decimal number',
function(done) {
var method = givenSharedMethod({
accepts: {arg: 'num', type: 'integer'},
});
method.invoke('ctx', {num: 2.5}, {}, ctx(method), function(err) {
setImmediate(function() {
expect(err).to.exist();
expect(err.message).to.match(/not a safe integer/);
expect(err.statusCode).to.equal(400);
done();
});
});
});
it('returns 400 when integer argument is `NaN`', function(done) {
var method = givenSharedMethod({
accepts: {arg: 'num', type: 'integer'},
});
method.invoke('ctx', {num: NaN}, {}, ctx(method), function(err) {
setImmediate(function() {
expect(err).to.exist();
expect(err.message).to.match(/not a number/i);
expect(err.statusCode).to.equal(400);
done();
});
});
});
it('returns 400 when integer argument is not a safe integer',
function(done) {
var method = givenSharedMethod(
function(arg, cb) {
return cb({'num': arg});
},
{
accepts: {arg: 'num', type: 'integer'},
});
method.invoke('ctx', {num: 2343546576878989879789}, {}, ctx(method),
function(err) {
setImmediate(function() {
expect(err).to.exist();
expect(err.message).to.match(/integer/i);
expect(err.statusCode).to.equal(400);
done();
});
});
});
it('treats integer argument of type x.0 as integer', function(done) {
var method = givenSharedMethod(
function(arg, cb) {
return cb({'num': arg});
},
{
accepts: {arg: 'num', type: 'integer'},
});
method.invoke('ctx', {num: 12.0}, {}, ctx(method), function(result) {
setImmediate(function() {
expect(result.num).to.equal(12);
done();
});
});
});
it('returns 500 for non-integer return value if type: `integer`',
function(done) {
var method = givenSharedMethod(
function(cb) {
cb(null, 3.141);
},
{
returns: {arg: 'value', type: 'integer'},
});
method.invoke('ctx', {}, {}, ctx(method), function(err, result) {
setImmediate(function() {
expect(err).to.exist();
expect(err.message).to.match(/integer/i);
expect(err.statusCode).to.equal(500);
done();
});
});
});
it('returns 500 if returned value is not a safe integer', function(done) {
var method = givenSharedMethod(
function(cb) {
cb(null, -2343546576878989879789);
},
{
returns: {arg: 'value', type: 'integer'},
});
method.invoke('ctx', {}, {}, ctx(method), function(err, result) {
setImmediate(function() {
expect(err).to.exist();
expect(err.message).to.match(/integer/i);
expect(err.statusCode).to.equal(500);
done();
});
});
});
});
describe('data type: Date', function() {
it('converts return values to GMT timezone', function(done) {
var method = givenSharedMethod(
function(cb) {
cb(null, new Date(0));
},
{
returns: {arg: 'value', type: 'date'},
});
method.invoke('ctx', {}, {}, ctx(method), function(err, result) {
setImmediate(function() {
if (err) return done(err);
expect(result).to.eql({
value: {
$type: 'date',
$data: '1970-01-01T00:00:00.000Z',
},
});
done();
});
});
});
});
it('returns 400 and doesn\'t crash with unparsable object', function(done) {
var method = givenSharedMethod({
accepts: [{arg: 'obj', type: 'object'}],
});
method.invoke('ctx', {obj: 'test'}, {}, ctx(method), function(err) {
setImmediate(function() {
expect(err).to.exist();
expect(err.message).to.contain('not an object');
expect(err.statusCode).to.equal(400);
done();
});
});
});
it('resolves promise returned from the method', function(done) {
var method = givenSharedMethod(
function() {
return new Promise(function(resolve, reject) {
resolve(['one', 'two']);
});
},
{
returns: [
{arg: 'first', type: 'string'},
{arg: 'second', type: 'string'},
],
});
method.invoke('ctx', {}, {}, ctx(method), function(err, result) {
setImmediate(function() {
expect(result).to.eql({first: 'one', second: 'two'});
done();
});
});
});
it('handles promise resolved with a single arg', function(done) {
var method = givenSharedMethod(
function() {
return new Promise(function(resolve, reject) {
resolve('data');
});
},
{
returns: [
{arg: 'value', type: 'string'},
],
});
method.invoke('ctx', {}, {}, ctx(method), function(err, result) {
setImmediate(function() {
expect(result).to.eql({value: 'data'});
done();
});
});
});
it('handles promise resolved with a single array arg', function(done) {
var method = givenSharedMethod(
function() {
return new Promise(function(resolve, reject) {
resolve(['a', 'b']);
});
},
{
returns: [
{arg: 'value', type: ['string']},
],
});
method.invoke('ctx', {}, {}, ctx(method), function(err, result) {
setImmediate(function() {
expect(result).to.eql({value: ['a', 'b']});
done();
});
});
});
it('handles rejected promise returned from the method', function(done) {
var testError = new Error('expected test error');
var method = givenSharedMethod(function() {
return new Promise(function(resolve, reject) {
reject(testError);
});
});
method.invoke('ctx', {}, {}, ctx(method), function(err, result) {
setImmediate(function() {
expect(err).to.equal(testError);
done();
});
});
});
it('should remove from result the targeted value from promise', function(done) {
var body = {everything: 'ok'};
var method = givenSharedMethod(function() {
return Promise.resolve([201, body]);
}, {
returns: [
{arg: 'statusResult', type: 'number', http: {target: 'status'}},
{arg: 'result', type: 'object', root: true},
],
});
var context = ctx(method);
// override function that should be provided in HttpContext
context.setReturnArgByName = function(name) {
return name === 'statusResult';
};
method.invoke('ctx', {}, {}, context, function(err, result) {
setImmediate(function() {
expect(result).to.not.have.property('statusResult');
expect(result).to.eql(body);
done();
});
});
});
});
function givenSharedMethod(fn, options) {
if (options === undefined && typeof fn === 'object') {
options = fn;
fn = function() {
arguments[arguments.length - 1]();
};
}
var mockSharedClass = {fn: fn};
return new SharedMethod(fn, 'fn', mockSharedClass, options);
}
function ctx(method) {
return new Context(method, new TypeRegistry({warnOnUnknownType: false}));
}
});