loopback-context/test/main.test.js

250 lines
7.7 KiB
JavaScript

// Copyright IBM Corp. 2016,2018. All Rights Reserved.
// Node module: loopback-context
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var asyncV152 = require('async-1.5.2');
var whenV377 = require('when-3.7.7');
var LoopBackContext = require('..');
var Domain = require('domain');
var EventEmitter = require('events').EventEmitter;
var expect = require('./helpers/expect');
var loopback = require('loopback');
var request = require('supertest');
describe('LoopBack Context', function() {
var runInOtherDomain, runnerInterval;
before(function setupRunInOtherDomain() {
var emitterInOtherDomain = new EventEmitter();
Domain.create().add(emitterInOtherDomain);
runInOtherDomain = function(fn) {
emitterInOtherDomain.once('run', fn);
};
runnerInterval = setInterval(function() {
emitterInOtherDomain.emit('run');
}, 10);
});
after(function tearDownRunInOtherDomain() {
clearInterval(runnerInterval);
});
// See the following two items for more details:
// https://github.com/strongloop/loopback/issues/809
// https://github.com/strongloop/loopback/pull/337#issuecomment-61680577
it('preserves callback domain', function(done) {
var app = loopback({localRegistry: true, loadBuiltinModels: true});
app.set('remoting', {context: false});
app.set('legacyExplorer', false);
app.use(LoopBackContext.perRequest());
app.use(loopback.rest());
app.dataSource('db', {connector: 'memory'});
var TestModel = loopback.createModel({name: 'TestModel'});
app.model(TestModel, {dataSource: 'db', public: true});
// function for remote method
TestModel.test = function(inst, cb) {
var tmpCtx = LoopBackContext.getCurrentContext();
if (tmpCtx) tmpCtx.set('data', 'a value stored in context');
if (process.domain) cb = process.domain.bind(cb); // IMPORTANT
runInOtherDomain(cb);
};
// remote method
TestModel.remoteMethod('test', {
accepts: {arg: 'inst', type: 'TestModel'},
returns: {root: true},
http: {path: '/test', verb: 'get'},
});
// after remote hook
TestModel.afterRemote('**', function(ctxx, inst, next) {
var tmpCtx = LoopBackContext.getCurrentContext();
if (tmpCtx) {
ctxx.result.data = tmpCtx.get('data');
} else {
ctxx.result.data = 'context not available';
}
next();
});
request(app)
.get('/TestModels/test')
.end(function(err, res) {
if (err) return done(err);
expect(res.body.data).to.equal('a value stored in context');
done();
});
});
it('works outside REST middleware', function(done) {
LoopBackContext.runInContext(function() {
var ctx = LoopBackContext.getCurrentContext();
expect(ctx).is.an('object');
ctx.set('test-key', 'test-value');
process.nextTick(function() {
var ctx = LoopBackContext.getCurrentContext();
expect(ctx).is.an('object');
expect(ctx.get('test-key')).to.equal('test-value');
done();
});
});
});
// Credits for the original idea for this test case to @marlonkjoseph
// Original source of the POC gist of the idea:
// https://gist.github.com/marlonkjoseph/f42f3c71f746896a0d4b7279a34ea753
// Heavily edited by others
it('keeps context when using waterfall() from async 1.5.2',
function(done) {
LoopBackContext.runInContext(function() {
// Trigger async waterfall callbacks
asyncV152.waterfall([
function pushToContext(next) {
var ctx = LoopBackContext.getCurrentContext();
expect(ctx).is.an('object');
ctx.set('test-key', 'test-value');
next();
},
function pullFromContext(next) {
var ctx = LoopBackContext.getCurrentContext();
expect(ctx).is.an('object');
var testValue = ctx && ctx.get('test-key', 'test-value');
next(null, testValue);
},
function verify(testValue, next) {
expect(testValue).to.equal('test-value');
next();
},
], done);
});
});
it('handles concurrent then() calls with when v3.7.7 promises & bind option',
function() {
return Promise.all([
runWithPushedValue('test-value-1', {bind: true}),
runWithPushedValue('test-value-2', {bind: true}),
])
.then(function verify(values) {
var failureCount = getFailureCount(values);
expect(failureCount).to.equal(0);
});
});
it('fails once without bind option and when v3.7.7 promises',
function() {
return Promise.all([
runWithPushedValue('test-value-3'),
runWithPushedValue('test-value-4'),
])
.then(function verify(values) {
var failureCount = getFailureCount(values);
expect(failureCount).to.equal(1);
});
});
var timeout = 100;
function runWithPushedValue(pushedValue, options) {
return new Promise(function concurrentExecutor(outerResolve, reject) {
LoopBackContext.runInContext(function pushToContext() {
var ctx = LoopBackContext.getCurrentContext(options);
expect(ctx).is.an('object');
ctx.set('test-key', pushedValue);
var whenPromise = whenV377().delay(timeout);
whenPromise.then(function pullFromContextAndReturn() {
var pulledValue = ctx && ctx.get('test-key');
outerResolve({
pulledValue: pulledValue,
pushedValue: pushedValue,
});
}).catch(reject);
});
});
}
function getFailureCount(values) {
var failureCount = 0;
values.forEach(function(v) {
if (v.pulledValue !== v.pushedValue) {
failureCount++;
}
});
return failureCount;
}
it('doesn\'t mix up req\'s in chains of ' +
'Express-middleware-like func\'s if next() cb is bound',
function() {
return Promise.all([
runWithRequestId('test-value-5', true),
runWithRequestId('test-value-6', true),
])
.then(function verify(values) {
var failureCount = getFailureCount(values);
expect(failureCount).to.equal(0);
});
});
it('fails & mixes up ctx among requests in mw chains if next() cb is unbound',
function() {
return Promise.all([
runWithRequestId('test-value-7'),
runWithRequestId('test-value-8'),
])
.then(function verify(values) {
var failureCount = getFailureCount(values);
expect(failureCount).to.equal(1);
});
});
function runWithRequestId(pushedValue, bindNextCb) {
return new Promise(function chainExecutor(outerResolve, reject) {
LoopBackContext.runInContext(function concurrentChain() {
function middlewareBreakingCls(req, res, next) {
var ctx = LoopBackContext.getCurrentContext({bind: true});
if (bindNextCb) {
next = ctx.bind(next);
}
ctx.set('test-key', pushedValue);
var whenPromise = whenV377().delay(timeout);
whenPromise.then(next).catch(reject);
};
function middlewareReadingContext(req, res, next) {
var ctx = LoopBackContext.getCurrentContext({bind: true});
var pulledValue = ctx && ctx.get('test-key');
next(null, pulledValue);
};
// Run the chain
var req = null;
var res = null;
middlewareBreakingCls(req, res, function(err) {
if (err) return reject(err);
middlewareReadingContext(req, res, function(err, result) {
if (err) return reject(err);
outerResolve({
pulledValue: result,
pushedValue: pushedValue,
});
});
});
});
});
}
});