UNPKG

12.2 kBJavaScriptView Raw
1// Copyright IBM Corp. 2015,2019. All Rights Reserved.
2// Node module: loopback-datasource-juggler
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6'use strict';
7
8const ModelBuilder = require('../').ModelBuilder;
9const should = require('./init');
10
11describe('async observer', function() {
12 let TestModel;
13 beforeEach(function defineTestModel() {
14 const modelBuilder = new ModelBuilder();
15 TestModel = modelBuilder.define('TestModel', {name: String});
16 });
17
18 it('calls registered async observers', function(done) {
19 const notifications = [];
20 TestModel.observe('before', pushAndNext(notifications, 'before'));
21 TestModel.observe('after', pushAndNext(notifications, 'after'));
22
23 TestModel.notifyObserversOf('before', {}, function(err) {
24 if (err) return done(err);
25 notifications.push('call');
26 TestModel.notifyObserversOf('after', {}, function(err) {
27 if (err) return done(err);
28
29 notifications.should.eql(['before', 'call', 'after']);
30 done();
31 });
32 });
33 });
34
35 it('allows multiple observers for the same operation', function(done) {
36 const notifications = [];
37 TestModel.observe('event', pushAndNext(notifications, 'one'));
38 TestModel.observe('event', pushAndNext(notifications, 'two'));
39
40 TestModel.notifyObserversOf('event', {}, function(err) {
41 if (err) return done(err);
42 notifications.should.eql(['one', 'two']);
43 done();
44 });
45 });
46
47 it('allows multiple operations to be notified in one call', function(done) {
48 const notifications = [];
49 TestModel.observe('event1', pushAndNext(notifications, 'one'));
50 TestModel.observe('event2', pushAndNext(notifications, 'two'));
51
52 TestModel.notifyObserversOf(['event1', 'event2'], {}, function(err) {
53 if (err) return done(err);
54 notifications.should.eql(['one', 'two']);
55 done();
56 });
57 });
58
59 it('inherits observers from base model', function(done) {
60 const notifications = [];
61 TestModel.observe('event', pushAndNext(notifications, 'base'));
62
63 const Child = TestModel.extend('Child');
64 Child.observe('event', pushAndNext(notifications, 'child'));
65
66 Child.notifyObserversOf('event', {}, function(err) {
67 if (err) return done(err);
68 notifications.should.eql(['base', 'child']);
69 done();
70 });
71 });
72
73 it('allow multiple operations to be notified with base models', function(done) {
74 const notifications = [];
75 TestModel.observe('event1', pushAndNext(notifications, 'base1'));
76 TestModel.observe('event2', pushAndNext(notifications, 'base2'));
77
78 const Child = TestModel.extend('Child');
79 Child.observe('event1', pushAndNext(notifications, 'child1'));
80 Child.observe('event2', pushAndNext(notifications, 'child2'));
81
82 Child.notifyObserversOf(['event1', 'event2'], {}, function(err) {
83 if (err) return done(err);
84 notifications.should.eql(['base1', 'child1', 'base2', 'child2']);
85 done();
86 });
87 });
88
89 it('does not modify observers in the base model', function(done) {
90 const notifications = [];
91 TestModel.observe('event', pushAndNext(notifications, 'base'));
92
93 const Child = TestModel.extend('Child');
94 Child.observe('event', pushAndNext(notifications, 'child'));
95
96 TestModel.notifyObserversOf('event', {}, function(err) {
97 if (err) return done(err);
98 notifications.should.eql(['base']);
99 done();
100 });
101 });
102
103 it('always calls inherited observers', function(done) {
104 const notifications = [];
105 TestModel.observe('event', pushAndNext(notifications, 'base'));
106
107 const Child = TestModel.extend('Child');
108 // Important: there are no observers on the Child model
109
110 Child.notifyObserversOf('event', {}, function(err) {
111 if (err) return done(err);
112 notifications.should.eql(['base']);
113 done();
114 });
115 });
116
117 it('can remove observers', function(done) {
118 const notifications = [];
119
120 function call(ctx, next) {
121 notifications.push('call');
122 process.nextTick(next);
123 }
124
125 TestModel.observe('event', call);
126 TestModel.removeObserver('event', call);
127
128 TestModel.notifyObserversOf('event', {}, function(err) {
129 if (err) return done(err);
130 notifications.should.eql([]);
131 done();
132 });
133 });
134
135 it('can clear all observers', function(done) {
136 const notifications = [];
137
138 function call(ctx, next) {
139 notifications.push('call');
140 process.nextTick(next);
141 }
142
143 TestModel.observe('event', call);
144 TestModel.observe('event', call);
145 TestModel.observe('event', call);
146 TestModel.clearObservers('event');
147
148 TestModel.notifyObserversOf('event', {}, function(err) {
149 if (err) return done(err);
150 notifications.should.eql([]);
151 done();
152 });
153 });
154
155 it('handles no observers', function(done) {
156 TestModel.notifyObserversOf('no-observers', {}, function(err) {
157 // the test passes when no error was raised
158 done(err);
159 });
160 });
161
162 it('passes context to final callback', function(done) {
163 const context = {};
164 TestModel.notifyObserversOf('event', context, function(err, ctx) {
165 (ctx || 'null').should.equal(context);
166 done();
167 });
168 });
169
170 describe('notifyObserversAround', function() {
171 let notifications;
172 beforeEach(function() {
173 notifications = [];
174 TestModel.observe('before execute',
175 pushAndNext(notifications, 'before execute'));
176 TestModel.observe('after execute',
177 pushAndNext(notifications, 'after execute'));
178 });
179
180 it('should notify before/after observers', function(done) {
181 const context = {};
182
183 function work(done) {
184 process.nextTick(function() {
185 done(null, 1);
186 });
187 }
188
189 TestModel.notifyObserversAround('execute', context, work,
190 function(err, result) {
191 notifications.should.eql(['before execute', 'after execute']);
192 result.should.eql(1);
193 done();
194 });
195 });
196
197 it('should allow work with context', function(done) {
198 const context = {};
199
200 function work(context, done) {
201 process.nextTick(function() {
202 done(null, 1);
203 });
204 }
205
206 TestModel.notifyObserversAround('execute', context, work,
207 function(err, result) {
208 notifications.should.eql(['before execute', 'after execute']);
209 result.should.eql(1);
210 done();
211 });
212 });
213
214 it('should notify before/after observers with multiple results',
215 function(done) {
216 const context = {};
217
218 function work(done) {
219 process.nextTick(function() {
220 done(null, 1, 2);
221 });
222 }
223
224 TestModel.notifyObserversAround('execute', context, work,
225 function(err, r1, r2) {
226 r1.should.eql(1);
227 r2.should.eql(2);
228 notifications.should.eql(['before execute', 'after execute']);
229 done();
230 });
231 });
232
233 it('should allow observers to skip other ones',
234 function(done) {
235 TestModel.observe('before invoke',
236 function(context, next) {
237 notifications.push('before invoke');
238 context.end(null, 0);
239 });
240 TestModel.observe('after invoke',
241 pushAndNext(notifications, 'after invoke'));
242
243 const context = {};
244
245 function work(done) {
246 process.nextTick(function() {
247 done(null, 1, 2);
248 });
249 }
250
251 TestModel.notifyObserversAround('invoke', context, work,
252 function(err, r1) {
253 r1.should.eql(0);
254 notifications.should.eql(['before invoke']);
255 done();
256 });
257 });
258
259 it('should allow observers to tweak results',
260 function(done) {
261 TestModel.observe('after invoke',
262 function(context, next) {
263 notifications.push('after invoke');
264 context.results = [3];
265 next();
266 });
267
268 const context = {};
269
270 function work(done) {
271 process.nextTick(function() {
272 done(null, 1, 2);
273 });
274 }
275
276 TestModel.notifyObserversAround('invoke', context, work,
277 function(err, r1) {
278 r1.should.eql(3);
279 notifications.should.eql(['after invoke']);
280 done();
281 });
282 });
283 });
284
285 it('resolves promises returned by observers', function(done) {
286 TestModel.observe('event', function(ctx) {
287 return Promise.resolve('value-to-ignore');
288 });
289 TestModel.notifyObserversOf('event', {}, function(err, ctx) {
290 // the test times out when the promises are not supported
291 done();
292 });
293 });
294
295 it('handles rejected promise returned by an observer', function(done) {
296 const testError = new Error('expected test error');
297 TestModel.observe('event', function(ctx) {
298 return Promise.reject(testError);
299 });
300 TestModel.notifyObserversOf('event', {}, function(err, ctx) {
301 err.should.eql(testError);
302 done();
303 });
304 });
305
306 it('returns a promise when no callback is provided', function() {
307 const context = {value: 'a-test-context'};
308 const p = TestModel.notifyObserversOf('event', context);
309 (p !== undefined).should.be.true;
310 return p.then(function(result) {
311 result.should.eql(context);
312 });
313 });
314
315 it('returns a rejected promise when no callback is provided', function() {
316 const testError = new Error('expected test error');
317 TestModel.observe('event', function(ctx, next) { next(testError); });
318 const p = TestModel.notifyObserversOf('event', context);
319 return p.then(
320 function(result) {
321 throw new Error('The promise should have been rejected.');
322 },
323 function(err) {
324 err.should.eql(testError);
325 },
326 );
327 });
328
329 it('should call after operation hook on error', function(done) {
330 const context = {
331 req: {},
332 };
333 const operationError = new Error('The operation failed without result');
334 let callCount = 0;
335
336 function fail(context, done) {
337 process.nextTick(() => {
338 done(operationError);
339 });
340 }
341
342 TestModel.observe('after execute error', function(ctx, next) {
343 callCount++;
344 next();
345 });
346
347 TestModel.notifyObserversAround('execute', context, fail, (err, ctx) => {
348 callCount.should.eql(1);
349 err.message.should.eql(operationError.message);
350 ctx.error.message.should.eql(operationError.message);
351 done();
352 });
353 });
354
355 it('should call after operation hook on error while overwriting error', function(done) {
356 const context = {
357 req: {},
358 };
359 const operationError = new Error('The operation failed without result');
360 const overwriteError = new Error('Overwriting the original error');
361 let callCount = 0;
362
363 function fail(context, done) {
364 process.nextTick(() => {
365 done(operationError);
366 });
367 }
368
369 TestModel.observe('after execute error', function(ctx, next) {
370 callCount++;
371 next(overwriteError);
372 });
373
374 TestModel.notifyObserversAround('execute', context, fail, (err, ctx) => {
375 callCount.should.eql(1);
376 err.message.should.eql(overwriteError.message);
377 ctx.error.message.should.eql(operationError.message);
378 done();
379 });
380 });
381
382 it('should call after operation hook on error while allowing to change err', function(done) {
383 const context = {
384 req: {},
385 };
386 const operationError = new Error('The operation failed without result');
387 let callCount = 0;
388
389 function fail(context, done) {
390 process.nextTick(() => {
391 done(operationError);
392 });
393 }
394
395 TestModel.observe('after execute error', function(ctx, next) {
396 callCount++;
397 const err = ctx.error;
398 next(err, ctx);
399 });
400
401 TestModel.notifyObserversAround('execute', context, fail, (err, ctx) => {
402 callCount.should.eql(1);
403 err.message.should.eql(operationError.message);
404 ctx.error.message.should.eql(operationError.message);
405 done();
406 });
407 });
408});
409
410function pushAndNext(array, value) {
411 return function(ctx, next) {
412 array.push(value);
413 process.nextTick(next);
414 };
415}