UNPKG

imubot

Version:
852 lines (664 loc) 27.7 kB
# Assertions and Stubbing chai = require 'chai' sinon = require 'sinon' chai.use require 'sinon-chai' { expect } = chai mockery = require 'mockery' # Mubot classes Bot = require '../src/bot.coffee' { CatchAllMessage, EnterMessage, LeaveMessage, TextMessage, TopicMessage } = require '../src/message' Adapter = require '../src/adapter' ScopedHttpClient = require 'scoped-http-client' # Preload the Mubot mock adapter but substitute in the latest version of Adapter mockery.enable() mockery.registerAllowable 'mubot-mock-adapter' mockery.registerAllowable 'lodash' # mubot-mock-adapter uses lodash # Force mubot-mock-adapter to use the latest version of Adapter mockery.registerMock 'mubot/src/adapter', Adapter # Load the mock adapter into the cache require 'mubot-mock-adapter' # We're done with mockery mockery.deregisterMock 'mubot/src/adapter' mockery.disable() describe 'Bot', -> beforeEach -> @bot = new Bot null, 'mock-adapter', yes, 'TestMubot' @bot.alias = 'Mubot' @bot.run # Re-throw AssertionErrors for clearer test failures @bot.on 'error', (name, err, response) -> return unless err?.constructor? if err.constructor.name == "AssertionError" process.nextTick -> throw err @user = @bot.brain.userForId '1', { name: 'mubottester' room: '#mocha' } afterEach -> @bot.server.close() @bot.shutdown() describe 'Unit Tests', -> describe '#http', -> beforeEach -> url = 'http://localhost' @httpClient = @bot.http(url) it 'creates a new ScopedHttpClient', -> # 'instanceOf' check doesn't work here due to the design of # ScopedHttpClient expect(@httpClient).to.have.property('get') expect(@httpClient).to.have.property('post') it 'passes options through to the ScopedHttpClient', -> agent = {} httpClient = @bot.http('http://localhost', agent: agent) expect(httpClient.options.agent).to.equal(agent) it 'sets a sane user agent', -> expect(@httpClient.options.headers['User-Agent']).to.contain('Mubot') it 'merges in any global http options', -> agent = {} @bot.globalHttpOptions = {agent: agent} httpClient = @bot.http('http://localhost') expect(httpClient.options.agent).to.equal(agent) it 'local options override global http options', -> agentA = {} agentB = {} @bot.globalHttpOptions = {agent: agentA} httpClient = @bot.http('http://localhost', agent: agentB) expect(httpClient.options.agent).to.equal(agentB) describe '#respondPattern', -> it 'matches messages starting with bot\'s name', -> testMessage = @bot.name + 'message123' testRegex = /(.*)/ pattern = @bot.respondPattern testRegex expect(testMessage).to.match(pattern) match = testMessage.match(pattern)[1] expect(match).to.equal('message123') it 'matches messages starting with bot\'s alias', -> testMessage = @bot.alias + 'message123' testRegex = /(.*)/ pattern = @bot.respondPattern testRegex expect(testMessage).to.match(pattern) match = testMessage.match(pattern)[1] expect(match).to.equal('message123') it 'does not match unaddressed messages', -> testMessage = 'message123' testRegex = /(.*)/ pattern = @bot.respondPattern testRegex expect(testMessage).to.not.match(pattern) it 'matches properly when name is substring of alias', -> @bot.name = 'Meg' @bot.alias = 'Megan' testMessage1 = @bot.name + ' message123' testMessage2 = @bot.alias + ' message123' testRegex = /(.*)/ pattern = @bot.respondPattern testRegex expect(testMessage1).to.match(pattern) match1 = testMessage1.match(pattern)[1] expect(match1).to.equal('message123') expect(testMessage2).to.match(pattern) match2 = testMessage2.match(pattern)[1] expect(match2).to.equal('message123') it 'matches properly when alias is substring of name', -> @bot.name = 'Megan' @bot.alias = 'Meg' testMessage1 = @bot.name + ' message123' testMessage2 = @bot.alias + ' message123' testRegex = /(.*)/ pattern = @bot.respondPattern testRegex expect(testMessage1).to.match(pattern) match1 = testMessage1.match(pattern)[1] expect(match1).to.equal('message123') expect(testMessage2).to.match(pattern) match2 = testMessage2.match(pattern)[1] expect(match2).to.equal('message123') describe '#listen', -> it 'registers a new listener directly', -> expect(@bot.listeners).to.have.length(0) @bot.listen (->), -> expect(@bot.listeners).to.have.length(1) describe '#hear', -> it 'registers a new listener directly', -> expect(@bot.listeners).to.have.length(0) @bot.hear /.*/, -> expect(@bot.listeners).to.have.length(1) describe '#respond', -> it 'registers a new listener using hear', -> sinon.spy @bot, 'hear' @bot.respond /.*/, -> expect(@bot.hear).to.have.been.called describe '#enter', -> it 'registers a new listener using listen', -> sinon.spy @bot, 'listen' @bot.enter -> expect(@bot.listen).to.have.been.called describe '#leave', -> it 'registers a new listener using listen', -> sinon.spy @bot, 'listen' @bot.leave -> expect(@bot.listen).to.have.been.called describe '#topic', -> it 'registers a new listener using listen', -> sinon.spy @bot, 'listen' @bot.topic -> expect(@bot.listen).to.have.been.called describe '#catchAll', -> it 'registers a new listener using listen', -> sinon.spy @bot, 'listen' @bot.catchAll -> expect(@bot.listen).to.have.been.called describe '#receive', -> it 'calls all registered listeners', (done) -> # Need to use a real Message so that the CatchAllMessage constructor works testMessage = new TextMessage(@user, 'message123') listener = call: (response, middleware, cb) -> cb() sinon.spy(listener, 'call') @bot.listeners = [ listener listener listener listener ] @bot.receive testMessage, -> # When no listeners match, each listener is called twice: once with # the original message and once with a CatchAll message expect(listener.call).to.have.callCount(8) done() it 'sends a CatchAllMessage if no listener matches', (done) -> # Testing for recursion with a new CatchAllMessage that wraps the # original message testMessage = new TextMessage(@user, 'message123') @bot.listeners = [] # Replace @bot.receive so we can catch when the functions recurses oldReceive = @bot.receive @bot.receive = (message, cb) -> expect(message).to.be.instanceof(CatchAllMessage) expect(message.message).to.be.equal(testMessage) cb() sinon.spy(@bot, 'receive') # Call the original receive method that we want to test oldReceive.call @bot, testMessage, () => expect(@bot.receive).to.have.been.called done() it 'does not trigger a CatchAllMessage if a listener matches', (done) -> testMessage = new TextMessage(@user, 'message123') matchingListener = call: (message, middleware, cb) -> # indicate that the message matched the listener cb(true) # Replace @bot.receive so we can catch if the functions recurses oldReceive = @bot.receive @bot.receive = sinon.spy() @bot.listeners = [ matchingListener ] # Call the original receive method that we want to test oldReceive.call @bot, testMessage, done # Ensure the function did not recurse expect(@bot.receive).to.not.have.been.called it 'stops processing if a listener marks the message as done', (done) -> testMessage = new TextMessage(@user, 'message123') matchingListener = call: (message, middleware, cb) -> message.done = true # Listener must have matched cb(true) listenerSpy = call: sinon.spy() @bot.listeners = [ matchingListener listenerSpy ] @bot.receive testMessage, -> expect(listenerSpy.call).to.not.have.been.called done() it 'gracefully handles listener uncaughtExceptions (move on to next listener)', (done) -> testMessage = {} theError = new Error() badListener = call: -> throw theError goodListenerCalled = false goodListener = call: (_, middleware, cb) -> goodListenerCalled = true cb(true) @bot.listeners = [ badListener goodListener ] @bot.emit = (name, err, response) -> expect(name).to.equal('error') expect(err).to.equal(theError) expect(response.message).to.equal(testMessage) sinon.spy(@bot, 'emit') @bot.receive testMessage, () => expect(@bot.emit).to.have.been.called expect(goodListenerCalled).to.be.ok done() it 'executes the callback after the function returns when there are no listeners', (done) -> testMessage = new TextMessage @user, 'message123' finished = false @bot.receive testMessage, -> expect(finished).to.be.ok done() finished = true describe '#loadFile', -> beforeEach -> @sandbox = sinon.sandbox.create() afterEach -> @sandbox.restore() it 'should require the specified file', -> module = require 'module' script = sinon.spy (bot) -> @sandbox.stub(module, '_load').returns(script) @sandbox.stub @bot, 'parseHelp' @bot.loadFile('./scripts', 'test-script.coffee') expect(module._load).to.have.been.calledWith('scripts/test-script') describe 'proper script', -> beforeEach -> module = require 'module' @script = sinon.spy (bot) -> @sandbox.stub(module, '_load').returns(@script) @sandbox.stub @bot, 'parseHelp' it 'should call the script with the Bot', -> @bot.loadFile('./scripts', 'test-script.coffee') expect(@script).to.have.been.calledWith(@bot) it 'should parse the script documentation', -> @bot.loadFile('./scripts', 'test-script.coffee') expect(@bot.parseHelp).to.have.been.calledWith('scripts/test-script.coffee') describe 'non-Function script', -> beforeEach -> module = require 'module' @script = {} @sandbox.stub(module, '_load').returns(@script) @sandbox.stub @bot, 'parseHelp' it 'logs a warning', -> sinon.stub @bot.logger, 'warning' @bot.loadFile('./scripts', 'test-script.coffee') expect(@bot.logger.warning).to.have.been.called describe 'Listener Registration', -> describe '#listen', -> it 'forwards the matcher, options, and callback to Listener', -> callback = sinon.spy() matcher = sinon.spy() options = {} @bot.listen(matcher, options, callback) testListener = @bot.listeners[0] expect(testListener.matcher).to.equal(matcher) expect(testListener.callback).to.equal(callback) expect(testListener.options).to.equal(options) describe '#hear', -> it 'matches TextMessages', -> callback = sinon.spy() testMessage = new TextMessage(@user, 'message123') testRegex = /^message123$/ @bot.hear(testRegex, callback) testListener = @bot.listeners[0] result = testListener.matcher(testMessage) expect(result).to.be.ok it 'does not match EnterMessages', -> callback = sinon.spy() testMessage = new EnterMessage(@user) testRegex = /.*/ @bot.hear(testRegex, callback) testListener = @bot.listeners[0] result = testListener.matcher(testMessage) expect(result).to.not.be.ok describe '#respond', -> it 'matches TextMessages addressed to the bot', -> callback = sinon.spy() testMessage = new TextMessage(@user, 'TestMubot message123') testRegex = /message123$/ @bot.respond(testRegex, callback) testListener = @bot.listeners[0] result = testListener.matcher(testMessage) expect(result).to.be.ok it 'does not match EnterMessages', -> callback = sinon.spy() testMessage = new EnterMessage(@user) testRegex = /.*/ @bot.respond(testRegex, callback) testListener = @bot.listeners[0] result = testListener.matcher(testMessage) expect(result).to.not.be.ok describe '#enter', -> it 'matches EnterMessages', -> callback = sinon.spy() testMessage = new EnterMessage(@user) @bot.enter(callback) testListener = @bot.listeners[0] result = testListener.matcher(testMessage) expect(result).to.be.ok it 'does not match TextMessages', -> callback = sinon.spy() testMessage = new TextMessage(@user, 'message123') @bot.enter(callback) testListener = @bot.listeners[0] result = testListener.matcher(testMessage) expect(result).to.not.be.ok describe '#leave', -> it 'matches LeaveMessages', -> callback = sinon.spy() testMessage = new LeaveMessage(@user) @bot.leave(callback) testListener = @bot.listeners[0] result = testListener.matcher(testMessage) expect(result).to.be.ok it 'does not match TextMessages', -> callback = sinon.spy() testMessage = new TextMessage(@user, 'message123') @bot.leave(callback) testListener = @bot.listeners[0] result = testListener.matcher(testMessage) expect(result).to.not.be.ok describe '#topic', -> it 'matches TopicMessages', -> callback = sinon.spy() testMessage = new TopicMessage(@user) @bot.topic(callback) testListener = @bot.listeners[0] result = testListener.matcher(testMessage) expect(result).to.be.ok it 'does not match TextMessages', -> callback = sinon.spy() testMessage = new TextMessage(@user, 'message123') @bot.topic(callback) testListener = @bot.listeners[0] result = testListener.matcher(testMessage) expect(result).to.not.be.ok describe '#catchAll', -> it 'matches CatchAllMessages', -> callback = sinon.spy() testMessage = new CatchAllMessage(new TextMessage(@user, 'message123')) @bot.catchAll(callback) testListener = @bot.listeners[0] result = testListener.matcher(testMessage) expect(result).to.be.ok it 'does not match TextMessages', -> callback = sinon.spy() testMessage = new TextMessage(@user, 'message123') @bot.catchAll(callback) testListener = @bot.listeners[0] result = testListener.matcher(testMessage) expect(result).to.not.be.ok describe 'Message Processing', -> it 'calls a matching listener', (done) -> testMessage = new TextMessage(@user, 'message123') @bot.hear /^message123$/, (response) -> expect(response.message).to.equal(testMessage) done() @bot.receive testMessage it 'calls multiple matching listeners', (done) -> testMessage = new TextMessage(@user, 'message123') listenersCalled = 0 listenerCallback = (response) -> expect(response.message).to.equal(testMessage) listenersCalled++ @bot.hear /^message123$/, listenerCallback @bot.hear /^message123$/, listenerCallback @bot.receive testMessage, -> expect(listenersCalled).to.equal(2) done() it 'calls the catch-all listener if no listeners match', (done) -> testMessage = new TextMessage(@user, 'message123') listenerCallback = sinon.spy() @bot.hear /^no-matches$/, listenerCallback @bot.catchAll (response) -> expect(listenerCallback).to.not.have.been.called expect(response.message).to.equal(testMessage) done() @bot.receive testMessage it 'does not call the catch-all listener if any listener matched', (done) -> testMessage = new TextMessage(@user, 'message123') listenerCallback = sinon.spy() @bot.hear /^message123$/, listenerCallback catchAllCallback = sinon.spy() @bot.catchAll catchAllCallback @bot.receive testMessage, -> expect(listenerCallback).to.have.been.called.once expect(catchAllCallback).to.not.have.been.called done() it 'stops processing if message.finish() is called synchronously', (done) -> testMessage = new TextMessage(@user, 'message123') @bot.hear /^message123$/, (response) -> response.message.finish() listenerCallback = sinon.spy() @bot.hear /^message123$/, listenerCallback @bot.receive testMessage, -> expect(listenerCallback).to.not.have.been.called done() it 'calls non-TextListener objects', (done) -> testMessage = new EnterMessage @user @bot.enter (response) -> expect(response.message).to.equal(testMessage) done() @bot.receive testMessage it 'gracefully handles listener uncaughtExceptions (move on to next listener)', (done) -> testMessage = new TextMessage @user, 'message123' theError = new Error() @bot.hear /^message123$/, -> throw theError goodListenerCalled = false @bot.hear /^message123$/, -> goodListenerCalled = true [badListener,goodListener] = @bot.listeners @bot.emit = (name, err, response) -> expect(name).to.equal('error') expect(err).to.equal(theError) expect(response.message).to.equal(testMessage) sinon.spy(@bot, 'emit') @bot.receive testMessage, () => expect(@bot.emit).to.have.been.called expect(goodListenerCalled).to.be.ok done() describe 'Listener Middleware', -> it 'allows listener callback execution', (testDone) -> listenerCallback = sinon.spy() @bot.hear /^message123$/, listenerCallback @bot.listenerMiddleware (context, next, done) -> # Allow Listener callback execution next done testMessage = new TextMessage @user, 'message123' @bot.receive testMessage, -> expect(listenerCallback).to.have.been.called testDone() it 'can block listener callback execution', (testDone) -> listenerCallback = sinon.spy() @bot.hear /^message123$/, listenerCallback @bot.listenerMiddleware (context, next, done) -> # Block Listener callback execution done() testMessage = new TextMessage @user, 'message123' @bot.receive testMessage, -> expect(listenerCallback).to.not.have.been.called testDone() it 'receives the correct arguments', (testDone) -> @bot.hear /^message123$/, -> testListener = @bot.listeners[0] testMessage = new TextMessage @user, 'message123' @bot.listenerMiddleware (context, next, done) => # Escape middleware error handling for clearer test failures process.nextTick () => expect(context.listener).to.equal(testListener) expect(context.response.message).to.equal(testMessage) expect(next).to.be.a('function') expect(done).to.be.a('function') testDone() @bot.receive testMessage it 'executes middleware in order of definition', (testDone) -> execution = [] testMiddlewareA = (context, next, done) -> execution.push 'middlewareA' next -> execution.push 'doneA' done() testMiddlewareB = (context, next, done) -> execution.push 'middlewareB' next -> execution.push 'doneB' done() @bot.listenerMiddleware testMiddlewareA @bot.listenerMiddleware testMiddlewareB @bot.hear /^message123$/, -> execution.push 'listener' testMessage = new TextMessage @user, 'message123' @bot.receive testMessage, -> expect(execution).to.deep.equal([ 'middlewareA' 'middlewareB' 'listener' 'doneB' 'doneA' ]) testDone() describe 'Receive Middleware', -> it 'fires for all messages, including non-matching ones', (testDone) -> middlewareSpy = sinon.spy() listenerCallback = sinon.spy() @bot.hear /^message123$/, listenerCallback @bot.receiveMiddleware (context, next, done) -> middlewareSpy() next(done) testMessage = new TextMessage @user, 'not message 123' @bot.receive testMessage, () -> expect(listenerCallback).to.not.have.been.called expect(middlewareSpy).to.have.been.called testDone() it 'can block listener execution', (testDone) -> middlewareSpy = sinon.spy() listenerCallback = sinon.spy() @bot.hear /^message123$/, listenerCallback @bot.receiveMiddleware (context, next, done) -> # Block Listener callback execution middlewareSpy() done() testMessage = new TextMessage @user, 'message123' @bot.receive testMessage, () -> expect(listenerCallback).to.not.have.been.called expect(middlewareSpy).to.have.been.called testDone() it 'receives the correct arguments', (testDone) -> @bot.hear /^message123$/, () -> testMessage = new TextMessage @user, 'message123' @bot.receiveMiddleware (context, next, done) -> # Escape middleware error handling for clearer test failures expect(context.response.message).to.equal(testMessage) expect(next).to.be.a('function') expect(done).to.be.a('function') testDone() next(done) @bot.receive testMessage it 'executes receive middleware in order of definition', (testDone) -> execution = [] testMiddlewareA = (context, next, done) -> execution.push 'middlewareA' next () -> execution.push 'doneA' done() testMiddlewareB = (context, next, done) -> execution.push 'middlewareB' next () -> execution.push 'doneB' done() @bot.receiveMiddleware testMiddlewareA @bot.receiveMiddleware testMiddlewareB @bot.hear /^message123$/, () -> execution.push 'listener' testMessage = new TextMessage @user, 'message123' @bot.receive testMessage, () -> expect(execution).to.deep.equal([ 'middlewareA' 'middlewareB' 'listener' 'doneB' 'doneA' ]) testDone() it 'allows editing the message portion of the given response', (testDone) -> execution = [] testMiddlewareA = (context, next, done) -> context.response.message.text = 'foobar' next() testMiddlewareB = (context, next, done) -> # Subsequent middleware should see the modified message expect(context.response.message.text).to.equal("foobar") next() @bot.receiveMiddleware testMiddlewareA @bot.receiveMiddleware testMiddlewareB testCallback = sinon.spy() # We'll never get to this if testMiddlewareA has not modified the message. @bot.hear /^foobar$/, testCallback testMessage = new TextMessage @user, 'message123' @bot.receive testMessage, -> expect(testCallback).to.have.been.called testDone() describe 'Response Middleware', -> it 'executes response middleware in order', (testDone) -> @bot.adapter.send = sendSpy = sinon.spy() listenerCallback = sinon.spy() @bot.hear /^message123$/, (response) -> response.send "foobar, sir, foobar." @bot.responseMiddleware (context, next, done) -> context.strings[0] = context.strings[0].replace(/foobar/g, "barfoo") next() @bot.responseMiddleware (context, next, done) -> context.strings[0] = context.strings[0].replace(/barfoo/g, "replaced bar-foo") next() testMessage = new TextMessage @user, 'message123' @bot.receive testMessage, () -> expect(sendSpy.getCall(0).args[1]).to.equal('replaced bar-foo, sir, replaced bar-foo.') testDone() it 'allows replacing outgoing strings', (testDone) -> @bot.adapter.send = sendSpy = sinon.spy() listenerCallback = sinon.spy() @bot.hear /^message123$/, (response) -> response.send "foobar, sir, foobar." @bot.responseMiddleware (context, next, done) -> context.strings = ["whatever I want."] next() testMessage = new TextMessage @user, 'message123' @bot.receive testMessage, () -> expect(sendSpy.getCall(0).args[1]).to.deep.equal("whatever I want.") testDone() it 'marks plaintext as plaintext', (testDone) -> @bot.adapter.send = sendSpy = sinon.spy() listenerCallback = sinon.spy() @bot.hear /^message123$/, (response) -> response.send "foobar, sir, foobar." @bot.hear /^message456$/, (response) -> response.play "good luck with that" method = undefined plaintext = undefined @bot.responseMiddleware (context, next, done) -> method = context.method plaintext = context.plaintext next(done) testMessage = new TextMessage @user, 'message123' @bot.receive testMessage, () => expect(plaintext).to.equal true expect(method).to.equal "send" testMessage2 = new TextMessage @user, 'message456' @bot.receive testMessage2, () -> expect(plaintext).to.equal undefined expect(method).to.equal "play" testDone() it 'does not send trailing functions to middleware', (testDone) -> @bot.adapter.send = sendSpy = sinon.spy() asserted = false postSendCallback = -> @bot.hear /^message123$/, (response) -> response.send "foobar, sir, foobar.", postSendCallback @bot.responseMiddleware (context, next, done) -> # We don't send the callback function to middleware, so it's not here. expect(context.strings).to.deep.equal ["foobar, sir, foobar."] expect(context.method).to.equal "send" asserted = true next() testMessage = new TextMessage @user, 'message123' @bot.receive testMessage, -> expect(asserted).to.equal(true) expect(sendSpy.getCall(0).args[1]).to.equal('foobar, sir, foobar.') expect(sendSpy.getCall(0).args[2]).to.equal(postSendCallback) testDone()