// 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'; var loopback = require('../'); var lt = require('./helpers/loopback-testing-helper'); var path = require('path'); var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-integration-app'); var app = require(path.join(SIMPLE_APP, 'server/server.js')); var assert = require('assert'); var expect = require('./helpers/expect'); describe('remoting - integration', function() { lt.beforeEach.withApp(app); lt.beforeEach.givenModel('store'); afterEach(function(done) { this.app.models.store.destroyAll(done); }); describe('app.remotes.options', function() { it('should load remoting options', function() { var remotes = app.remotes(); assert.deepEqual(remotes.options, {'json': {'limit': '1kb', 'strict': false}, 'urlencoded': {'limit': '8kb', 'extended': true}, 'errorHandler': {'debug': true, log: false}}); }); it('rest handler', function() { var handler = app.handler('rest'); assert(handler); }); it('should accept request that has entity below 1kb', function(done) { // Build an object that is smaller than 1kb var name = ''; for (var i = 0; i < 256; i++) { name += '11'; } this.http = this.post('/api/stores'); this.http.send({ 'name': name, }); this.http.end(function(err) { if (err) return done(err); this.req = this.http.req; this.res = this.http.res; assert.equal(this.res.statusCode, 200); done(); }.bind(this)); }); it('should reject request that has entity beyond 1kb', function(done) { // Build an object that is larger than 1kb var name = ''; for (var i = 0; i < 2048; i++) { name += '11111111111'; } this.http = this.post('/api/stores'); this.http.send({ 'name': name, }); this.http.end(function(err) { if (err) return done(err); this.req = this.http.req; this.res = this.http.res; // Request is rejected with 413 assert.equal(this.res.statusCode, 413); done(); }.bind(this)); }); }); describe('Model shared classes', function() { it('has expected remote methods with default model.settings.replaceOnPUT' + 'set to true (3.x)', function() { var storeClass = findClass('store'); var methods = getFormattedMethodsExcludingRelations(storeClass.methods); var expectedMethods = [ 'create(data:object:store):store POST /stores', 'patchOrCreate(data:object:store):store PATCH /stores', 'replaceOrCreate(data:object:store):store PUT /stores', 'replaceOrCreate(data:object:store):store POST /stores/replaceOrCreate', 'exists(id:any):boolean GET /stores/:id/exists', 'findById(id:any,filter:object):store GET /stores/:id', 'replaceById(id:any,data:object:store):store PUT /stores/:id', 'replaceById(id:any,data:object:store):store POST /stores/:id/replace', 'find(filter:object):store GET /stores', 'findOne(filter:object):store GET /stores/findOne', 'updateAll(where:object,data:object:store):object POST /stores/update', 'deleteById(id:any):object DELETE /stores/:id', 'count(where:object):number GET /stores/count', 'prototype.patchAttributes(data:object:store):store PATCH /stores/:id', 'createChangeStream(options:object):ReadableStream POST /stores/change-stream', ]; // The list of methods is from docs: // http://loopback.io/doc/en/lb2/Exposing-models-over-REST.html expect(methods).to.include.members(expectedMethods); }); it('has expected remote methods for scopes', function() { var storeClass = findClass('store'); var methods = getFormattedScopeMethods(storeClass.methods); var expectedMethods = [ '__get__superStores(filter:object):store GET /stores/superStores', '__create__superStores(data:object:store):store POST /stores/superStores', '__delete__superStores() DELETE /stores/superStores', '__count__superStores(where:object):number GET /stores/superStores/count', ]; expect(methods).to.include.members(expectedMethods); }); it('should have correct signatures for belongsTo methods', function() { var widgetClass = findClass('widget'); var methods = getFormattedPrototypeMethods(widgetClass.methods); var expectedMethods = [ 'prototype.__get__store(refresh:boolean):store ' + 'GET /widgets/:id/store', ]; expect(methods).to.include.members(expectedMethods); }); it('should have correct signatures for hasMany methods', function() { var storeClass = findClass('store'); var methods = getFormattedPrototypeMethods(storeClass.methods); var expectedMethods = [ 'prototype.__findById__widgets(fk:any):widget ' + 'GET /stores/:id/widgets/:fk', 'prototype.__destroyById__widgets(fk:any) ' + 'DELETE /stores/:id/widgets/:fk', 'prototype.__updateById__widgets(fk:any,data:object:widget):widget ' + 'PUT /stores/:id/widgets/:fk', 'prototype.__get__widgets(filter:object):widget ' + 'GET /stores/:id/widgets', 'prototype.__create__widgets(data:object:widget):widget ' + 'POST /stores/:id/widgets', 'prototype.__delete__widgets() ' + 'DELETE /stores/:id/widgets', 'prototype.__count__widgets(where:object):number ' + 'GET /stores/:id/widgets/count', ]; expect(methods).to.include.members(expectedMethods); }); it('should have correct signatures for hasMany-through methods', function() { // jscs:disable validateIndentation var physicianClass = findClass('physician'); var methods = getFormattedPrototypeMethods(physicianClass.methods); var expectedMethods = [ 'prototype.__findById__patients(fk:any):patient ' + 'GET /physicians/:id/patients/:fk', 'prototype.__destroyById__patients(fk:any) ' + 'DELETE /physicians/:id/patients/:fk', 'prototype.__updateById__patients(fk:any,data:object:patient):patient ' + 'PUT /physicians/:id/patients/:fk', 'prototype.__link__patients(fk:any,data:object:appointment):appointment ' + 'PUT /physicians/:id/patients/rel/:fk', 'prototype.__unlink__patients(fk:any) ' + 'DELETE /physicians/:id/patients/rel/:fk', 'prototype.__exists__patients(fk:any):boolean ' + 'HEAD /physicians/:id/patients/rel/:fk', 'prototype.__get__patients(filter:object):patient ' + 'GET /physicians/:id/patients', 'prototype.__create__patients(data:object:patient):patient ' + 'POST /physicians/:id/patients', 'prototype.__delete__patients() ' + 'DELETE /physicians/:id/patients', 'prototype.__count__patients(where:object):number ' + 'GET /physicians/:id/patients/count', ]; expect(methods).to.include.members(expectedMethods); }); }); it('has upsertWithWhere remote method', function() { var storeClass = findClass('store'); var methods = getFormattedMethodsExcludingRelations(storeClass.methods); var expectedMethods = [ 'upsertWithWhere(where:object,data:object:store):store POST /stores/upsertWithWhere', ]; expect(methods).to.include.members(expectedMethods); }); describe('createOnlyInstance', function() { it('sets createOnlyInstance to true if id is generated and forceId is not set to false', function() { var storeClass = findClass('store'); var createMethod = getCreateMethod(storeClass.methods); assert(createMethod.accepts[0].createOnlyInstance === true); }); it('sets createOnlyInstance to false if forceId is set to false in the model', function() { var customerClass = findClass('customerforceidfalse'); var createMethod = getCreateMethod(customerClass.methods); assert(createMethod.accepts[0].createOnlyInstance === false); }); it('sets createOnlyInstance based on target model for scoped or related methods', function() { var userClass = findClass('user'); var createMethod = userClass.methods.find(function(m) { return (m.name === 'prototype.__create__accessTokens'); }); assert(createMethod.accepts[0].createOnlyInstance === false); }); }); }); describe('With model.settings.replaceOnPUT false', function() { lt.beforeEach.withApp(app); lt.beforeEach.givenModel('storeWithReplaceOnPUTfalse'); afterEach(function(done) { this.app.models.storeWithReplaceOnPUTfalse.destroyAll(done); }); it('should have expected remote methods', function() { var storeClass = findClass('storeWithReplaceOnPUTfalse'); var methods = getFormattedMethodsExcludingRelations(storeClass.methods); var expectedMethods = [ 'create(data:object:storeWithReplaceOnPUTfalse):storeWithReplaceOnPUTfalse POST /stores-updating', 'patchOrCreate(data:object:storeWithReplaceOnPUTfalse):storeWithReplaceOnPUTfalse PUT /stores-updating', 'patchOrCreate(data:object:storeWithReplaceOnPUTfalse):storeWithReplaceOnPUTfalse PATCH /stores-updating', 'replaceOrCreate(data:object:storeWithReplaceOnPUTfalse):storeWithReplaceOnPUTfalse POST /stores-updating/replaceOrCreate', 'upsertWithWhere(where:object,data:object:storeWithReplaceOnPUTfalse):storeWithReplaceOnPUTfalse POST /stores-updating/upsertWithWhere', 'exists(id:any):boolean GET /stores-updating/:id/exists', 'exists(id:any):boolean HEAD /stores-updating/:id', 'findById(id:any,filter:object):storeWithReplaceOnPUTfalse GET /stores-updating/:id', 'replaceById(id:any,data:object:storeWithReplaceOnPUTfalse):storeWithReplaceOnPUTfalse POST /stores-updating/:id/replace', 'find(filter:object):storeWithReplaceOnPUTfalse GET /stores-updating', 'findOne(filter:object):storeWithReplaceOnPUTfalse GET /stores-updating/findOne', 'updateAll(where:object,data:object:storeWithReplaceOnPUTfalse):object POST /stores-updating/update', 'deleteById(id:any):object DELETE /stores-updating/:id', 'count(where:object):number GET /stores-updating/count', 'prototype.patchAttributes(data:object:storeWithReplaceOnPUTfalse):storeWithReplaceOnPUTfalse PUT /stores-updating/:id', 'prototype.patchAttributes(data:object:storeWithReplaceOnPUTfalse):storeWithReplaceOnPUTfalse PATCH /stores-updating/:id', 'createChangeStream(options:object):ReadableStream POST /stores-updating/change-stream', 'createChangeStream(options:object):ReadableStream GET /stores-updating/change-stream', ]; expect(methods).to.eql(expectedMethods); }); }); describe('With model.settings.replaceOnPUT true', function() { lt.beforeEach.withApp(app); lt.beforeEach.givenModel('storeWithReplaceOnPUTtrue'); afterEach(function(done) { this.app.models.storeWithReplaceOnPUTtrue.destroyAll(done); }); it('should have expected remote methods', function() { var storeClass = findClass('storeWithReplaceOnPUTtrue'); var methods = getFormattedMethodsExcludingRelations(storeClass.methods); var expectedMethods = [ 'patchOrCreate(data:object:storeWithReplaceOnPUTtrue):storeWithReplaceOnPUTtrue PATCH /stores-replacing', 'replaceOrCreate(data:object:storeWithReplaceOnPUTtrue):storeWithReplaceOnPUTtrue POST /stores-replacing/replaceOrCreate', 'replaceOrCreate(data:object:storeWithReplaceOnPUTtrue):storeWithReplaceOnPUTtrue PUT /stores-replacing', 'replaceById(id:any,data:object:storeWithReplaceOnPUTtrue):storeWithReplaceOnPUTtrue POST /stores-replacing/:id/replace', 'replaceById(id:any,data:object:storeWithReplaceOnPUTtrue):storeWithReplaceOnPUTtrue PUT /stores-replacing/:id', 'prototype.patchAttributes(data:object:storeWithReplaceOnPUTtrue):storeWithReplaceOnPUTtrue PATCH /stores-replacing/:id', ]; expect(methods).to.include.members(expectedMethods); }); }); function formatReturns(m) { var returns = m.returns; if (!returns || returns.length === 0) { return ''; } var type = returns[0].type; // handle anonymous type definitions, e.g // { arg: 'info', type: { count: 'number' } } if (typeof type === 'object' && !Array.isArray(type)) type = 'object'; return type ? ':' + type : ''; } function formatMethod(m) { var arr = []; var endpoints = m.getEndpoints(); for (var i = 0; i < endpoints.length; i++) { arr.push([ m.name, '(', m.accepts.filter(function(a) { return !(a.http && typeof a.http === 'function'); }).map(function(a) { return a.arg + ':' + a.type + (a.model ? ':' + a.model : ''); }).join(','), ')', formatReturns(m), ' ', endpoints[i].verb, ' ', endpoints[i].fullPath, ].join('')); } return arr; } function findClass(name) { return app.handler('rest').adapter .getClasses() .filter(function(c) { return c.name === name; })[0]; } function getFormattedMethodsExcludingRelations(methods) { return methods.filter(function(m) { return m.name.indexOf('__') === -1; }) .map(function(m) { return formatMethod(m); }) .reduce(function(p, c) { return p.concat(c); }); } function getCreateMethod(methods) { return methods.find(function(m) { return (m.name === 'create'); }); } function getFormattedScopeMethods(methods) { return methods.filter(function(m) { return m.name.indexOf('__') === 0; }) .map(function(m) { return formatMethod(m); }) .reduce(function(p, c) { return p.concat(c); }); } function getFormattedPrototypeMethods(methods) { return methods.filter(function(m) { return m.name.indexOf('prototype.__') === 0; }) .map(function(m) { return formatMethod(m); }) .reduce(function(p, c) { return p.concat(c); }); }