1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | 'use strict';
|
7 |
|
8 | const DataSource = require('..').DataSource;
|
9 | const EventEmitter = require('events');
|
10 | const Connector = require('loopback-connector').Connector;
|
11 | const Transaction = require('loopback-connector').Transaction;
|
12 | const should = require('./init.js');
|
13 |
|
14 | describe('Transactions on memory connector', function() {
|
15 | let db, tx;
|
16 |
|
17 | before(() => {
|
18 | db = getSchema();
|
19 | db.define('Model');
|
20 | });
|
21 |
|
22 | it('returns an EventEmitter object', done => {
|
23 | tx = db.transaction();
|
24 | tx.should.be.instanceOf(EventEmitter);
|
25 | done();
|
26 | });
|
27 |
|
28 | it('exposes and caches slave models', done => {
|
29 | testModelCaching(tx.models, db.models);
|
30 | done();
|
31 | });
|
32 |
|
33 | it('changes count when committing', done => {
|
34 | db.models.Model.count((err, count) => {
|
35 | should.not.exist(err);
|
36 | should.exist(count);
|
37 | count.should.equal(0);
|
38 | tx.models.Model.create(Array(1), () => {
|
39 |
|
40 | });
|
41 | tx.commit(err => {
|
42 | should.not.exist(err);
|
43 | db.models.Model.count((err, count) => {
|
44 | should.not.exist(err);
|
45 | should.exist(count);
|
46 | count.should.equal(1);
|
47 | done();
|
48 | });
|
49 | });
|
50 | });
|
51 | });
|
52 | });
|
53 |
|
54 | describe('Transactions on test connector without execute()', () => {
|
55 | let db, tx;
|
56 |
|
57 | before(() => {
|
58 | db = createDataSource();
|
59 | });
|
60 |
|
61 | beforeEach(resetState);
|
62 |
|
63 | it('resolves to an EventEmitter', done => {
|
64 | const promise = db.transaction();
|
65 | promise.should.be.Promise();
|
66 | promise.then(transaction => {
|
67 | should.exist(transaction);
|
68 | transaction.should.be.instanceof(EventEmitter);
|
69 | tx = transaction;
|
70 | done();
|
71 | }, done);
|
72 | });
|
73 |
|
74 | it('beginTransaction returns a transaction', async () => {
|
75 | const promise = db.beginTransaction(Transaction.READ_UNCOMMITTED);
|
76 | promise.should.be.Promise();
|
77 | const transaction = await promise;
|
78 | transaction.should.be.instanceof(EventEmitter);
|
79 | });
|
80 |
|
81 | it('exposes and caches slave models', done => {
|
82 | testModelCaching(tx.models, db.models);
|
83 | done();
|
84 | });
|
85 |
|
86 | it('does not allow nesting of transactions', done => {
|
87 | (() => tx.transaction()).should.throw('Nesting transactions is not supported');
|
88 | done();
|
89 | });
|
90 |
|
91 | it('calls commit() on the connector', done => {
|
92 | db.transaction().then(tx => {
|
93 | tx.commit(err => {
|
94 | callCount.should.deepEqual({commit: 1, rollback: 0, create: 0});
|
95 | done(err);
|
96 | });
|
97 | }, done);
|
98 | });
|
99 |
|
100 | it('calls rollback() on the connector', done => {
|
101 | db.transaction().then(tx => {
|
102 | tx.rollback(err => {
|
103 | callCount.should.deepEqual({commit: 0, rollback: 1, create: 0});
|
104 | done(err);
|
105 | });
|
106 | }, done);
|
107 | });
|
108 | });
|
109 |
|
110 | describe('Transactions on test connector with execute()', () => {
|
111 | let db;
|
112 |
|
113 | before(() => {
|
114 | db = createDataSource();
|
115 | });
|
116 |
|
117 | beforeEach(resetState);
|
118 |
|
119 | it('passes models and calls commit() automatically', done => {
|
120 | db.transaction(models => {
|
121 | testModelCaching(models, db.models);
|
122 | return models.Model.create({});
|
123 | }, err => {
|
124 | callCount.should.deepEqual({commit: 1, rollback: 0, create: 1});
|
125 | transactionPassed.should.be.true();
|
126 | done(err);
|
127 | });
|
128 | });
|
129 |
|
130 | it('calls rollback() automatically when throwing an error', done => {
|
131 | let error;
|
132 | db.transaction(models => {
|
133 | error = new Error('exception');
|
134 | throw error;
|
135 | }, err => {
|
136 | error.should.equal(err);
|
137 | callCount.should.deepEqual({commit: 0, rollback: 1, create: 0});
|
138 | done();
|
139 | });
|
140 | });
|
141 |
|
142 | it('reports execution timeouts', done => {
|
143 | let timedOut = false;
|
144 | db.transaction(models => {
|
145 | setTimeout(() => {
|
146 | models.Model.create({}, function(err) {
|
147 | if (!timedOut) {
|
148 | done(new Error('Timeout was ineffective'));
|
149 | } else {
|
150 | should.exist(err);
|
151 | err.message.should.startWith('The transaction is not active:');
|
152 | done();
|
153 | }
|
154 | });
|
155 | }, 50);
|
156 | }, {
|
157 | timeout: 25,
|
158 | }, err => {
|
159 | timedOut = true;
|
160 | should.exist(err);
|
161 | err.code.should.equal('TRANSACTION_TIMEOUT');
|
162 | err.message.should.equal('Transaction is rolled back due to timeout');
|
163 | callCount.should.deepEqual({commit: 0, rollback: 1, create: 0});
|
164 | });
|
165 | });
|
166 | });
|
167 |
|
168 | function createDataSource() {
|
169 | const db = new DataSource({
|
170 | initialize: (dataSource, cb) => {
|
171 | dataSource.connector = new TestConnector();
|
172 | cb();
|
173 | },
|
174 | });
|
175 | db.define('Model');
|
176 | return db;
|
177 | }
|
178 |
|
179 | function testModelCaching(txModels, dbModels) {
|
180 | should.exist(txModels);
|
181 |
|
182 |
|
183 | const accessor = Object.getOwnPropertyDescriptor(txModels, 'Model');
|
184 | should.exist(accessor);
|
185 | should.exist(accessor.get);
|
186 | accessor.get.should.be.Function();
|
187 | const Model = txModels.Model;
|
188 | should.exist(Model);
|
189 |
|
190 | const desc = Object.getOwnPropertyDescriptor(txModels, 'Model');
|
191 | should.exist(desc.value);
|
192 | Model.should.equal(txModels.Model);
|
193 | Model.prototype.should.be.instanceof(dbModels.Model);
|
194 | }
|
195 |
|
196 | let callCount;
|
197 | let transactionPassed;
|
198 |
|
199 | function resetState() {
|
200 | callCount = {commit: 0, rollback: 0, create: 0};
|
201 | transactionPassed = false;
|
202 | }
|
203 |
|
204 | class TestConnector extends Connector {
|
205 | constructor() {
|
206 | super('test');
|
207 | }
|
208 |
|
209 | beginTransaction(isolationLevel, cb) {
|
210 | this.currentTransaction = new Transaction(this, this);
|
211 | process.nextTick(() => cb(null, this.currentTransaction));
|
212 | }
|
213 |
|
214 | commit(tx, cb) {
|
215 | callCount.commit++;
|
216 | cb();
|
217 | }
|
218 |
|
219 | rollback(tx, cb) {
|
220 | callCount.rollback++;
|
221 | cb();
|
222 | }
|
223 |
|
224 | create(model, data, options, cb) {
|
225 | callCount.create++;
|
226 | const transaction = options.transaction;
|
227 | const current = this.currentTransaction;
|
228 | transactionPassed = transaction &&
|
229 | (current === transaction || current === transaction.connection);
|
230 | cb();
|
231 | }
|
232 | }
|