UNPKG

15.6 kBJavaScriptView Raw
1'use strict'
2
3/* global describe, beforeEach, it, afterEach */
4/* eslint-disable no-unused-expressions */
5
6// Assertions and Stubbing
7const chai = require('chai')
8const sinon = require('sinon')
9chai.use(require('sinon-chai'))
10
11const expect = chai.expect
12
13// Hubot classes
14const Robot = require('../src/robot')
15const TextMessage = require('../src/message').TextMessage
16const Response = require('../src/response')
17const Middleware = require('../src/middleware')
18
19// mock `hubot-mock-adapter` module from fixture
20const mockery = require('mockery')
21
22describe('Middleware', function () {
23 describe('Unit Tests', function () {
24 beforeEach(function () {
25 // Stub out event emitting
26 this.robot = {emit: sinon.spy()}
27
28 this.middleware = new Middleware(this.robot)
29 })
30
31 describe('#execute', function () {
32 it('executes synchronous middleware', function (testDone) {
33 const testMiddleware = sinon.spy((context, next, done) => {
34 next(done)
35 })
36
37 this.middleware.register(testMiddleware)
38
39 const middlewareFinished = function () {
40 expect(testMiddleware).to.have.been.called
41 testDone()
42 }
43
44 this.middleware.execute(
45 {},
46 (_, done) => done(),
47 middlewareFinished
48 )
49 })
50
51 it('executes asynchronous middleware', function (testDone) {
52 const testMiddleware = sinon.spy((context, next, done) =>
53 // Yield to the event loop
54 process.nextTick(() => next(done))
55 )
56
57 this.middleware.register(testMiddleware)
58
59 const middlewareFinished = function (context, done) {
60 expect(testMiddleware).to.have.been.called
61 testDone()
62 }
63
64 this.middleware.execute(
65 {},
66 (_, done) => done(),
67 middlewareFinished
68 )
69 })
70
71 it('passes the correct arguments to each middleware', function (testDone) {
72 const testContext = {}
73 const testMiddleware = (context, next, done) =>
74 // Break out of middleware error handling so assertion errors are
75 // more visible
76 process.nextTick(function () {
77 // Check that variables were passed correctly
78 expect(context).to.equal(testContext)
79 next(done)
80 })
81
82 this.middleware.register(testMiddleware)
83
84 this.middleware.execute(
85 testContext,
86 (_, done) => done(),
87 () => testDone())
88 })
89
90 it('executes all registered middleware in definition order', function (testDone) {
91 const middlewareExecution = []
92
93 const testMiddlewareA = (context, next, done) => {
94 middlewareExecution.push('A')
95 next(done)
96 }
97
98 const testMiddlewareB = function (context, next, done) {
99 middlewareExecution.push('B')
100 next(done)
101 }
102
103 this.middleware.register(testMiddlewareA)
104 this.middleware.register(testMiddlewareB)
105
106 const middlewareFinished = function () {
107 expect(middlewareExecution).to.deep.equal(['A', 'B'])
108 testDone()
109 }
110
111 this.middleware.execute(
112 {},
113 (_, done) => done(),
114 middlewareFinished
115 )
116 })
117
118 it('executes the next callback after the function returns when there is no middleware', function (testDone) {
119 let finished = false
120 this.middleware.execute(
121 {},
122 function () {
123 expect(finished).to.be.ok
124 testDone()
125 },
126 function () {}
127 )
128 finished = true
129 })
130
131 it('always executes middleware after the function returns', function (testDone) {
132 let finished = false
133
134 this.middleware.register(function (context, next, done) {
135 expect(finished).to.be.ok
136 testDone()
137 })
138
139 this.middleware.execute({}, function () {}, function () {})
140 finished = true
141 })
142
143 it('creates a default "done" function', function (testDone) {
144 let finished = false
145
146 this.middleware.register(function (context, next, done) {
147 expect(finished).to.be.ok
148 testDone()
149 })
150
151 // we're testing the lack of a third argument here.
152 this.middleware.execute({}, function () {})
153 finished = true
154 })
155
156 it('does the right thing with done callbacks', function (testDone) {
157 // we want to ensure that the 'done' callbacks are nested correctly
158 // (executed in reverse order of definition)
159 const execution = []
160
161 const testMiddlewareA = function (context, next, done) {
162 execution.push('middlewareA')
163 next(function () {
164 execution.push('doneA')
165 done()
166 })
167 }
168
169 const testMiddlewareB = function (context, next, done) {
170 execution.push('middlewareB')
171 next(function () {
172 execution.push('doneB')
173 done()
174 })
175 }
176
177 this.middleware.register(testMiddlewareA)
178 this.middleware.register(testMiddlewareB)
179
180 const allDone = function () {
181 expect(execution).to.deep.equal(['middlewareA', 'middlewareB', 'doneB', 'doneA'])
182 testDone()
183 }
184
185 this.middleware.execute(
186 {},
187 // Short circuit at the bottom of the middleware stack
188 (_, done) => done(),
189 allDone
190 )
191 })
192
193 it('defaults to the latest done callback if none is provided', function (testDone) {
194 // we want to ensure that the 'done' callbacks are nested correctly
195 // (executed in reverse order of definition)
196 const execution = []
197
198 const testMiddlewareA = function (context, next, done) {
199 execution.push('middlewareA')
200 next(function () {
201 execution.push('doneA')
202 done()
203 })
204 }
205
206 const testMiddlewareB = function (context, next, done) {
207 execution.push('middlewareB')
208 next()
209 }
210
211 this.middleware.register(testMiddlewareA)
212 this.middleware.register(testMiddlewareB)
213
214 const allDone = function () {
215 expect(execution).to.deep.equal(['middlewareA', 'middlewareB', 'doneA'])
216 testDone()
217 }
218
219 this.middleware.execute(
220 {},
221 // Short circuit at the bottom of the middleware stack
222 (_, done) => done(),
223 allDone
224 )
225 })
226
227 describe('error handling', function () {
228 it('does not execute subsequent middleware after the error is thrown', function (testDone) {
229 const middlewareExecution = []
230
231 const testMiddlewareA = function (context, next, done) {
232 middlewareExecution.push('A')
233 next(done)
234 }
235
236 const testMiddlewareB = function (context, next, done) {
237 middlewareExecution.push('B')
238 throw new Error()
239 }
240
241 const testMiddlewareC = function (context, next, done) {
242 middlewareExecution.push('C')
243 next(done)
244 }
245
246 this.middleware.register(testMiddlewareA)
247 this.middleware.register(testMiddlewareB)
248 this.middleware.register(testMiddlewareC)
249
250 const middlewareFinished = sinon.spy()
251 const middlewareFailed = () => {
252 expect(middlewareFinished).to.not.have.been.called
253 expect(middlewareExecution).to.deep.equal(['A', 'B'])
254 testDone()
255 }
256
257 this.middleware.execute(
258 {},
259 middlewareFinished,
260 middlewareFailed
261 )
262 })
263
264 it('emits an error event', function (testDone) {
265 const testResponse = {}
266 const theError = new Error()
267
268 const testMiddleware = function (context, next, done) {
269 throw theError
270 }
271
272 this.middleware.register(testMiddleware)
273
274 this.robot.emit = sinon.spy(function (name, err, response) {
275 expect(name).to.equal('error')
276 expect(err).to.equal(theError)
277 expect(response).to.equal(testResponse)
278 })
279
280 const middlewareFinished = sinon.spy()
281 const middlewareFailed = () => {
282 expect(this.robot.emit).to.have.been.called
283 testDone()
284 }
285
286 this.middleware.execute(
287 {response: testResponse},
288 middlewareFinished,
289 middlewareFailed
290 )
291 })
292
293 it('unwinds the middleware stack (calling all done functions)', function (testDone) {
294 let extraDoneFunc = null
295
296 const testMiddlewareA = function (context, next, done) {
297 // Goal: make sure that the middleware stack is unwound correctly
298 extraDoneFunc = sinon.spy(done)
299 next(extraDoneFunc)
300 }
301
302 const testMiddlewareB = function (context, next, done) {
303 throw new Error()
304 }
305
306 this.middleware.register(testMiddlewareA)
307 this.middleware.register(testMiddlewareB)
308
309 const middlewareFinished = sinon.spy()
310 const middlewareFailed = function () {
311 // Sanity check that the error was actually thrown
312 expect(middlewareFinished).to.not.have.been.called
313
314 expect(extraDoneFunc).to.have.been.called
315 testDone()
316 }
317
318 this.middleware.execute(
319 {},
320 middlewareFinished,
321 middlewareFailed
322 )
323 })
324 })
325 })
326
327 describe('#register', function () {
328 it('adds to the list of middleware', function () {
329 const testMiddleware = function (context, next, done) {}
330
331 this.middleware.register(testMiddleware)
332
333 expect(this.middleware.stack).to.include(testMiddleware)
334 })
335
336 it('validates the arity of middleware', function () {
337 const testMiddleware = function (context, next, done, extra) {}
338
339 expect(() => this.middleware.register(testMiddleware)).to.throw(/Incorrect number of arguments/)
340 })
341 })
342 })
343
344 // Per the documentation in docs/scripting.md
345 // Any new fields that are exposed to middleware should be explicitly
346 // tested for.
347 describe('Public Middleware APIs', function () {
348 beforeEach(function () {
349 mockery.enable({
350 warnOnReplace: false,
351 warnOnUnregistered: false
352 })
353 mockery.registerMock('hubot-mock-adapter', require('./fixtures/mock-adapter'))
354 this.robot = new Robot(null, 'mock-adapter', true, 'TestHubot')
355 this.robot.run
356
357 // Re-throw AssertionErrors for clearer test failures
358 this.robot.on('error', function (name, err, response) {
359 if (__guard__(err != null ? err.constructor : undefined, x => x.name) === 'AssertionError') {
360 process.nextTick(function () {
361 throw err
362 })
363 }
364 })
365
366 this.user = this.robot.brain.userForId('1', {
367 name: 'hubottester',
368 room: '#mocha'
369 })
370
371 // Dummy middleware
372 this.middleware = sinon.spy((context, next, done) => next(done))
373
374 this.testMessage = new TextMessage(this.user, 'message123')
375 this.robot.hear(/^message123$/, function (response) {})
376 this.testListener = this.robot.listeners[0]
377 })
378
379 afterEach(function () {
380 mockery.disable()
381 this.robot.shutdown()
382 })
383
384 describe('listener middleware context', function () {
385 beforeEach(function () {
386 this.robot.listenerMiddleware((context, next, done) => {
387 this.middleware(context, next, done)
388 })
389 })
390
391 describe('listener', function () {
392 it('is the listener object that matched', function (testDone) {
393 this.robot.receive(this.testMessage, () => {
394 expect(this.middleware).to.have.been.calledWithMatch(
395 sinon.match.has('listener',
396 sinon.match.same(this.testListener)), // context
397 sinon.match.any, // next
398 sinon.match.any // done
399 )
400 testDone()
401 })
402 })
403
404 it('has options.id (metadata)', function (testDone) {
405 this.robot.receive(this.testMessage, () => {
406 expect(this.middleware).to.have.been.calledWithMatch(
407 sinon.match.has('listener',
408 sinon.match.has('options',
409 sinon.match.has('id'))), // context
410 sinon.match.any, // next
411 sinon.match.any // done
412 )
413 testDone()
414 })
415 })
416 })
417
418 describe('response', () =>
419 it('is a Response that wraps the message', function (testDone) {
420 this.robot.receive(this.testMessage, () => {
421 expect(this.middleware).to.have.been.calledWithMatch(
422 sinon.match.has('response',
423 sinon.match.instanceOf(Response).and(
424 sinon.match.has('message',
425 sinon.match.same(this.testMessage)))), // context
426 sinon.match.any, // next
427 sinon.match.any // done
428 )
429 testDone()
430 })
431 })
432 )
433 })
434
435 describe('receive middleware context', function () {
436 beforeEach(function () {
437 this.robot.receiveMiddleware((context, next, done) => {
438 this.middleware(context, next, done)
439 })
440 })
441
442 describe('response', () =>
443 it('is a match-less Response object', function (testDone) {
444 this.robot.receive(this.testMessage, () => {
445 expect(this.middleware).to.have.been.calledWithMatch(
446 sinon.match.has('response',
447 sinon.match.instanceOf(Response).and(
448 sinon.match.has('message',
449 sinon.match.same(this.testMessage)))), // context
450 sinon.match.any, // next
451 sinon.match.any // done
452 )
453 testDone()
454 })
455 })
456 )
457 })
458
459 describe('next', function () {
460 beforeEach(function () {
461 this.robot.listenerMiddleware((context, next, done) => {
462 this.middleware(context, next, done)
463 })
464 })
465
466 it('is a function with arity one', function (testDone) {
467 this.robot.receive(this.testMessage, () => {
468 expect(this.middleware).to.have.been.calledWithMatch(
469 sinon.match.any, // context
470 sinon.match.func.and(
471 sinon.match.has('length',
472 sinon.match(1))), // next
473 sinon.match.any // done
474 )
475 testDone()
476 })
477 })
478 })
479
480 describe('done', function () {
481 beforeEach(function () {
482 this.robot.listenerMiddleware((context, next, done) => {
483 this.middleware(context, next, done)
484 })
485 })
486
487 it('is a function with arity zero', function (testDone) {
488 this.robot.receive(this.testMessage, () => {
489 expect(this.middleware).to.have.been.calledWithMatch(
490 sinon.match.any, // context
491 sinon.match.any, // next
492 sinon.match.func.and(
493 sinon.match.has('length',
494 sinon.match(0))) // done
495 )
496 testDone()
497 })
498 })
499 })
500 })
501})
502
503function __guard__ (value, transform) {
504 (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined
505}