UNPKG

9.84 kBJavaScriptView Raw
1// Copyright IBM Corp. 2015,2020. All Rights Reserved.
2// Node module: loopback-connector
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6'use strict';
7const Transaction = require('../index').Transaction;
8
9const chai = require('chai');
10chai.use(require('chai-as-promised'));
11const {expect} = chai;
12const chaiAsPromised = require('chai-as-promised');
13const testConnector = require('./connectors/test-sql-connector');
14
15const juggler = require('loopback-datasource-juggler');
16let db, Post, Review;
17describe('transactions', function() {
18 before(function(done) {
19 db = new juggler.DataSource({
20 connector: testConnector,
21 debug: true,
22 });
23 db.once('connected', function() {
24 Post = db.define('PostTX', {
25 title: {type: String, length: 255, index: true},
26 content: {type: String},
27 });
28 Review = db.define('ReviewTX', {
29 author: String,
30 content: {type: String},
31 });
32 Post.hasMany(Review, {as: 'reviews', foreignKey: 'postId'});
33 done();
34 });
35 });
36
37 let currentTx;
38 let hooks = [];
39 // Return an async function to start a transaction and create a post
40 function createPostInTx(post, timeout) {
41 return function(done) {
42 // Transaction.begin(db.connector, Transaction.READ_COMMITTED,
43 Post.beginTransaction({
44 isolationLevel: Transaction.READ_COMMITTED,
45 timeout: timeout,
46 },
47 function(err, tx) {
48 if (err) return done(err);
49 expect(typeof tx.id).to.eql('string');
50 hooks = [];
51 tx.observe('before commit', function(context, next) {
52 hooks.push('before commit');
53 next();
54 });
55 tx.observe('after commit', function(context, next) {
56 hooks.push('after commit');
57 next();
58 });
59 tx.observe('before rollback', function(context, next) {
60 hooks.push('before rollback');
61 next();
62 });
63 tx.observe('after rollback', function(context, next) {
64 hooks.push('after rollback');
65 next();
66 });
67 currentTx = tx;
68 Post.create(post, {transaction: tx, model: 'Post'},
69 function(err, p) {
70 if (err) {
71 done(err);
72 } else {
73 p.reviews.create({
74 author: 'John',
75 content: 'Review for ' + p.title,
76 }, {transaction: tx, model: 'Review'},
77 function(err, c) {
78 done(err);
79 });
80 }
81 });
82 });
83 };
84 }
85
86 // Return an async function to find matching posts and assert number of
87 // records to equal to the count
88 function expectToFindPosts(where, count, inTx) {
89 return function(done) {
90 const options = {model: 'Post'};
91 if (inTx) {
92 options.transaction = currentTx;
93 }
94 Post.find({where: where}, options,
95 function(err, posts) {
96 if (err) return done(err);
97 expect(posts.length).to.be.eql(count);
98 // Make sure both find() and count() behave the same way
99 Post.count(where, options,
100 function(err, result) {
101 if (err) return done(err);
102 expect(result).to.be.eql(count);
103 if (count) {
104 // Find related reviews
105 options.model = 'Review';
106 // Please note the empty {} is required, otherwise, the options
107 // will be treated as a filter
108 posts[0].reviews({}, options, function(err, reviews) {
109 if (err) return done(err);
110 expect(reviews.length).to.be.eql(count);
111 done();
112 });
113 } else {
114 done();
115 }
116 });
117 });
118 };
119 }
120
121 describe('commit', function() {
122 const post = {title: 't1', content: 'c1'};
123 before(createPostInTx(post));
124
125 it('should not see the uncommitted insert', expectToFindPosts(post, 0));
126
127 it('should see the uncommitted insert from the same transaction',
128 expectToFindPosts(post, 1, true));
129
130 it('should commit a transaction', function(done) {
131 currentTx.commit(function(err) {
132 expect(hooks).to.eql(['before commit', 'after commit']);
133 done(err);
134 });
135 });
136
137 it('should see the committed insert', expectToFindPosts(post, 1));
138
139 it('should report error if the transaction is not active', function(done) {
140 currentTx.commit(function(err) {
141 expect(err).to.be.instanceof(Error);
142 done();
143 });
144 });
145 });
146
147 describe('rollback', function() {
148 before(function() {
149 // Reset the collection
150 db.connector.data = {};
151 });
152
153 const post = {title: 't2', content: 'c2'};
154 before(createPostInTx(post));
155
156 it('should not see the uncommitted insert', expectToFindPosts(post, 0));
157
158 it('should see the uncommitted insert from the same transaction',
159 expectToFindPosts(post, 1, true));
160
161 it('should rollback a transaction', function(done) {
162 currentTx.rollback(function(err) {
163 expect(hooks).to.eql(['before rollback', 'after rollback']);
164 done(err);
165 });
166 });
167
168 it('should not see the rolledback insert', expectToFindPosts(post, 0));
169
170 it('should report error if the transaction is not active', function(done) {
171 currentTx.rollback(function(err) {
172 expect(err).to.be.instanceof(Error);
173 done();
174 });
175 });
176 });
177
178 describe('timeout', function() {
179 const TIMEOUT = 50;
180 before(function() {
181 // Reset the collection
182 db.connector.data = {};
183 });
184
185 const post = {title: 't3', content: 'c3'};
186 beforeEach(createPostInTx(post, TIMEOUT));
187
188 it('should report timeout', function(done) {
189 // wait until the "create post" transaction times out
190 setTimeout(runTheTest, TIMEOUT * 3);
191
192 function runTheTest() {
193 Post.find({where: {title: 't3'}}, {transaction: currentTx},
194 function(err, posts) {
195 expect(err).to.match(/transaction.*not active/);
196 done();
197 });
198 }
199 });
200
201 it('should invoke the timeout hook', function(done) {
202 currentTx.observe('timeout', function(context, next) {
203 next();
204 done();
205 });
206
207 // If the event is not fired quickly enough, then the test can
208 // quickly fail - no need to wait full two seconds (Mocha's default)
209 this.timeout(TIMEOUT * 3);
210 });
211 });
212
213 describe('isActive', function() {
214 it('returns true when connection is active', function(done) {
215 Post.beginTransaction({
216 isolationLevel: Transaction.READ_COMMITTED,
217 timeout: 1000,
218 },
219 function(err, tx) {
220 if (err) return done(err);
221 expect(tx.isActive()).to.equal(true);
222 return done();
223 });
224 });
225 it('returns false when connection is not active', function(done) {
226 Post.beginTransaction({
227 isolationLevel: Transaction.READ_COMMITTED,
228 timeout: 1000,
229 },
230 function(err, tx) {
231 if (err) return done(err);
232 delete tx.connection;
233 expect(tx.isActive()).to.equal(false);
234 return done();
235 });
236 });
237 });
238
239 describe('transaction instance', function() {
240 function TestTransaction(connector, connection) {
241 this.connector = connector;
242 this.connection = connection;
243 }
244 Object.assign(TestTransaction.prototype, Transaction.prototype);
245 TestTransaction.prototype.foo = true;
246 function beginTransaction(isolationLevel, cb) {
247 return cb(null, new TestTransaction(testConnector, {}));
248 }
249
250 it('should do nothing when transaction is like a Transaction', function(done) {
251 testConnector.initialize(db, function(err, resultConnector) {
252 resultConnector.beginTransaction = beginTransaction;
253 Transaction.begin(resultConnector, Transaction.READ_COMMITTED,
254 function(err, result) {
255 if (err) done(err);
256 expect(result).to.be.instanceof(TestTransaction);
257 expect(result.foo).to.equal(true);
258 done();
259 });
260 });
261 });
262
263 it('should create new instance when transaction is not like a Transaction',
264 function(done) {
265 testConnector.initialize(db, function(err, resultConnector) {
266 resultConnector.beginTransaction = beginTransaction;
267 delete TestTransaction.prototype.commit;
268 Transaction.begin(resultConnector, Transaction.READ_COMMITTED,
269 function(err, result) {
270 if (err) done(err);
271 expect(result).to.not.be.instanceof(TestTransaction);
272 expect(result).to.be.instanceof(Transaction);
273 expect(result.foo).to.equal(undefined);
274 done();
275 });
276 });
277 });
278 });
279
280 it('can return promise for commit', function() {
281 const connectorObject = {};
282 connectorObject.commit = function(connection, cb) {
283 return cb(null, 'committed');
284 };
285 const transactionInstance = new Transaction(connectorObject, {});
286 return expect(transactionInstance.commit()).to.eventually.equal('committed');
287 });
288
289 it('can return promise for rollback', function() {
290 const connectorObject = {};
291 connectorObject.rollback = function(connection, cb) {
292 return cb(null, 'rolledback');
293 };
294 const transactionInstance = new Transaction(connectorObject, {});
295 return expect(transactionInstance.rollback()).to.eventually.equal('rolledback');
296 });
297
298 it('can return promise for begin', function() {
299 const connectorObject = {};
300 connectorObject.beginTransaction = function(connection, cb) {
301 return cb(null, 'begun');
302 };
303
304 return expect(
305 Transaction.begin(connectorObject, ''),
306 ).to.eventually.be.instanceOf(Transaction);
307 });
308});