loopback-connector/test/transaction.test.js

309 lines
9.6 KiB
JavaScript
Raw Normal View History

2020-02-10 18:52:06 +00:00
// Copyright IBM Corp. 2015,2020. All Rights Reserved.
2016-05-06 04:50:59 +00:00
// Node module: loopback-connector
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
2017-03-06 23:40:47 +00:00
'use strict';
const Transaction = require('../index').Transaction;
2015-05-15 17:27:08 +00:00
const chai = require('chai');
chai.use(require('chai-as-promised'));
const {expect} = chai;
const chaiAsPromised = require('chai-as-promised');
const testConnector = require('./connectors/test-sql-connector');
2015-05-15 17:27:08 +00:00
const juggler = require('loopback-datasource-juggler');
let db, Post, Review;
2015-05-15 17:27:08 +00:00
describe('transactions', function() {
before(function(done) {
db = new juggler.DataSource({
connector: testConnector,
2016-04-09 18:35:52 +00:00
debug: true,
2015-05-15 17:27:08 +00:00
});
db.once('connected', function() {
Post = db.define('PostTX', {
2017-03-06 23:40:47 +00:00
title: {type: String, length: 255, index: true},
content: {type: String},
2015-05-15 17:27:08 +00:00
});
Review = db.define('ReviewTX', {
author: String,
2017-03-06 23:40:47 +00:00
content: {type: String},
});
2017-03-06 23:40:47 +00:00
Post.hasMany(Review, {as: 'reviews', foreignKey: 'postId'});
2015-05-15 17:27:08 +00:00
done();
});
});
let currentTx;
let hooks = [];
2015-05-15 17:27:08 +00:00
// Return an async function to start a transaction and create a post
function createPostInTx(post, timeout) {
return function(done) {
// Transaction.begin(db.connector, Transaction.READ_COMMITTED,
Post.beginTransaction({
2016-04-09 18:35:52 +00:00
isolationLevel: Transaction.READ_COMMITTED,
timeout: timeout,
},
2018-06-12 14:33:52 +00:00
function(err, tx) {
if (err) return done(err);
expect(typeof tx.id).to.eql('string');
hooks = [];
tx.observe('before commit', function(context, next) {
hooks.push('before commit');
next();
});
tx.observe('after commit', function(context, next) {
hooks.push('after commit');
next();
2015-05-15 17:27:08 +00:00
});
2018-06-12 14:33:52 +00:00
tx.observe('before rollback', function(context, next) {
hooks.push('before rollback');
next();
});
tx.observe('after rollback', function(context, next) {
hooks.push('after rollback');
next();
});
currentTx = tx;
Post.create(post, {transaction: tx, model: 'Post'},
function(err, p) {
if (err) {
done(err);
} else {
p.reviews.create({
author: 'John',
content: 'Review for ' + p.title,
}, {transaction: tx, model: 'Review'},
function(err, c) {
done(err);
});
}
});
});
2015-05-15 17:27:08 +00:00
};
}
// Return an async function to find matching posts and assert number of
// records to equal to the count
function expectToFindPosts(where, count, inTx) {
return function(done) {
const options = {model: 'Post'};
2015-05-15 17:27:08 +00:00
if (inTx) {
options.transaction = currentTx;
}
2017-03-06 23:40:47 +00:00
Post.find({where: where}, options,
2015-05-15 17:27:08 +00:00
function(err, posts) {
if (err) return done(err);
expect(posts.length).to.be.eql(count);
// Make sure both find() and count() behave the same way
Post.count(where, options,
function(err, result) {
if (err) return done(err);
expect(result).to.be.eql(count);
if (count) {
// Find related reviews
options.model = 'Review';
// Please note the empty {} is required, otherwise, the options
// will be treated as a filter
posts[0].reviews({}, options, function(err, reviews) {
if (err) return done(err);
expect(reviews.length).to.be.eql(count);
done();
});
} else {
done();
}
});
2015-05-15 17:27:08 +00:00
});
};
}
describe('commit', function() {
const post = {title: 't1', content: 'c1'};
2015-05-15 17:27:08 +00:00
before(createPostInTx(post));
it('should not see the uncommitted insert', expectToFindPosts(post, 0));
it('should see the uncommitted insert from the same transaction',
expectToFindPosts(post, 1, true));
it('should commit a transaction', function(done) {
currentTx.commit(function(err) {
expect(hooks).to.eql(['before commit', 'after commit']);
done(err);
});
});
it('should see the committed insert', expectToFindPosts(post, 1));
it('should report error if the transaction is not active', function(done) {
currentTx.commit(function(err) {
expect(err).to.be.instanceof(Error);
done();
});
});
});
describe('rollback', function() {
before(function() {
// Reset the collection
db.connector.data = {};
2015-05-15 17:27:08 +00:00
});
const post = {title: 't2', content: 'c2'};
2015-05-15 17:27:08 +00:00
before(createPostInTx(post));
it('should not see the uncommitted insert', expectToFindPosts(post, 0));
it('should see the uncommitted insert from the same transaction',
expectToFindPosts(post, 1, true));
it('should rollback a transaction', function(done) {
currentTx.rollback(function(err) {
expect(hooks).to.eql(['before rollback', 'after rollback']);
done(err);
});
});
it('should not see the rolledback insert', expectToFindPosts(post, 0));
it('should report error if the transaction is not active', function(done) {
currentTx.rollback(function(err) {
expect(err).to.be.instanceof(Error);
done();
});
});
});
describe('timeout', function() {
2018-06-12 14:44:24 +00:00
const TIMEOUT = 50;
2015-05-15 17:27:08 +00:00
before(function() {
// Reset the collection
db.connector.data = {};
2015-05-15 17:27:08 +00:00
});
const post = {title: 't3', content: 'c3'};
2018-06-12 14:44:24 +00:00
beforeEach(createPostInTx(post, TIMEOUT));
2015-05-15 17:27:08 +00:00
it('should report timeout', function(done) {
2018-06-12 14:44:24 +00:00
// wait until the "create post" transaction times out
setTimeout(runTheTest, TIMEOUT * 3);
function runTheTest() {
2017-03-06 23:40:47 +00:00
Post.find({where: {title: 't3'}}, {transaction: currentTx},
2015-05-15 17:27:08 +00:00
function(err, posts) {
2018-06-12 14:44:24 +00:00
expect(err).to.match(/transaction.*not active/);
2015-05-15 17:27:08 +00:00
done();
});
2018-06-12 14:44:24 +00:00
}
2015-05-15 17:27:08 +00:00
});
it('should invoke the timeout hook', function(done) {
currentTx.observe('timeout', function(context, next) {
next();
done();
});
2018-06-12 14:44:24 +00:00
// If the event is not fired quickly enough, then the test can
// quickly fail - no need to wait full two seconds (Mocha's default)
this.timeout(TIMEOUT * 3);
2015-05-15 17:27:08 +00:00
});
});
2017-07-14 02:35:01 +00:00
2020-01-21 16:06:46 +00:00
describe('isActive', function() {
it('returns true when connection is active', function(done) {
Post.beginTransaction({
isolationLevel: Transaction.READ_COMMITTED,
timeout: 1000,
},
function(err, tx) {
if (err) return done(err);
expect(tx.isActive()).to.equal(true);
return done();
});
});
it('returns false when connection is not active', function(done) {
Post.beginTransaction({
isolationLevel: Transaction.READ_COMMITTED,
timeout: 1000,
},
function(err, tx) {
if (err) return done(err);
delete tx.connection;
expect(tx.isActive()).to.equal(false);
return done();
});
});
});
2017-07-14 02:35:01 +00:00
describe('transaction instance', function() {
function TestTransaction(connector, connection) {
this.connector = connector;
this.connection = connection;
}
Object.assign(TestTransaction.prototype, Transaction.prototype);
TestTransaction.prototype.foo = true;
function beginTransaction(isolationLevel, cb) {
return cb(null, new TestTransaction(testConnector, {}));
}
2017-07-14 02:35:01 +00:00
it('should do nothing when transaction is like a Transaction', function(done) {
testConnector.initialize(db, function(err, resultConnector) {
resultConnector.beginTransaction = beginTransaction;
Transaction.begin(resultConnector, Transaction.READ_COMMITTED,
function(err, result) {
if (err) done(err);
expect(result).to.be.instanceof(TestTransaction);
expect(result.foo).to.equal(true);
done();
});
});
});
it('should create new instance when transaction is not like a Transaction',
function(done) {
testConnector.initialize(db, function(err, resultConnector) {
resultConnector.beginTransaction = beginTransaction;
delete TestTransaction.prototype.commit;
Transaction.begin(resultConnector, Transaction.READ_COMMITTED,
function(err, result) {
if (err) done(err);
expect(result).to.not.be.instanceof(TestTransaction);
expect(result).to.be.instanceof(Transaction);
expect(result.foo).to.equal(undefined);
done();
});
});
});
});
it('can return promise for commit', function() {
const connectorObject = {};
connectorObject.commit = function(connection, cb) {
return cb(null, 'committed');
};
const transactionInstance = new Transaction(connectorObject, {});
return expect(transactionInstance.commit()).to.eventually.equal('committed');
});
it('can return promise for rollback', function() {
const connectorObject = {};
connectorObject.rollback = function(connection, cb) {
return cb(null, 'rolledback');
};
const transactionInstance = new Transaction(connectorObject, {});
return expect(transactionInstance.rollback()).to.eventually.equal('rolledback');
});
it('can return promise for begin', function() {
const connectorObject = {};
connectorObject.beginTransaction = function(connection, cb) {
return cb(null, 'begun');
};
return expect(
Transaction.begin(connectorObject, ''),
).to.eventually.be.instanceOf(Transaction);
});
2015-05-15 17:27:08 +00:00
});