418 lines
13 KiB
JavaScript
418 lines
13 KiB
JavaScript
// Copyright IBM Corp. 2014,2018. All Rights Reserved.
|
|
// Node module: loopback
|
|
// This file is licensed under the MIT License.
|
|
// License text available at https://opensource.org/licenses/MIT
|
|
|
|
'use strict';
|
|
const assert = require('assert');
|
|
const expect = require('./helpers/expect');
|
|
const loopback = require('../');
|
|
const path = require('path');
|
|
const request = require('supertest');
|
|
|
|
describe('loopback.rest', function() {
|
|
this.timeout(10000);
|
|
let app, MyModel;
|
|
|
|
beforeEach(function() {
|
|
// override the global app object provided by test/support.js
|
|
// and create a local one that does not share state with other tests
|
|
app = loopback({localRegistry: true, loadBuiltinModels: true});
|
|
app.set('remoting', {errorHandler: {debug: true, log: false}});
|
|
const db = app.dataSource('db', {connector: 'memory'});
|
|
MyModel = app.registry.createModel('MyModel');
|
|
MyModel.attachTo(db);
|
|
});
|
|
|
|
it('works out-of-the-box', function(done) {
|
|
app.model(MyModel);
|
|
app.use(loopback.rest());
|
|
request(app).get('/mymodels')
|
|
.expect(200)
|
|
.end(done);
|
|
});
|
|
|
|
it('should report 200 for DELETE /:id found', function(done) {
|
|
app.model(MyModel);
|
|
app.use(loopback.rest());
|
|
MyModel.create({name: 'm1'}, function(err, inst) {
|
|
request(app)
|
|
.del('/mymodels/' + inst.id)
|
|
.expect(200, function(err, res) {
|
|
expect(res.body.count).to.equal(1);
|
|
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should report 404 for GET /:id not found', function(done) {
|
|
app.model(MyModel);
|
|
app.use(loopback.rest());
|
|
request(app).get('/mymodels/1')
|
|
.expect(404)
|
|
.end(function(err, res) {
|
|
if (err) return done(err);
|
|
|
|
const errorResponse = res.body.error;
|
|
assert(errorResponse);
|
|
assert.equal(errorResponse.code, 'MODEL_NOT_FOUND');
|
|
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should report 404 for HEAD /:id not found', function(done) {
|
|
app.model(MyModel);
|
|
app.use(loopback.rest());
|
|
request(app).head('/mymodels/1')
|
|
.expect(404)
|
|
.end(done);
|
|
});
|
|
|
|
it('should report 200 for GET /:id/exists not found', function(done) {
|
|
app.model(MyModel);
|
|
app.use(loopback.rest());
|
|
request(app).get('/mymodels/1/exists')
|
|
.expect(200)
|
|
.end(function(err, res) {
|
|
if (err) return done(err);
|
|
|
|
expect(res.body).to.eql({exists: false});
|
|
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should report 200 for GET /:id found', function(done) {
|
|
app.model(MyModel);
|
|
app.use(loopback.rest());
|
|
MyModel.create({name: 'm1'}, function(err, inst) {
|
|
request(app).get('/mymodels/' + inst.id)
|
|
.expect(200)
|
|
.end(done);
|
|
});
|
|
});
|
|
|
|
it('should report 200 for HEAD /:id found', function(done) {
|
|
app.model(MyModel);
|
|
app.use(loopback.rest());
|
|
MyModel.create({name: 'm2'}, function(err, inst) {
|
|
request(app).head('/mymodels/' + inst.id)
|
|
.expect(200)
|
|
.end(done);
|
|
});
|
|
});
|
|
|
|
it('should report 200 for GET /:id/exists found', function(done) {
|
|
app.model(MyModel);
|
|
app.use(loopback.rest());
|
|
MyModel.create({name: 'm2'}, function(err, inst) {
|
|
request(app).get('/mymodels/' + inst.id + '/exists')
|
|
.expect(200)
|
|
.end(function(err, res) {
|
|
if (err) return done(err);
|
|
|
|
expect(res.body).to.eql({exists: true});
|
|
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should honour `remoting.rest.supportedTypes`', function(done) {
|
|
const app = loopback({localRegistry: true});
|
|
|
|
// NOTE it is crucial to set `remoting` before creating any models
|
|
const supportedTypes = ['json', 'application/javascript', 'text/javascript'];
|
|
app.set('remoting', {rest: {supportedTypes: supportedTypes}});
|
|
|
|
app.model(MyModel);
|
|
app.use(loopback.rest());
|
|
|
|
request(app).get('/mymodels')
|
|
.set('Accept', 'text/html,application/xml;q= 0.9,*/*;q= 0.8')
|
|
.expect('Content-Type', 'application/json; charset=utf-8')
|
|
.expect(200, done);
|
|
});
|
|
|
|
it('allows models to provide a custom HTTP path', function(done) {
|
|
const CustomModel = app.registry.createModel('CustomModel',
|
|
{name: String},
|
|
{http: {'path': 'domain1/CustomModelPath'}});
|
|
|
|
app.model(CustomModel, {dataSource: 'db'});
|
|
app.use(loopback.rest());
|
|
|
|
request(app).get('/domain1/CustomModelPath').expect(200).end(done);
|
|
});
|
|
|
|
it('should report 200 for url-encoded HTTP path', function(done) {
|
|
const CustomModel = app.registry.createModel('CustomModel',
|
|
{name: String},
|
|
{http: {path: 'domain%20one/CustomModelPath'}});
|
|
|
|
app.model(CustomModel, {dataSource: 'db'});
|
|
app.use(loopback.rest());
|
|
|
|
request(app).get('/domain%20one/CustomModelPath').expect(200).end(done);
|
|
});
|
|
|
|
it('includes loopback.token when necessary', function(done) {
|
|
givenUserModelWithAuth();
|
|
app.enableAuth({dataSource: 'db'});
|
|
app.use(loopback.rest());
|
|
|
|
givenLoggedInUser(function(err, token) {
|
|
if (err) return done(err);
|
|
expect(token).instanceOf(app.models.AccessToken);
|
|
request(app).get('/users/' + token.userId)
|
|
.set('Authorization', token.id)
|
|
.expect(200)
|
|
.end(done);
|
|
}, done);
|
|
});
|
|
|
|
it('does not include loopback.token when auth not enabled', function(done) {
|
|
const User = givenUserModelWithAuth();
|
|
User.getToken = function(req, cb) {
|
|
cb(null, req.accessToken ? req.accessToken.id : null);
|
|
};
|
|
loopback.remoteMethod(User.getToken, {
|
|
accepts: [{type: 'object', http: {source: 'req'}}],
|
|
returns: [{type: 'object', name: 'id'}],
|
|
});
|
|
|
|
app.use(loopback.rest());
|
|
givenLoggedInUser(function(err, token) {
|
|
if (err) return done(err);
|
|
|
|
request(app).get('/users/getToken')
|
|
.set('Authorization', token.id)
|
|
.expect(200)
|
|
.end(function(err, res) {
|
|
if (err) return done(err);
|
|
|
|
expect(res.body.id).to.equal(null);
|
|
|
|
done();
|
|
});
|
|
}, done);
|
|
});
|
|
|
|
it('rebuilds REST endpoints after a model was added', () => {
|
|
app.use(loopback.rest());
|
|
|
|
return request(app).get('/mymodels').expect(404).then(() => {
|
|
app.model(MyModel);
|
|
|
|
return request(app).get('/mymodels').expect(200);
|
|
});
|
|
});
|
|
|
|
it('rebuilds REST endpoints after a model was deleted', () => {
|
|
app.model(MyModel);
|
|
app.use(loopback.rest());
|
|
|
|
return request(app).get('/mymodels').expect(200)
|
|
.then(() => {
|
|
app.deleteModelByName('MyModel');
|
|
|
|
return request(app).get('/mymodels').expect(404);
|
|
});
|
|
});
|
|
|
|
it('rebuilds REST endpoints after a remoteMethod was added', () => {
|
|
app.model(MyModel);
|
|
app.use(loopback.rest());
|
|
|
|
return request(app).get('/mymodels/customMethod').expect(404)
|
|
.then(() => {
|
|
MyModel.customMethod = function(req, cb) {
|
|
cb(null, true);
|
|
};
|
|
MyModel.remoteMethod('customMethod', {
|
|
http: {verb: 'get'},
|
|
accepts: [{type: 'object', http: {source: 'req'}}],
|
|
returns: [{type: 'boolean', name: 'success'}],
|
|
});
|
|
|
|
return request(app).get('/mymodels/customMethod').expect(200);
|
|
});
|
|
});
|
|
|
|
it('rebuilds REST endpoints after a remoteMethod was disabled', () => {
|
|
app.model(MyModel);
|
|
app.use(loopback.rest());
|
|
MyModel.customMethod = function(req, cb) {
|
|
cb(null, true);
|
|
};
|
|
MyModel.remoteMethod('customMethod', {
|
|
http: {verb: 'get'},
|
|
accepts: [{type: 'object', http: {source: 'req'}}],
|
|
returns: [{type: 'boolean', name: 'success'}],
|
|
});
|
|
return request(app).get('/mymodels/customMethod').expect(200)
|
|
.then(() => {
|
|
MyModel.disableRemoteMethodByName('customMethod');
|
|
|
|
return request(app).get('/mymodels/customMethod').expect(404);
|
|
});
|
|
});
|
|
|
|
function givenUserModelWithAuth() {
|
|
const AccessToken = app.registry.getModel('AccessToken');
|
|
app.model(AccessToken, {dataSource: 'db'});
|
|
const User = app.registry.getModel('User');
|
|
// Speed up the password hashing algorithm for tests
|
|
User.settings.saltWorkFactor = 4;
|
|
app.model(User, {dataSource: 'db'});
|
|
|
|
// NOTE(bajtos) This is puzzling to me. The built-in User & AccessToken
|
|
// models should come with both relations already set up, i.e. the
|
|
// following two lines should not be neccessary.
|
|
// And it does behave that way when only tests in this file are run.
|
|
// However, when I run the full test suite (all files), the relations
|
|
// get broken.
|
|
AccessToken.belongsTo(User, {as: 'user', foreignKey: 'userId'});
|
|
User.hasMany(AccessToken, {as: 'accessTokens', foreignKey: 'userId'});
|
|
|
|
return User;
|
|
}
|
|
|
|
function givenLoggedInUser(cb, done) {
|
|
const credentials = {email: 'user@example.com', password: 'pwd'};
|
|
const User = app.models.User;
|
|
User.create(credentials,
|
|
function(err, user) {
|
|
if (err) return done(err);
|
|
|
|
User.login(credentials, cb);
|
|
});
|
|
}
|
|
|
|
describe('shared methods', function() {
|
|
function getFixturePath(dirName) {
|
|
return path.join(__dirname, 'fixtures/shared-methods/' + dirName +
|
|
'/server/server.js');
|
|
}
|
|
|
|
describe('with specific definitions in model-config.json', function() {
|
|
it('should not be exposed when the definition value is false',
|
|
function(done) {
|
|
const app = require(getFixturePath('model-config-defined-false'));
|
|
request(app)
|
|
.get('/todos')
|
|
.expect(404, done);
|
|
});
|
|
|
|
it('should be exposed when the definition value is true', function(done) {
|
|
const app = require(getFixturePath('model-config-defined-true'));
|
|
request(app)
|
|
.get('/todos')
|
|
.expect(200, done);
|
|
});
|
|
});
|
|
|
|
describe('with default definitions in model-config.json', function() {
|
|
it('should not be exposed when the definition value is false',
|
|
function(done) {
|
|
const app = require(getFixturePath('model-config-default-false'));
|
|
request(app)
|
|
.get('/todos')
|
|
.expect(404, done);
|
|
});
|
|
|
|
it('should be exposed when the definition value is true', function(done) {
|
|
const app = require(getFixturePath('model-config-default-true'));
|
|
app.models.Todo.create([
|
|
{content: 'a'},
|
|
{content: 'b'},
|
|
{content: 'c'},
|
|
], function() {
|
|
request(app)
|
|
.del('/todos')
|
|
.expect(200)
|
|
.end(function(err, res) {
|
|
if (err) return done(err);
|
|
|
|
expect(res.body.count).to.equal(3);
|
|
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('with specific definitions in config.json', function() {
|
|
it('should not be exposed when the definition value is false',
|
|
function(done) {
|
|
const app = require(getFixturePath('config-defined-false'));
|
|
request(app)
|
|
.get('/todos')
|
|
.expect(404, done);
|
|
});
|
|
|
|
it('should be exposed when the definition value is true',
|
|
function(done) {
|
|
const app = require(getFixturePath('config-defined-true'));
|
|
request(app)
|
|
.get('/todos')
|
|
.expect(200, done);
|
|
});
|
|
});
|
|
|
|
describe('with default definitions in config.json', function() {
|
|
it('should not be exposed when the definition value is false',
|
|
function(done) {
|
|
const app = require(getFixturePath('config-default-false'));
|
|
request(app)
|
|
.get('/todos')
|
|
.expect(404, done);
|
|
});
|
|
|
|
it('should be exposed when the definition value is true', function(done) {
|
|
const app = require(getFixturePath('config-default-true'));
|
|
app.models.Todo.create([
|
|
{content: 'a'},
|
|
{content: 'b'},
|
|
{content: 'c'},
|
|
], function() {
|
|
request(app)
|
|
.del('/todos')
|
|
.expect(200)
|
|
.end(function(err, res) {
|
|
if (err) return done(err);
|
|
|
|
expect(res.body.count).to.equal(3);
|
|
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// The fixture in `shared-method/both-configs-set/config.json` has `*:false`
|
|
// set which disables the REST endpoints for built-in models such as User as
|
|
// a side effect since tests share the same loopback instance. As a
|
|
// consequence, this causes the tests in user.integration to fail.
|
|
describe.skip('with definitions in both config.json and model-config.json',
|
|
function() {
|
|
it('should prioritize the settings in model-config.json', function(done) {
|
|
const app = require(getFixturePath('both-configs-set'));
|
|
request(app)
|
|
.del('/todos')
|
|
.expect(404, done);
|
|
});
|
|
|
|
it('should fall back to config.json settings if setting is not found in' +
|
|
'model-config.json', function(done) {
|
|
const app = require(getFixturePath('both-configs-set'));
|
|
request(app)
|
|
.get('/todos')
|
|
.expect(404, done);
|
|
});
|
|
});
|
|
});
|
|
});
|