@studiolabs/strong-remoting
Version:
StrongLoop Remoting Module
1,669 lines (1,467 loc) • 91.1 kB
JavaScript
// Copyright IBM Corp. 2013,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
'use strict';
var assert = require('assert');
var extend = require('util')._extend;
var inherits = require('util').inherits;
var RemoteObjects = require('../');
var SharedClass = RemoteObjects.SharedClass;
var express = require('express');
var request = require('supertest');
var expect = require('./helpers/expect');
var factory = require('./helpers/shared-objects-factory.js');
var Promise = global.Promise || require('bluebird');
var Readable = require('stream').Readable;
var ACCEPT_XML_OR_ANY = 'application/xml,*/*;q=0.8';
var TEST_ERROR = new Error('expected test error');
describe('strong-remoting-rest', function() {
var app, appSupportingJsonOnly, server, objects, remotes, lastRequest, lastResponse,
restHandlerOptions;
var adapterName = 'rest';
before(function(done) {
app = express();
app.disable('x-powered-by');
app.use(function(req, res, next) {
// create the handler for each request
const handler = objects.handler(adapterName, restHandlerOptions);
handler.apply(objects, arguments);
lastRequest = req;
lastResponse = res;
});
server = app.listen(done);
});
before(function(done) {
appSupportingJsonOnly = express();
appSupportingJsonOnly.use(function(req, res, next) {
// create the handler for each request
var supportedTypes = ['json', 'application/javascript', 'text/javascript'];
var opts = {supportedTypes: supportedTypes};
objects.handler(adapterName, opts).apply(objects, arguments);
});
server = appSupportingJsonOnly.listen(done);
});
// setup
beforeEach(function() {
restHandlerOptions = undefined;
objects = RemoteObjects.create({
json: {limit: '1kb'},
errorHandler: {debug: true, log: false},
types: {warnOnUnknownType: false},
});
remotes = objects.exports;
// connect to the app
objects.connect('http://localhost:' + server.address().port, adapterName);
});
before(() => {
process.on('unhandledRejection', unhandledRejection);
});
after(() => {
process.removeListener('unhandledRejection', unhandledRejection);
});
function json(method, url) {
if (url === undefined) {
url = method;
method = 'get';
}
return request(app)[method](url)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.expect('Content-Type', /json/);
}
function xml(method, url) {
if (url === undefined) {
url = method;
method = 'get';
}
return request(app)[method](url)
.set('Accept', 'application/xml')
.set('Content-Type', 'application/xml')
.expect('Content-Type', /xml/);
}
describe('remoting options', function() {
// The 1kb limit is set by RemoteObjects.create({json: {limit: '1kb'}});
it('should reject json payload larger than 1kb', function(done) {
var method = givenSharedStaticMethod(
function greet(msg, cb) {
cb(null, msg);
},
{
accepts: {arg: 'person', type: 'string', http: {source: 'body'}},
returns: {arg: 'msg', type: 'string'},
}
);
// Build an object that is larger than 1kb
var name = '';
for (var i = 0; i < 2048; i++) {
name += '11111111111';
}
request(app).post(method.url)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.send(name)
.expect(413, done);
});
it('should allow custom error handlers', function(done) {
var called = false;
var method = givenSharedStaticMethod(
function(cb) {
cb(new Error('foo'));
}
);
objects.options.errorHandler.handler = function(err, req, res, next) {
expect(err.message).to.contain('foo');
err = new Error('foobar');
called = true;
next(err);
};
request(app).get(method.url)
.expect('Content-Type', /json/)
.expect(500)
.end(expectErrorResponseContaining({message: 'foobar'}, function(err) {
expect(called).to.eql(true);
done(err);
}));
});
it('should exclude stack traces by default', function(done) {
var method = givenSharedStaticMethod(
function(cb) { cb(new Error('test-error')); });
// reset the errorHandler options
objects.options.errorHandler = {};
request(app).get(method.url)
.expect('Content-Type', /json/)
.expect(500)
.end(expectErrorResponseContaining(
{message: 'Internal Server Error'}, ['stack'], done));
});
it('should turn off url-not-found handler', function(done) {
objects.options.rest = {handleUnknownPaths: false};
app.use(function(req, res, next) {
res.status(404).send('custom-not-found');
});
request(app).get('/thisUrlDoesNotExists/someMethod')
.expect(404)
.expect('custom-not-found')
.end(done);
});
it('should turn off method-not-found handler', function(done) {
var method = givenSharedStaticMethod();
objects.options.rest = {handleUnknownPaths: false};
app.use(function(req, res, next) {
res.send(404, 'custom-not-found');
});
request(app).get(method.classUrl + '/thisMethodDoesNotExist')
.expect(404)
.expect('custom-not-found')
.end(done);
});
it('should by default use defined error handler', function(done) {
app.use(function(err, req, res, next) {
res.send('custom-error-handler-called');
});
request(app).get('/thisUrlDoesNotExists/someMethod')
.expect(404)
.expect(function(res) {
expect(res.text).not.to.equal('custom-error-handler-called');
})
.end(done);
});
it('should turn off error handler', function(done) {
objects.options.rest = {handleErrors: false};
app.use(function(err, req, res, next) {
res.send('custom-error-handler-called');
});
request(app).get('/thisUrlDoesNotExists/someMethod')
.expect(200)
.expect('custom-error-handler-called')
.end(done);
});
it('should configure custom REST content types', function(done) {
var supportedTypes = ['json', 'application/javascript', 'text/javascript'];
objects.options.rest = {supportedTypes: supportedTypes};
var method = givenSharedStaticMethod(
function(cb) {
cb(null, {key: 'value'});
},
{
returns: {arg: 'result', type: 'object'},
}
);
var browserAcceptHeader = [
'text/html',
'application/xhtml+xml',
'application/xml;q=0.9',
'image/webp',
'*/*;q=0.8',
].join(',');
request(app).get(method.url)
.set('Accept', browserAcceptHeader)
.expect('Content-Type', 'application/json; charset=utf-8')
.expect(200, done);
});
it('should disable XML content types by default', function(done) {
delete objects.options.rest;
var method = givenSharedStaticMethod(
function(cb) { cb(null, {key: 'value'}); },
{returns: {arg: 'result', type: 'object'}}
);
request(app).get(method.url)
.set('Accept', ACCEPT_XML_OR_ANY)
.expect(200)
.expect('Content-Type', /json/)
.end(done);
});
it('should enable XML types via `options.rest.xml`', function(done) {
objects.options.rest = {xml: true};
var method = givenSharedStaticMethod(
function(value, cb) { cb(null, {key: value}); },
{
accepts: {arg: 'value', type: 'string'},
returns: {arg: 'result', type: 'object'},
});
request(app).post(method.url)
.set('Accept', ACCEPT_XML_OR_ANY)
.set('Content-Type', 'application/json')
.send({value: 'some-value'})
.expect(200)
.expect('Content-Type', /xml/)
.end(function(err, res) {
if (err) return done(err);
expect(res.text.replace(/>\s+</mg, '><')).to.equal(
'<?xml version="1.0" encoding="UTF-8"?>' +
'<response><result><key>some-value</key></result></response>'
);
done();
});
});
it('should enable XML via `options.rest.supportedTypes`', function(done) {
objects.options.rest = {supportedTypes: ['application/xml']};
var method = givenSharedStaticMethod(
function(cb) { cb(null, 'value'); },
{returns: {arg: 'result', type: 'object'}}
);
request(app).post(method.url)
.set('Accept', ACCEPT_XML_OR_ANY)
.expect(200)
.expect('Content-Type', /xml/)
.end(done);
});
it('should treat application/vnd.api+json accept header correctly', function(done) {
objects.options.rest = {supportedTypes: ['application/vnd.api+json']};
var method = givenSharedStaticMethod(
function(cb) { cb(null, {value: 'value'}); },
{returns: {arg: 'result', type: 'object'}}
);
request(app).get(method.url)
.set('Accept', 'application/vnd.api+json')
.expect(200)
.expect('Content-Type', /application\/vnd\.api\+json/)
.end(function(err, res) {
if (err) return done(err);
expect(res.body).to.deep.equal({result: {value: 'value'}});
done();
});
});
});
describe('CORS', function() {
var method;
beforeEach(function() {
method = givenSharedStaticMethod(
function greet(person, cb) {
if (person === 'error') {
var err = new Error('error');
err.statusCode = 400;
cb(err);
} else {
cb(null, 'hello');
}
},
{
accepts: {arg: 'person', type: 'string'},
returns: {arg: 'msg', type: 'string'},
}
);
});
it('should reject cross-origin requests', function(done) {
request(app).post(method.url)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.set('Origin', 'http://localhost:3001')
.send({person: 'ABC'})
.expect(200, function(err, res) {
expect(Object.keys(res.headers)).to.not.include.members([
'access-control-allow-origin',
'access-control-allow-credentials',
]);
done();
});
});
it('should reject preflight (OPTIONS) requests', function(done) {
request(app).options(method.url)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.set('Origin', 'http://localhost:3001')
.send()
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2
.expect(200, function(err, res) {
expect(Object.keys(res.headers)).to.not.include.members([
'access-control-allow-origin',
'access-control-allow-credentials',
]);
done();
});
});
});
function enableXmlSupport() {
objects.options.rest = objects.options.rest || {};
objects.options.rest.xml = true;
}
describe('call of constructor method', function() {
beforeEach(enableXmlSupport);
it('should work', function(done) {
var method = givenSharedStaticMethod(
function greet(msg, cb) {
cb(null, msg);
},
{
accepts: {arg: 'person', type: 'string'},
returns: {arg: 'msg', type: 'string'},
}
);
json(method.url + '?person=hello')
.expect(200, {msg: 'hello'}, done);
});
it('should honor Accept: header', function(done) {
var method = givenSharedStaticMethod(
function greet2(msg, cb) {
cb(null, msg);
},
{
accepts: {arg: 'person', type: 'string'},
returns: {arg: 'msg', type: 'string'},
}
);
xml(method.url + '?person=hello')
.expect(200, '<?xml version="1.0" encoding="UTF-8"?>\n<response>\n ' +
'<msg>hello</msg>\n</response>', done);
});
it('should handle returns of array', function(done) {
var method = givenSharedStaticMethod(
function greet3(msg, cb) {
cb(null, [msg]);
},
{
accepts: {arg: 'person', type: ['string']},
returns: {arg: 'msg', type: 'string'},
}
);
xml(method.url + '?person=["hello"]')
.expect(200, '<?xml version="1.0" encoding="UTF-8"?>\n<response>\n ' +
'<msg>hello</msg>\n</response>', done);
});
it('should handle returns of array to XML', function(done) {
var method = givenSharedStaticMethod(
function greet4(msg, cb) {
cb(null, [msg]);
},
{
accepts: {arg: 'person', type: ['string']},
returns: {arg: 'msg', type: ['string'], root: true},
}
);
xml(method.url + '?person=["hello"]')
.expect(200, '<?xml version="1.0" encoding="UTF-8"?>\n<response>\n ' +
'<result>hello</result>\n</response>', done);
});
it('should allow arguments in the path', function(done) {
var method = givenSharedStaticMethod(
function bar(a, b, cb) {
cb(null, a + b);
},
{
accepts: [
{arg: 'b', type: 'number'},
{arg: 'a', type: 'number', http: {source: 'path'}},
],
returns: {arg: 'n', type: 'number'},
http: {path: '/:a'},
}
);
json(method.classUrl + '/1?b=2')
.expect({n: 3}, done);
});
it('should allow arguments in the query', function(done) {
var method = givenSharedStaticMethod(
function bar(a, b, cb) {
cb(null, a + b);
},
{
accepts: [
{arg: 'b', type: 'number'},
{arg: 'a', type: 'number', http: {source: 'query'}},
],
returns: {arg: 'n', type: 'number'},
http: {path: '/'},
}
);
json(method.classUrl + '/?a=1&b=2')
.expect({n: 3}, done);
});
it('should allow string[] arg in the query', function(done) {
var method = givenSharedStaticMethod(
function bar(a, b, cb) {
cb(null, b.join('') + a);
},
{
accepts: [
{arg: 'a', type: 'string'},
{arg: 'b', type: ['string'], http: {source: 'query'}},
],
returns: {arg: 'n', type: 'string'},
http: {path: '/'},
}
);
json(method.classUrl + '/?a=z&b[0]=x&b[1]=y')
.expect({n: 'xyz'}, done);
});
it('should allow string[] arg in the query with stringified value',
function(done) {
var method = givenSharedStaticMethod(
function bar(a, b, cb) {
cb(null, b.join('') + a);
},
{
accepts: [
{arg: 'a', type: 'string'},
{arg: 'b', type: ['string'], http: {source: 'query'}},
],
returns: {arg: 'n', type: 'string'},
http: {path: '/'},
}
);
json(method.classUrl + '/?a=z&b=["x", "y"]')
.expect({n: 'xyz'}, done);
});
it('should allow custom argument functions', function(done) {
var method = givenSharedStaticMethod(
function bar(a, b, cb) {
cb(null, a + b);
},
{
accepts: [
{arg: 'b', type: 'number'},
{arg: 'a', type: 'number', http: function(ctx) {
return +ctx.req.query.a;
}},
],
returns: {arg: 'n', type: 'number'},
http: {path: '/'},
}
);
json(method.classUrl + '/?a=1&b=2')
.expect({n: 3}, done);
});
it('should pass undefined if the argument is not supplied', function(done) {
var called = false;
var method = givenSharedStaticMethod(
function bar(a, cb) {
called = true;
assert(a === undefined, 'a should be undefined');
cb();
},
{
accepts: [
{arg: 'b', type: 'number'},
],
}
);
json(method.url).end(function() {
assert(called);
done();
});
});
it('should allow arguments in the body', function(done) {
var method = givenSharedStaticMethod(
function bar(a, cb) {
cb(null, a);
},
{
accepts: [
{arg: 'a', type: 'object', http: {source: 'body'}},
],
returns: {arg: 'data', type: 'object', root: true},
http: {path: '/'},
}
);
request(app).post(method.classUrl)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.send('{"x": 1, "y": "Y"}')
.expect('Content-Type', /json/)
.expect(200, function(err, res) {
expect(res.body).to.deep.equal({'x': 1, 'y': 'Y'});
done(err, res);
});
});
it('should allow arguments in the body with date', function(done) {
var method = givenSharedStaticMethod(
function bar(a, cb) {
cb(null, a);
},
{
accepts: [
{arg: 'a', type: 'object', http: {source: 'body'}},
],
returns: {arg: 'data', type: 'object', root: true},
http: {path: '/'},
}
);
var data = {date: {$type: 'date', $data: new Date()}};
request(app).post(method.classUrl)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.send(data)
.expect('Content-Type', /json/)
.expect(200, function(err, res) {
expect(res.body).to.deep.equal({date: data.date.$data.toISOString()});
done(err, res);
});
});
it('should allow arguments in the form', function(done) {
var method = givenSharedStaticMethod(
function bar(a, b, cb) {
cb(null, a + b);
},
{
accepts: [
{arg: 'b', type: 'number', http: {source: 'form'}},
{arg: 'a', type: 'number', http: {source: 'form'}},
],
returns: {arg: 'n', type: 'number'},
http: {path: '/'},
}
);
request(app).post(method.classUrl)
.set('Accept', 'application/json')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send('a=1&b=2')
.expect('Content-Type', /json/)
.expect({n: 3}, done);
});
it('should allow arguments in the header', function(done) {
var method = givenSharedStaticMethod(
function bar(a, b, cb) {
cb(null, a + b);
},
{
accepts: [
{arg: 'b', type: 'number', http: {source: 'header'}},
{arg: 'a', type: 'number', http: {source: 'header'}},
],
returns: {arg: 'n', type: 'number'},
http: {verb: 'get', path: '/'},
}
);
request(app).get(method.classUrl)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.set('a', 1)
.set('b', 2)
.send()
.expect('Content-Type', /json/)
.expect({n: 3}, done);
});
it('should allow arguments in the header without http source',
function(done) {
var method = givenSharedStaticMethod(
function bar(a, b, cb) {
cb(null, a + b);
},
{
accepts: [
{arg: 'b', type: 'number'},
{arg: 'a', type: 'number'},
],
returns: {arg: 'n', type: 'number'},
http: {verb: 'get', path: '/'},
}
);
request(app).get(method.classUrl)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.set('a', 1)
.set('b', 2)
.send()
.expect('Content-Type', /json/)
.expect({n: 3}, done);
});
it('should allow arguments from http req and res', function(done) {
var method = givenSharedStaticMethod(
function bar(req, res, cb) {
res.status(200).send(req.body);
},
{
accepts: [
{arg: 'req', type: 'object', http: {source: 'req'}},
{arg: 'res', type: 'object', http: {source: 'res'}},
],
http: {path: '/'},
}
);
request(app).post(method.classUrl)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.send('{"x": 1, "y": "Y"}')
.expect('Content-Type', /json/)
.expect(200, function(err, res) {
expect(res.body).to.deep.equal({'x': 1, 'y': 'Y'});
done(err, res);
});
});
it('should allow arguments from http context', function(done) {
var method = givenSharedStaticMethod(
function bar(ctx, cb) {
ctx.res.status(200).send(ctx.req.body);
},
{
accepts: [
{arg: 'ctx', type: 'object', http: {source: 'context'}},
],
http: {path: '/'},
}
);
request(app).post(method.classUrl)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.send('{"x": 1, "y": "Y"}')
.expect('Content-Type', /json/)
.expect(200, function(err, res) {
expect(res.body).to.deep.equal({'x': 1, 'y': 'Y'});
done(err, res);
});
});
it('should respond with 204 if returns is not defined', function(done) {
var method = givenSharedStaticMethod(
function(cb) { cb(null, 'value-to-ignore'); }
);
json(method.url)
.expect(204, done);
});
it('should preserve non-200 status when responding with no content', function(done) {
var method = givenSharedStaticMethod(
function(ctx, cb) {
ctx.res.status(302);
cb();
}, {
accepts: [
{
arg: 'ctx',
type: 'object',
http: {
source: 'context',
},
},
],
});
request(app).get(method.url)
.set('Accept', 'application/json')
.expect(302, done);
});
it('should accept custom content-type header if respond with 204', function(done) {
var method = givenSharedStaticMethod();
objects.before(method.name, function(ctx, next) {
ctx.res.set('Content-Type',
'application/json; charset=utf-8; profile=http://example.org/');
next();
});
request(app).get(method.url)
.set('Accept', 'application/json')
.expect('Content-Type',
'application/json; charset=utf-8; profile=http://example.org/')
.expect(204, done);
});
it('should respond with named results if returns has multiple args', function(done) {
var method = givenSharedStaticMethod(
function(a, b, cb) {
cb(null, a, b);
},
{
accepts: [
{arg: 'a', type: 'number'},
{arg: 'b', type: 'number'},
],
returns: [
{arg: 'a', type: 'number'},
{arg: 'b', type: 'number'},
],
}
);
json(method.url + '?a=1&b=2')
.expect({a: 1, b: 2}, done);
});
it('should remove any X-Powered-By header to LoopBack', function(done) {
var method = givenSharedStaticMethod(
function(cb) { cb(null, 'value-to-ignore'); }
);
json(method.url)
.expect(204)
.end(function(err, result) {
expect(result.headers).not.to.have.keys(['x-powered-by']);
done();
});
});
it('should report error for mismatched arg type', function(done) {
remotes.foo = {
bar: function(a, fn) {
fn(null, a);
},
};
var fn = remotes.foo.bar;
fn.shared = true;
fn.accepts = [
{arg: 'a', type: 'object'},
];
fn.returns = {root: true};
json('get', '/foo/bar?a=foo')
.expect(400, done);
});
it('should not coerce nested boolean strings - true', function(done) {
remotes.foo = {
bar: function(a, fn) {
fn(null, a);
},
};
var fn = remotes.foo.bar;
fn.shared = true;
fn.accepts = [
{arg: 'a', type: 'object'},
];
fn.returns = {root: true};
json('get', '/foo/bar?a[foo]=true')
.expect({foo: 'true'}, done);
});
it('should not coerce nested boolean strings - false', function(done) {
remotes.foo = {
bar: function(a, fn) {
fn(null, a);
},
};
var fn = remotes.foo.bar;
fn.shared = true;
fn.accepts = [
{arg: 'a', type: 'object'},
];
fn.returns = {root: true};
json('get', '/foo/bar?a[foo]=false')
.expect({foo: 'false'}, done);
});
it('should coerce number strings', function(done) {
remotes.foo = {
bar: function(a, b, fn) {
fn(null, a + b);
},
};
var fn = remotes.foo.bar;
fn.shared = true;
fn.accepts = [
{arg: 'a', type: 'number'},
{arg: 'b', type: 'number'},
];
fn.returns = {root: true};
json('get', '/foo/bar?a=42&b=0.42')
.expect(200, function(err, res) {
assert.equal(res.body, 42.42);
done();
});
});
it('should coerce strings with type set to "any"', function(done) {
remotes.foo = {
bar: function(a, b, c, fn) {
fn(null, c === true ? a + b : 0);
},
};
var fn = remotes.foo.bar;
fn.shared = true;
fn.accepts = [
{arg: 'a', type: 'any'},
{arg: 'b', type: 'any'},
{arg: 'c', type: 'any'},
];
fn.returns = {root: true};
json('get', '/foo/bar?a=42&b=0.42&c=true')
.expect(200, function(err, res) {
assert.equal(res.body, 42.42);
done();
});
});
describe('data type - integer', function() {
it('should coerce integer strings', function(done) {
remotes.foo = {
bar: function(a, b, fn) {
fn(null, a + b);
},
};
var fn = remotes.foo.bar;
fn.shared = true;
fn.accepts = [
{arg: 'a', type: 'integer'},
{arg: 'b', type: 'integer'},
];
fn.returns = {root: true};
json('get', '/foo/bar?a=53&b=2')
.expect(200, function(err, res) {
assert.equal(res.body, 55);
done();
});
});
it('supports target type [integer]', function(done) {
var method = givenSharedStaticMethod(
function(arg, cb) {
cb(null, {value: arg});
},
{
accepts: {arg: 'arg', type: ['integer']},
returns: {arg: 'data', type: ['integer'], root: true},
http: {method: 'POST'},
});
request(app).post(method.url)
.send({arg: [1, 2]})
.expect(200, {value: [1, 2]})
.end(done);
});
it('supports return type [integer]', function(done) {
var method = givenSharedStaticMethod(
function(arg, cb) {
cb(null, [arg[0], arg[1]]);
},
{
accepts: {arg: 'arg', type: ['number']},
returns: {arg: 'data', type: ['integer']},
http: {method: 'POST'},
});
request(app).post(method.url)
.send({arg: [1, 2]})
.expect(200, {data: [1, 2]})
.end(done);
});
});
it('should pass an array argument even when non-array passed', function(done) {
remotes.foo = {
bar: function(a, fn) {
fn(null, Array.isArray(a));
},
};
var fn = remotes.foo.bar;
fn.shared = true;
fn.accepts = [
{arg: 'a', type: ['number']},
];
fn.returns = {root: true};
json('get',
'/foo/bar?a=1234')
.expect(200, function(err, res) {
assert.equal(res.body, true);
done();
});
});
it('should coerce contents of array with simple array types', function(done) {
remotes.foo = {
bar: function(a, fn) {
fn(null, a.reduce(function(memo, val) { return memo + val; }, 0));
},
};
var fn = remotes.foo.bar;
fn.shared = true;
fn.accepts = [
{arg: 'a', type: ['number']},
];
fn.returns = {root: true};
json('get', '/foo/bar?a=[1,2,3,4,5]')
.expect(200, function(err, res) {
assert.equal(res.body, 15);
done();
});
});
it('should not flatten arrays for target type "any"', function(done) {
var method = givenSharedStaticMethod(
function(arg, cb) { cb(null, {value: arg}); },
{
accepts: {arg: 'arg', type: 'any'},
returns: {arg: 'data', type: 'any', root: true},
http: {method: 'POST'},
});
request(app).post(method.url)
.send({arg: ['single']})
.expect(200, {value: ['single']})
.end(done);
});
it('should support taget type [any]', function(done) {
var method = givenSharedStaticMethod(
function(arg, cb) { cb(null, {value: arg}); },
{
accepts: {arg: 'arg', type: ['any']},
returns: {arg: 'data', type: ['any'], root: true},
http: {method: 'POST'},
});
request(app).post(method.url)
.send({arg: ['single']})
.expect(200, {value: ['single']})
.end(done);
});
it('should support taget type `array` - of string', function(done) {
var method = givenSharedStaticMethod(
function(arg, cb) { cb(null, {value: arg}); },
{
accepts: {arg: 'arg', type: 'array'},
returns: {arg: 'data', type: 'array', root: true},
http: {method: 'POST'},
});
request(app).post(method.url)
.send({arg: ['single']})
.expect(200, {value: ['single']})
.end(done);
});
it('should support taget type `array` - of number', function(done) {
var method = givenSharedStaticMethod(
function(arg, cb) { cb(null, {value: arg}); },
{
accepts: {arg: 'arg', type: 'array'},
returns: {arg: 'data', type: 'array', root: true},
http: {method: 'POST'},
});
request(app).post(method.url)
.send({arg: [1]})
.expect(200, {value: [1]})
.end(done);
});
it('should support taget type `array` - of object', function(done) {
var method = givenSharedStaticMethod(
function(arg, cb) { cb(null, {value: arg}); },
{
accepts: {arg: 'arg', type: 'array'},
returns: {arg: 'data', type: 'array', root: true},
http: {method: 'POST'},
});
request(app).post(method.url)
.send({arg: [{foo: 'bar'}]})
.expect(200, {value: [{foo: 'bar'}]})
.end(done);
});
it('should allow empty body for json request', function(done) {
remotes.foo = {
bar: function(a, b, fn) {
fn(null, a, b);
},
};
var fn = remotes.foo.bar;
fn.shared = true;
fn.accepts = [
{arg: 'a', type: 'number'},
{arg: 'b', type: 'number'},
];
fn.returns = [
{arg: 'a', type: 'number'},
{arg: 'b', type: 'number'},
];
json('post', '/foo/bar?a=1&b=2').set('Content-Length', 0)
.expect({a: 1, b: 2}, done);
});
it('should split array string when configured', function(done) {
objects.options.rest = {arrayItemDelimiters: [',', '|']};
var method = givenSharedStaticMethod(
function(a, cb) { cb(null, a); },
{
accepts: {arg: 'a', type: ['number']},
returns: {arg: 'data', type: 'object'},
});
json('post', method.url + '?a=1,2|3')
.expect({data: [1, 2, 3]}, done);
});
it('should not create empty string array with empty string arg', function(done) {
objects.options.rest = {arrayItemDelimiters: [',', '|']};
var method = givenSharedStaticMethod(
function(a, cb) { cb(null, a); },
{
accepts: {arg: 'a', type: ['number']},
returns: {arg: 'data', type: 'object'},
});
json('post', method.url + '?a=')
.expect({ /* data is undefined */ }, done);
});
it('should still support JSON arrays with arrayItemDelimiters', function(done) {
objects.options.rest = {arrayItemDelimiters: [',', '|']};
var method = givenSharedStaticMethod(
function(a, cb) { cb(null, a); },
{
accepts: {arg: 'a', type: ['number']},
returns: {arg: 'data', type: 'object'},
});
json('post', method.url + '?a=[1,2,3]')
.expect({data: [1, 2, 3]}, done);
});
it('should call rest hooks', function(done) {
var hooksCalled = [];
var method = givenSharedStaticMethod({
rest: {
before: createHook('beforeRest'),
after: createHook('afterRest'),
},
});
objects.before(method.name, createHook('beforeRemote'));
objects.after(method.name, createHook('afterRemote'));
json(method.url)
.end(function(err) {
if (err) done(err);
assert.deepEqual(
hooksCalled,
['beforeRest', 'beforeRemote', 'afterRemote', 'afterRest']
);
done();
});
function createHook(name) {
return function(ctx, next) {
hooksCalled.push(name);
next();
};
}
});
it('should respect supported types', function(done) {
var method = givenSharedStaticMethod(
function(cb) {
cb(null, {key: 'value'});
},
{
returns: {arg: 'result', type: 'object'},
}
);
request(appSupportingJsonOnly).get(method.url)
.set('Accept',
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8')
.expect('Content-Type', 'application/json; charset=utf-8')
.expect(200, done);
});
describe('xml support', function() {
beforeEach(enableXmlSupport);
it('should produce xml from json objects', function(done) {
var method = givenSharedStaticMethod(
function bar(a, cb) {
cb(null, a);
},
{
accepts: [
{arg: 'a', type: 'object', http: {source: 'body'}},
],
returns: {arg: 'data', type: 'object', root: true},
http: {path: '/'},
}
);
request(app).post(method.classUrl)
.set('Accept', 'application/xml')
.set('Content-Type', 'application/json')
.send('{"x": 1, "y": "Y"}')
.expect('Content-Type', /xml/)
.expect(200, function(err, res) {
expect(res.text).to.equal('<?xml version="1.0" encoding="UTF-8"?>\n' +
'<response>\n <x>1</x>\n <y>Y</y>\n</response>');
done(err, res);
});
});
it('should produce xml from json array', function(done) {
var method = givenSharedStaticMethod(
function bar(cb) {
cb(null, [1, 2, 3]);
},
{
returns: {arg: 'data', type: ['number'], root: true},
http: {path: '/', verb: 'get'},
}
);
request(app).get(method.classUrl)
.set('Accept', 'application/xml')
.set('Content-Type', 'application/json')
.send('{"x": 1, "y": "Y"}')
.expect('Content-Type', /xml/)
.expect(200, function(err, res) {
expect(res.text).to.equal('<?xml version=\"1.0\" ' +
'encoding=\"UTF-8\"?>\n<response>\n <result>1</result>\n ' +
'<result>2</result>\n <result>3</result>\n</response>');
done(err, res);
});
});
it('should produce xml from json objects with toJSON()', function(done) {
var method = givenSharedStaticMethod(
function bar(a, cb) {
var result = a;
a.toJSON = function() {
return {
foo: a.y,
bar: a.x,
};
};
cb(null, a);
},
{
accepts: [
{arg: 'a', type: 'object', http: {source: 'body'}},
],
returns: {arg: 'data', type: 'object', root: true},
http: {path: '/'},
}
);
request(app).post(method.classUrl)
.set('Accept', 'application/xml')
.set('Content-Type', 'application/json')
.send('{"x": 1, "y": "Y"}')
.expect('Content-Type', /xml/)
.expect(200, function(err, res) {
expect(res.text).to.equal('<?xml version="1.0" encoding="UTF-8"?>\n' +
'<response>\n <foo>Y</foo>\n <bar>1</bar>\n</response>');
done(err, res);
});
});
it('should produce xml from json objects with toJSON() inside an array',
function(done) {
var method = givenSharedStaticMethod(
function bar(a, cb) {
a.toJSON = function() {
return {
foo: a.y,
bar: a.x,
};
};
cb(null, [a, {c: 1}]);
},
{
accepts: [
{arg: 'a', type: 'object', http: {source: 'body'}},
],
returns: {arg: 'data', type: 'object', root: true},
http: {path: '/'},
}
);
request(app).post(method.classUrl)
.set('Accept', 'application/xml')
.set('Content-Type', 'application/json')
.send('{"x": 1, "y": "Y"}')
.expect('Content-Type', /xml/)
.expect(200, function(err, res) {
expect(res.text).to.equal('<?xml version=\"1.0\" ' +
'encoding=\"UTF-8\"?>\n<response>\n <result>\n ' +
'<foo>Y</foo>\n <bar>1</bar>\n </result>\n <result>\n ' +
'<c>1</c>\n </result>\n</response>');
done(err, res);
});
});
it('should allow customized xml root element', function(done) {
var method = givenSharedStaticMethod(
function bar(cb) {
cb(null, {a: 1, b: 2});
},
{
returns: {
arg: 'data', type: 'object', root: true,
xml: {wrapperElement: 'foo'},
},
http: {path: '/'},
}
);
request(app).get(method.classUrl)
.set('Accept', 'application/xml')
.set('Content-Type', 'application/json')
.send()
.expect('Content-Type', /xml/)
.expect(200, function(err, res) {
expect(res.text).to.equal(
'<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n' +
'<foo>\n ' +
'<a>1</a>\n ' +
'<b>2</b>\n' +
'</foo>');
done(err, res);
});
});
it('should allow xml declaration to be disabled', function(done) {
var method = givenSharedStaticMethod(
function bar(cb) {
cb(null, {a: 1, b: 2});
},
{
returns: {
arg: 'data', type: 'object', root: true,
xml: {declaration: false},
},
http: {path: '/'},
}
);
request(app).get(method.classUrl)
.set('Accept', 'application/xml')
.set('Content-Type', 'application/json')
.send()
.expect('Content-Type', /xml/)
.expect(200, function(err, res) {
expect(res.text).to.equal(
'<response>\n ' +
'<a>1</a>\n ' +
'<b>2</b>\n' +
'</response>');
done(err, res);
});
});
it('should allow string results to output as xml', function(done) {
var method = givenSharedStaticMethod(
function bar(cb) {
var stringResult = 'a quick brown fox jumps over the lazy dog';
cb(null, stringResult);
},
{
returns: {
root: true,
xml: {wrapperElement: 'text'},
},
http: {path: '/'},
}
);
request(app).get(method.classUrl)
.set('Accept', 'application/xml')
.set('Content-Type', 'application/json')
.send()
.expect('Content-Type', /xml/)
.expect(200, function(err, res) {
expect(res.text).to.equal(
'<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n' +
'<text>a quick brown fox jumps over the lazy dog' +
'</text>');
done(err, res);
});
});
it('should handle UTF-8 & special & reserved characters', function(done) {
var method = givenSharedStaticMethod(
function bar(cb) {
var stringA = 'foo\xC1\xE1\u0102\u03A9asd><=$~!@#$%^&*()-_=+/.,;\'"[]{}?';
cb(null, {a: stringA});
},
{
returns: {
arg: 'data', type: 'object', root: true,
xml: {wrapperElement: false},
},
http: {path: '/'},
}
);
request(app).get(method.classUrl)
.set('Accept', 'application/xml')
.set('Content-Type', 'application/json')
.send()
.expect('Content-Type', /xml.*charset=utf-8/)
.expect(200, function(err, res) {
expect(res.text).to.equal(
'<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n' +
'<response>\n ' +
'<a>fooÁáĂΩasd><=$~!@#$%^&*()-_=+/.,;\'"[]{}?</a>\n' +
'</response>');
done();
});
});
it('should produce xml from json objects with toXML()', function(done) {
var method = givenSharedStaticMethod(
function bar(a, cb) {
var result = a;
a.toXML = function() {
return '<?xml version="1.0" encoding="UTF-8"?>' +
'<root><x>10</x></root>';
};
cb(null, a);
},
{
accepts: [
{arg: 'a', type: 'object', http: {source: 'body'}},
],
returns: {arg: 'data', type: 'object', root: true},
http: {path: '/'},
}
);
request(app).post(method.classUrl)
.set('Accept', 'application/xml')
.set('Content-Type', 'application/json')
.send('{"x": 1, "y": "Y"}')
.expect('Content-Type', /xml/)
.expect(200, function(err, res) {
expect(res.text).to.equal('<?xml version="1.0" encoding="UTF-8"?>' +
'<root><x>10</x></root>');
done(err, res);
});
});
});
describe('_format support', function() {
it('should produce xml if _format is xml', function(done) {
var method = givenSharedStaticMethod(
function bar(a, cb) {
cb(null, a);
},
{
accepts: [
{arg: 'a', type: 'object', http: {source: 'body'}},
],
returns: {arg: 'data', type: 'object', root: true},
http: {path: '/'},
}
);
request(app).post(method.classUrl + '?_format=xml')
.set('Accept', '*/*')
.set('Content-Type', 'application/json')
.send('{"x": 1, "y": "Y"}')
.expect('Content-Type', /xml/)
.expect(200, function(err, res) {
expect(res.text).to.equal('<?xml version="1.0" encoding="UTF-8"?>\n' +
'<response>\n <x>1</x>\n <y>Y</y>\n</response>');
done(err, res);
});
});
it('should produce json if _format is json', function(done) {
var method = givenSharedStaticMethod(
function bar(a, cb) {
cb(null, a);
},
{
accepts: [
{arg: 'a', type: 'object', http: {source: 'body'}},
],
returns: {arg: 'data', type: 'object', root: true},
http: {path: '/'},
}
);
request(app).post(method.classUrl + '?_format=json')
.set('Accept', 'application/xml')
.set('Content-Type', 'application/json')
.send('{"x": 1, "y": "Y"}')
.expect('Content-Type', /json/)
.expect(200, function(err, res) {
expect(res.body).to.deep.equal({x: 1, y: 'Y'});
done(err, res);
});
});
it('should return a 400 if _format array', function(done) {
var method = givenSharedStaticMethod(
function bar(a, cb) {
cb(null, a);
},
{
accepts: [
{arg: 'a', type: 'object', http: {source: 'body'}},
],
returns: {arg: 'data', type: 'object', root: true},
http: {path: '/'},
}
);
request(app).post(method.classUrl + '?_format=json&_format=xml')
.set('Accept', 'application/xml')
.set('Content-Type', 'application/json')
.send('{"x": 1, "y": "Y"}')
.expect(406, function(err, res) {
console.log(err);
done(err, res);
});
});
});
describe('uncaught errors', function() {
it('should return 500 if an error object is thrown', function(done) {
remotes.shouldThrow = {
bar: function(fn) {
throw new Error('an error');
},
};
var fn = remotes.shouldThrow.bar;
fn.shared = true;
json('get', '/shouldThrow/bar?a=1&b=2')
.expect(500)
.end(expectErrorResponseContaining({message: 'an error'}, done));
});
it('should return 500 if an array of errors is thrown', function(done) {
var testError = new Error('expected test error');
var errArray = [testError, testError];
function method(error) {
return givenSharedStaticMethod(function(cb) {
cb(error);
});
}
request(app).get(method(testError).url)
.set('Accept', 'application/json')
.expect(500)
.end(function(err, res) {
if (err) return done(err);
var expectedDetail = res.body.error;
request(app).get(method(errArray).url)
.set('Accept', 'application/json')
.expect(500)
.end(function(err, res) {
if (err) return done(err);
var error = res.body.error;
expect(error).to.have.property('message').that.match(/multiple errors/);
expect(error).to.include.keys('details');
expect(error.details).to.deep.include(expectedDetail);
done();
});
});
});
it('should return 500 if an error string is thrown', function(done) {
remotes.shouldThrow = {
bar: function(fn) {
throw 'an error';
},
};
var fn = remotes.shouldThrow.bar;
fn.shared = true;
json('get', '/shouldThrow/bar?a=1&b=2')
.expect(500)
.end(expectErrorResponseContaining({message: 'an error'}, done));
});
it('should return 500 for unhandled errors thrown from before hooks',
function(done) {
var method = givenSharedStaticMethod();
objects.before(method.name, function(ctx, next) {
process.nextTick(next);
});
objects.before(method.name, function(ctx, next) {
throw new Error('test error');
});
request(app).get(method.url)
.set('Accept', 'application/json')
.expect(500)
.end(expectErrorResponseContaining({message: 'test error'}, done));
});
});
it('should return 500 when method returns an error', function(done) {
var method = givenSharedStaticMethod(
function(cb) {
cb(new Error('test-error'));
}
);
// Send a plain, non-json request to make sure the error handler
// always returns a json response.
request(app).get(method.url)
.expect('Content-Type', /json/)
.expect(500)
.end(expectErrorResponseContaining({message: 'test-error'}, done));
});
it('should return 500 when "before" returns an error', function(done) {
var method = givenSharedStaticMethod();
objects.before(method.name, function(ctx, next) {
next(new Error('test-error'));
});
json(method.url)
.expect(500)
.end(expectErrorResponseContaining({message: 'test-error'}, done));
});
it('should return 500 when "after" returns an error', function(done) {
var method = givenSharedStaticMethod();
objects.after(method.name, function(ctx, next) {
next(new Error('test-error'));
});
json(method.url)
.expect(500)
.end(expectErrorResponseContaining({message: 'test-error'}, done));
});
it('should return 400 when a required arg is missing', function(done) {
var method = givenSharedPrototypeMethod(
fu