UNPKG

15.4 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 process.env.EXPRESS_PORT = 0
355 this.robot = new Robot(null, 'mock-adapter', true, 'TestHubot')
356 this.robot.run
357
358 // Re-throw AssertionErrors for clearer test failures
359 this.robot.on('error', function (name, err, response) {
360 if (__guard__(err != null ? err.constructor : undefined, x => x.name) === 'AssertionError') {
361 process.nextTick(function () {
362 throw err
363 })
364 }
365 })
366
367 this.user = this.robot.brain.userForId('1', {
368 name: 'hubottester',
369 room: '#mocha'
370 })
371
372 // Dummy middleware
373 this.middleware = sinon.spy((context, next, done) => next(done))
374
375 this.testMessage = new TextMessage(this.user, 'message123')
376 this.robot.hear(/^message123$/, function (response) {})
377 this.testListener = this.robot.listeners[0]
378 })
379
380 afterEach(function () {
381 mockery.disable()
382 this.robot.shutdown()
383 })
384
385 describe('listener middleware context', function () {
386 beforeEach(function () {
387 this.robot.listenerMiddleware((context, next, done) => {
388 this.middleware(context, next, done)
389 })
390 })
391
392 describe('listener', function () {
393 it('is the listener object that matched', function (testDone) {
394 this.robot.receive(this.testMessage, () => {
395 expect(this.middleware).to.have.been.calledWithMatch(
396 sinon.match.has('listener',
397 sinon.match.same(this.testListener)), // context
398 sinon.match.any, // next
399 sinon.match.any // done
400 )
401 testDone()
402 })
403 })
404
405 it('has options.id (metadata)', function (testDone) {
406 this.robot.receive(this.testMessage, () => {
407 expect(this.middleware).to.have.been.calledWithMatch(
408 sinon.match.has('listener',
409 sinon.match.has('options',
410 sinon.match.has('id'))), // context
411 sinon.match.any, // next
412 sinon.match.any // done
413 )
414 testDone()
415 })
416 })
417 })
418
419 describe('response', () =>
420 it('is a Response that wraps the message', function (testDone) {
421 this.robot.receive(this.testMessage, () => {
422 expect(this.middleware).to.have.been.calledWithMatch(
423 sinon.match.has('response',
424 sinon.match.instanceOf(Response).and(
425 sinon.match.has('message',
426 sinon.match.same(this.testMessage)))), // context
427 sinon.match.any, // next
428 sinon.match.any // done
429 )
430 testDone()
431 })
432 })
433 )
434 })
435
436 describe('receive middleware context', function () {
437 beforeEach(function () {
438 this.robot.receiveMiddleware((context, next, done) => {
439 this.middleware(context, next, done)
440 })
441 })
442
443 describe('response', () =>
444 it('is a match-less Response object', function (testDone) {
445 this.robot.receive(this.testMessage, () => {
446 expect(this.middleware).to.have.been.calledWithMatch(
447 sinon.match.has('response',
448 sinon.match.instanceOf(Response).and(
449 sinon.match.has('message',
450 sinon.match.same(this.testMessage)))), // context
451 sinon.match.any, // next
452 sinon.match.any // done
453 )
454 testDone()
455 })
456 })
457 )
458 })
459
460 describe('next', function () {
461 beforeEach(function () {
462 this.robot.listenerMiddleware((context, next, done) => {
463 this.middleware(context, next, done)
464 })
465 })
466
467 it('is a function with arity one', function (testDone) {
468 this.robot.receive(this.testMessage, () => {
469 expect(this.middleware).to.have.been.calledWithMatch(
470 sinon.match.any, // context
471 sinon.match.func.and(
472 sinon.match.has('length',
473 sinon.match(1))), // next
474 sinon.match.any // done
475 )
476 testDone()
477 })
478 })
479 })
480
481 describe('done', function () {
482 beforeEach(function () {
483 this.robot.listenerMiddleware((context, next, done) => {
484 this.middleware(context, next, done)
485 })
486 })
487
488 it('is a function with arity zero', function (testDone) {
489 this.robot.receive(this.testMessage, () => {
490 expect(this.middleware).to.have.been.calledWithMatch(
491 sinon.match.any, // context
492 sinon.match.any, // next
493 sinon.match.func.and(
494 sinon.match.has('length',
495 sinon.match(0))) // done
496 )
497 testDone()
498 })
499 })
500 })
501 })
502})
503
504function __guard__ (value, transform) {
505 (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined
506}