├── .gitignore ├── .mise.toml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── cognition.gemspec ├── lib ├── cognition.rb └── cognition │ ├── bot.rb │ ├── matcher.rb │ ├── message.rb │ ├── plugins │ ├── base.rb │ ├── cognition │ │ └── plugins │ │ │ └── default │ │ │ └── views │ │ │ ├── help.html.erb │ │ │ └── help.text.erb │ └── default.rb │ ├── responder.rb │ └── version.rb └── test ├── fixtures ├── anchor.rb ├── hello.rb └── hello │ └── views │ ├── bonjour.text.erb │ ├── hey.html.haml │ ├── hi.html.erb │ ├── hola.haml │ └── yo.html.erb ├── test_cognition.rb ├── test_default_plugin.rb ├── test_helper.rb ├── test_matcher.rb ├── test_message.rb ├── test_plugin.rb └── test_responder.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | *.gem 16 | -------------------------------------------------------------------------------- /.mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | ruby = "2.7" 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "byebug" 4 | 5 | # Specify your gem's dependencies in Cognition.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Nathan Anderson 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cognition 2 | 3 | This is a gem that parses a message, and compares it to various matchers. 4 | When it finds the **first match**, it executes an associated block of code or 5 | method, returning the output of whatever was run. 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | gem 'cognition' 13 | ``` 14 | 15 | And then execute: 16 | 17 | $ bundle 18 | 19 | Or install it yourself as: 20 | 21 | $ gem install cognition 22 | 23 | ## Usage 24 | 25 | Instantiate: 26 | ```ruby 27 | bot = Cognition::Bot.new 28 | ``` 29 | 30 | Process your message: 31 | ```ruby 32 | result = bot.process('command I need to process') 33 | ``` 34 | 35 | You can also include metadata with your message, like user info, or whatever: 36 | ```ruby 37 | result = bot.process('another command', {user_id: 15, name: 'Bob'}) 38 | ``` 39 | 40 | Internally, `Cognition` will turn your values into a `Cognition::Message` so 41 | the metadata will be passed along with the message, and arbitrary metadata 42 | is available in the #metadata Hash: 43 | ```ruby 44 | msg = Cognition::Message('another command', {user_id: 15, name: 'Bob'}) 45 | msg.metadata # Returns { user_id: 15, name: 'Bob' } 46 | ``` 47 | 48 | ### Special metadata 49 | If you include a `callback_url` key in your metadata, we'll give you a 50 | convenience method to reply to it using the `reply` method. This will 51 | invoke a HTTParty POST back to the URL with your text sent as the 52 | `content` variable. 53 | ```ruby 54 | msg = Cognition::Message('another command', { 55 | callback_url: "http://foo.bar/baz", 56 | user_id: 15, 57 | name: 'Bob' 58 | }) 59 | 60 | msg.reply("foo") # Posts 'content=foo' to http://foo.bar/baz 61 | ``` 62 | 63 | ## Creating a Plugin 64 | Creating plugins is easy. Subclass `Cognition::Plugins::Base` and setup your 65 | matches and logic that should be run. Your methods will be passed the `msg` 66 | object and any `metadata` you've passed to the bot's `process` method. Here's 67 | an example plugin: 68 | ```ruby 69 | class Hello < Cognition::Plugins::Base 70 | # Simple string based matcher. Must match *EXACTLY* 71 | match 'hello', :hello, help: { 'hello' => 'Returns Hello World' } 72 | 73 | # Advanced Regexp based matcher. Capture groups are made available 74 | # via MatchData in the matches method 75 | match /hello\s*(?.*)/, :hello_person, help: { 76 | 'hello ' => 'Greets you by name!' 77 | } 78 | 79 | def hello(*) 80 | 'Hello World' 81 | end 82 | 83 | def hello_person(msg, match_data = nil) 84 | name = match_data[:name] 85 | "Hello #{name}" 86 | end 87 | end 88 | ``` 89 | 90 | After you've done that, you will be able to do: 91 | ```ruby 92 | bot.register(Hello) 93 | bot.process("help hello") # "hello - Greets you by name!" 94 | bot.process("hello") # "Hello World" 95 | bot.process("hello foo") # "Hello foo" 96 | ``` 97 | 98 | ### Rendering templates 99 | Templates are opt-in right now, you need to call `render` yourself, and it 100 | will return a string with the rendered contents of a template. What template, 101 | you ask? The default for `/path/to/hello.rb` will look for a templates in 102 | `/path/to/hello/views/`. 103 | 104 | Given the following plugin: 105 | ```ruby 106 | class Hello < Cognition::Plugins::Base 107 | # ...snipped 108 | 109 | def hello(*) 110 | render 111 | end 112 | 113 | def hi(*) 114 | render(template: "/path/to/template.html.erb") 115 | end 116 | 117 | def hey(*) 118 | render(type: "text", extension: "haml") 119 | end 120 | end 121 | ``` 122 | 123 | 1. The `hello` method will look for `/path/to/hello/views/hello.html.erb` 124 | 2. The `hi` method will look for `/path/to/template.html.erb` 125 | 3. The `hey` method will look for `/path/to/hello/views/hey.text.haml` 126 | 127 | Setting instance variables or passing locals is up to the plugin creator. 128 | The `render` method takes a hash with the following keys: 129 | ```ruby 130 | { 131 | template: "full/path/to/template/file", # FULL path to template file 132 | type: "type of response" # text, html, json, etc... 133 | extension: "engine file extension" # erb, haml, etc... 134 | locals: {x: "foo", y: "bar"} # local variables, access as x & y 135 | } 136 | ``` 137 | 138 | ## Contributing 139 | 140 | 1. Fork it ( https://github.com/anoldguy/cognition/fork ) 141 | 2. Create your feature branch (`git checkout -b my-new-feature`) 142 | 3. Commit your changes (`git commit -am 'Add some feature'`) 143 | 4. Push to the branch (`git push origin my-new-feature`) 144 | 5. Create a new Pull Request 145 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new do |t| 5 | t.libs << "test" 6 | end 7 | 8 | desc "Run tests" 9 | task :default => :test 10 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "cognition" 5 | 6 | puts "Get started with your Cognition bot by registering the 'Hello' test plugin!" 7 | puts "> test_bot = Cognition::Bot.new" 8 | puts '> require_relative "../test/fixtures/hello"' 9 | puts "> test_bot.register(Hello)\n\n" 10 | puts "Process commands with: " 11 | puts "> test_bot.process('message')\n\n" 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | -------------------------------------------------------------------------------- /cognition.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "cognition/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "cognition" 8 | spec.version = Cognition::VERSION 9 | spec.authors = ["Nathan Anderson"] 10 | spec.email = ["andnat@gmail.com"] 11 | spec.summary = "A rules engine for running commands." 12 | spec.description = "Match text; run commands. Works great for building a chatbot!" 13 | spec.homepage = "https://github.com/basecamp/cognition" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_dependency "httparty", "~> 0.15" 22 | spec.add_dependency "tilt", "~> 2.0" 23 | 24 | spec.add_development_dependency "bundler", "~> 1.7" 25 | spec.add_development_dependency "rake", "~> 10.0" 26 | spec.add_development_dependency "minitest", "~> 5.0" 27 | spec.add_development_dependency "webmock", "~> 1.20" 28 | spec.add_development_dependency "haml", "~> 5.0" 29 | end 30 | -------------------------------------------------------------------------------- /lib/cognition.rb: -------------------------------------------------------------------------------- 1 | require "cognition/version" 2 | require "cognition/bot" 3 | 4 | module Cognition 5 | end 6 | -------------------------------------------------------------------------------- /lib/cognition/bot.rb: -------------------------------------------------------------------------------- 1 | require "cognition/message" 2 | require "cognition/matcher" 3 | require "cognition/responder" 4 | require "cognition/plugins/base" 5 | require "cognition/plugins/default" 6 | 7 | module Cognition 8 | class Bot 9 | attr_accessor :plugins, :matchers 10 | 11 | def initialize 12 | # Default plugin, responds to "ping" with "PONG" and provides help text 13 | register(Cognition::Plugins::Default) 14 | end 15 | 16 | def process(msg, metadata = {}) 17 | if msg.respond_to? :command 18 | process_msg(msg) 19 | else 20 | process_string(msg.to_s, metadata) 21 | end 22 | end 23 | 24 | def register(klass) 25 | return false if plugin_names.include? klass.to_s 26 | plugins << klass.new(self) 27 | end 28 | 29 | def reset 30 | @matchers = [] 31 | @plugins = [] 32 | register(Cognition::Plugins::Default) 33 | end 34 | 35 | def plugin_names 36 | plugins.map { |p| p.class.name } 37 | end 38 | 39 | def help 40 | matchers.flat_map(&:help) 41 | end 42 | 43 | private 44 | 45 | def process_msg(msg) 46 | response = false 47 | matchers.each do |matcher| 48 | if matcher.attempt(msg) != false 49 | response = matcher.response 50 | break 51 | end 52 | end 53 | response ? response : not_found(msg.command) 54 | end 55 | 56 | def process_string(message, metadata = {}) 57 | process_msg(Cognition::Message.new(message.strip, metadata)) 58 | end 59 | 60 | def matchers 61 | plugins.flat_map(&:matchers).compact 62 | end 63 | 64 | def plugins 65 | @plugins ||= [] 66 | end 67 | 68 | def not_found(message) 69 | "No such command: #{message}\nUse 'help' for available commands!" 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/cognition/matcher.rb: -------------------------------------------------------------------------------- 1 | module Cognition 2 | class Matcher 3 | attr_reader :trigger, :action, :response, :help, :match_data 4 | 5 | def initialize(trigger, options = {}, &action) 6 | fail ArgumentError, "matcher must have a trigger" unless trigger 7 | fail ArgumentError, "matcher must have an action" unless action 8 | @trigger = trigger 9 | @help = options[:help] ||= {} 10 | @action = action 11 | end 12 | 13 | def help 14 | @help.map do |command, description| 15 | "#{command} - #{description}" 16 | end 17 | end 18 | 19 | def attempt(msg) 20 | return false unless matches?(msg) 21 | run(msg) 22 | end 23 | 24 | def run(msg) 25 | @response = action.call(msg, match_data).to_s 26 | rescue => e 27 | @response = "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}" 28 | end 29 | 30 | def matches?(msg) 31 | case trigger 32 | when String 33 | trigger == msg.command 34 | when Regexp 35 | @match_data = trigger.match msg.command 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/cognition/message.rb: -------------------------------------------------------------------------------- 1 | module Cognition 2 | class Message 3 | attr_reader :command, :filter, :metadata, :responder 4 | 5 | def initialize(command, metadata = {}) 6 | @command, @filter = command.split("|").map(&:strip) 7 | @metadata = metadata 8 | @responder = Cognition::Responder.new(metadata["callback_url"]) if metadata["callback_url"] 9 | end 10 | 11 | def reply(text) 12 | return "No Callback URL provided" unless @responder 13 | @responder.reply(text) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/cognition/plugins/base.rb: -------------------------------------------------------------------------------- 1 | require "erb" 2 | require "tilt" 3 | require "cgi" 4 | 5 | module Cognition 6 | module Plugins 7 | class PluginTemplateNotFound < StandardError; end 8 | 9 | class Base 10 | class <= 3.4.1: test.rb:9:in 'A#c' 50 | calling_method = caller[0][/[`'](?:.*#)?([^']*)'/, 1] 51 | template = options[:template] || template_file(calling_method, options[:type], options[:extension]) 52 | filter Tilt.new(template).render(self, options[:locals]) 53 | rescue Errno::ENOENT => e 54 | raise PluginTemplateNotFound, e 55 | end 56 | 57 | private 58 | def template_file(name, type, extension) 59 | # Defaults to html ERB for now. Override when calling #render(path_to_file) 60 | File.join(templates_path, "#{name}.#{type}.#{extension}") 61 | end 62 | 63 | def templates_path 64 | File.expand_path("#{underscore(self.class.name)}/views", self.class.view_root) 65 | end 66 | 67 | def underscore(string) 68 | word = string.to_s.gsub(/::/, "/") 69 | word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, "\\1_\\2") 70 | word.gsub!(/([a-z\d])([A-Z])/, "\\1_\\2") 71 | word.tr!("-", "_") 72 | word.downcase! 73 | word 74 | end 75 | 76 | def filter(full_response) 77 | filter = @msg&.filter 78 | return full_response unless filter 79 | 80 | filter = filter.sub(/^grep\s*/, "") 81 | 82 | full_response.each_line.select do |line| 83 | line.downcase.include?(filter.downcase) || 84 | line.downcase.include?("ul>") # Don't strip wrapping ul tags 85 | end.join 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/cognition/plugins/cognition/plugins/default/views/help.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | <% @help.each do |text| %> 3 |
  • <%= CGI.escapeHTML(text) %>
  • 4 | <% end %> 5 |
6 | -------------------------------------------------------------------------------- /lib/cognition/plugins/cognition/plugins/default/views/help.text.erb: -------------------------------------------------------------------------------- 1 | <%= @help.join("\n") %> 2 | -------------------------------------------------------------------------------- /lib/cognition/plugins/default.rb: -------------------------------------------------------------------------------- 1 | module Cognition 2 | module Plugins 3 | class Default < Cognition::Plugins::Base 4 | # rubocop:disable Lint/AmbiguousRegexpLiteral 5 | match /^ping$/i, :pong, help: { 6 | "ping" => "Test if the endpoint is responding. Returns PONG." 7 | } 8 | 9 | match /^help\s*(?.*)/i, :help, help: { 10 | "help" => "Lists all commands with help", 11 | "help " => "Lists help for " 12 | } 13 | # rubocop:enable Lint/AmbiguousRegexpLiteral 14 | 15 | def pong(*) 16 | "PONG" 17 | end 18 | 19 | def help(msg, match_data = nil) 20 | type = msg.metadata[:type] ||= "text" 21 | if match_data[:command].nil? || match_data[:command].empty? 22 | @help = bot.help.sort 23 | else 24 | @help = bot.help.find_all { |text| text.match match_data[:command] } 25 | end 26 | render(type: type) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/cognition/responder.rb: -------------------------------------------------------------------------------- 1 | require "httparty" 2 | module Cognition 3 | class Responder 4 | include HTTParty 5 | attr_reader :url 6 | 7 | def initialize(uri) 8 | @options = { timeout: 5 } 9 | @uri = uri 10 | end 11 | 12 | def reply(text) 13 | self.class.post(@uri, @options.merge(body: { content: text })) 14 | rescue Timeout::Error 15 | "Request to #{@uri} timed out." 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/cognition/version.rb: -------------------------------------------------------------------------------- 1 | module Cognition 2 | VERSION = "2.1.3" 3 | end 4 | -------------------------------------------------------------------------------- /test/fixtures/anchor.rb: -------------------------------------------------------------------------------- 1 | require "tilt/haml" 2 | require "tilt/erb" 3 | require "haml" 4 | class Anchor < Cognition::Plugins::Base 5 | match "ping me", :anchored_ping, help: { 6 | "ping me" => "Returns 'OK'" 7 | } 8 | 9 | def anchored_ping(*) 10 | "OK" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/fixtures/hello.rb: -------------------------------------------------------------------------------- 1 | require "tilt/haml" 2 | require "tilt/erb" 3 | require "haml" 4 | class Hello < Cognition::Plugins::Base 5 | match "hello", :hello, help: { 6 | "hello" => "Returns Hello World" 7 | } 8 | 9 | def hello(*) 10 | "Hello World" 11 | end 12 | 13 | def hi(*) 14 | @message = "Hi" 15 | render 16 | end 17 | 18 | def hola(*) 19 | @message = "Hola" 20 | render(template: File.expand_path("../hello/views/hola.haml", __FILE__)) 21 | end 22 | 23 | def bonjour(*) 24 | @message = "Bonjour" 25 | render(type: "text") 26 | end 27 | 28 | def hey(*) 29 | @message = "Hey" 30 | render(extension: "haml") 31 | end 32 | 33 | def yo(*) 34 | @message = "Yo" 35 | render(locals: { num_yos: 3 }) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/fixtures/hello/views/bonjour.text.erb: -------------------------------------------------------------------------------- 1 | <%= @message %> is french! 2 | -------------------------------------------------------------------------------- /test/fixtures/hello/views/hey.html.haml: -------------------------------------------------------------------------------- 1 | %h1= @message 2 | -------------------------------------------------------------------------------- /test/fixtures/hello/views/hi.html.erb: -------------------------------------------------------------------------------- 1 | <%= @message %> from a default template! 2 | -------------------------------------------------------------------------------- /test/fixtures/hello/views/hola.haml: -------------------------------------------------------------------------------- 1 | %h1= @message -------------------------------------------------------------------------------- /test/fixtures/hello/views/yo.html.erb: -------------------------------------------------------------------------------- 1 | <% num_yos.times do %> 2 |

<%= @message %>

3 | <% end %> 4 | -------------------------------------------------------------------------------- /test/test_cognition.rb: -------------------------------------------------------------------------------- 1 | require "minitest/autorun" 2 | require "cognition" 3 | require_relative "fixtures/hello" 4 | require_relative "fixtures/anchor" 5 | 6 | class CognitionTest < Minitest::Test 7 | def setup 8 | @bot = Cognition::Bot.new 9 | end 10 | 11 | def test_registers_plugins 12 | @bot.register(Hello) 13 | 14 | assert_equal 2, @bot.plugin_names.count 15 | end 16 | 17 | def test_does_not_register_duplicate_plugins 18 | @bot.register(Hello) 19 | @bot.register(Hello) 20 | 21 | assert_equal 2, @bot.plugin_names.count 22 | end 23 | 24 | def test_processes_messages 25 | msg = Cognition::Message.new("ping") 26 | assert_equal "PONG", @bot.process(msg) 27 | end 28 | 29 | def test_processes_strings 30 | assert_equal "PONG", @bot.process("ping") 31 | end 32 | 33 | def test_processes_strings_with_metadata 34 | assert_equal "PONG", @bot.process("ping", foo: "bar") 35 | end 36 | 37 | def test_shows_help_if_no_matches 38 | @bot.register(Hello) 39 | msg = Cognition::Message.new("pong") 40 | output = @bot.process(msg) 41 | assert_match "No such command: pong\nUse 'help' for available commands!", output 42 | end 43 | 44 | def test_anchored_ping 45 | @bot.register(Anchor) 46 | 47 | assert_equal 'OK', @bot.process("ping me") 48 | end 49 | 50 | def test_grep 51 | msg = Cognition::Message.new("help | grep endpoint") 52 | output = @bot.process(msg) 53 | assert_equal "ping - Test if the endpoint is responding. Returns PONG.", output 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/test_default_plugin.rb: -------------------------------------------------------------------------------- 1 | require "minitest/autorun" 2 | require "cognition" 3 | 4 | class DefaultPluginTest < Minitest::Test 5 | def setup 6 | @bot = Cognition::Bot.new 7 | end 8 | 9 | def test_returns_help 10 | help = "help - Lists all commands with help\nhelp - Lists help for \nping - Test if the endpoint is responding. Returns PONG." 11 | assert_equal help, @bot.process("help") 12 | end 13 | 14 | def test_returns_filtered_help 15 | help = "ping - Test if the endpoint is responding. Returns PONG." 16 | assert_equal help, @bot.process("help ping") 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "webmock/minitest" 2 | 3 | module Minitest 4 | class Test 5 | include WebMock::API 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/test_matcher.rb: -------------------------------------------------------------------------------- 1 | require "minitest/autorun" 2 | require "cognition" 3 | 4 | class MatcherTest < Minitest::Test 5 | def test_raises_error_without_a_trigger 6 | assert_raises ArgumentError do 7 | _ = Cognition::Matcher.new(action: "foo") 8 | end 9 | end 10 | 11 | def test_raises_error_without_an_action 12 | assert_raises ArgumentError do 13 | _ = Cognition::Matcher.new(trigger: "foo") 14 | end 15 | end 16 | 17 | def test_matches_string 18 | msg = Cognition::Message.new("help") 19 | matcher = Cognition::Matcher.new("help", {}, &proc {}) 20 | assert matcher.matches?(msg) 21 | end 22 | 23 | def test_string_fails_with_invalid_message 24 | msg = Cognition::Message.new("Help") 25 | matcher = Cognition::Matcher.new("help", {}, &proc {}) 26 | refute matcher.matches?(msg) 27 | end 28 | 29 | def test_matches_regexp 30 | msg = Cognition::Message.new("ping") 31 | matcher = Cognition::Matcher.new(/ping/, {}, &proc {}) 32 | assert matcher.matches?(msg) 33 | end 34 | 35 | def test_regexp_fails_with_invalid_message 36 | msg = Cognition::Message.new("pink") 37 | matcher = Cognition::Matcher.new(/ping/, {}, &proc {}) 38 | refute matcher.matches?(msg) 39 | end 40 | 41 | def test_sets_response_on_attemp_if_matches 42 | msg = Cognition::Message.new("ping") 43 | matcher = Cognition::Matcher.new(/ping/, {}, &proc { "PONG" }) 44 | matcher.attempt(msg) 45 | assert_equal "PONG", matcher.response 46 | end 47 | 48 | def test_returns_false_on_attemp_if_no_match 49 | msg = Cognition::Message.new("pink") 50 | matcher = Cognition::Matcher.new(/ping/, {}, &proc { "PONG" }) 51 | refute matcher.attempt(msg) 52 | end 53 | 54 | def test_sets_match_data 55 | msg = Cognition::Message.new("hello john") 56 | matcher = Cognition::Matcher.new(/hello\s*(?.*)/, {}, &proc { "PONG" }) 57 | matcher.matches?(msg) 58 | assert_equal "john", matcher.match_data[:name] 59 | end 60 | 61 | def test_captures_response 62 | msg = Cognition::Message.new("hello john") 63 | matcher = Cognition::Matcher.new(/hello\s*(?.*)/, {}, &proc(&method(:dummy_method))) 64 | matcher.attempt(msg) 65 | assert_equal "Hello john", matcher.response 66 | end 67 | 68 | def test_only_sets_help_when_help_provided 69 | matcher_without_help = Cognition::Matcher.new(/hello\s*(?.*)/, {}, &proc(&method(:dummy_method))) 70 | assert_equal [], matcher_without_help.help 71 | end 72 | 73 | def test_sets_help 74 | matcher_with_help = Cognition::Matcher.new(/hello\s*(?.*)/, { help: { "hello" => "says hello" } }, &proc(&method(:dummy_method))) 75 | assert_equal ["hello - says hello"], matcher_with_help.help 76 | end 77 | 78 | def test_raises_exception_when_command_fails 79 | msg = Cognition::Message.new("boom") 80 | matcher = Cognition::Matcher.new(/boom/, {}, &proc(&method(:blow_up))) 81 | matcher.attempt(msg) 82 | assert matcher.response.include? "ZeroDivisionError" 83 | end 84 | 85 | def test_empty_response_is_not_a_failure 86 | msg = Cognition::Message.new("empty") 87 | matcher = Cognition::Matcher.new(/empty/, {}, &proc {}) 88 | matcher.attempt(msg) 89 | assert_equal "", matcher.response 90 | end 91 | end 92 | 93 | def dummy_method(_, match_data) 94 | "Hello #{match_data['name']}" 95 | end 96 | 97 | def blow_up(msg, match_data) 98 | raise ZeroDivisionError, "divided by 0" 99 | end -------------------------------------------------------------------------------- /test/test_message.rb: -------------------------------------------------------------------------------- 1 | require "minitest/autorun" 2 | require "cognition" 3 | 4 | class CognitionTest < Minitest::Test 5 | def test_sets_metadata 6 | msg = Cognition::Message.new("test", user_id: 15, foo: "bar") 7 | assert_equal 15, msg.metadata[:user_id] 8 | assert_equal "bar", msg.metadata[:foo] 9 | end 10 | 11 | def test_sets_responder_if_callback_url 12 | msg = Cognition::Message.new("ping", "callback_url" => "http://foo.bar/") 13 | assert_kind_of Cognition::Responder, msg.responder 14 | end 15 | 16 | def test_no_responder_if_no_callback_url 17 | msg = Cognition::Message.new("ping", user: { name: "foo", id: 123_456 }) 18 | refute msg.responder 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/test_plugin.rb: -------------------------------------------------------------------------------- 1 | require "minitest/autorun" 2 | require "cognition" 3 | 4 | class PluginTest < Minitest::Test 5 | def setup 6 | require_relative "fixtures/hello" 7 | bot = Cognition::Bot.new 8 | @hello = Hello.new(bot) 9 | end 10 | 11 | def test_sets_matchers 12 | assert_equal 1, @hello.matchers.count 13 | end 14 | 15 | def test_renders_default_template 16 | assert_equal "Hi from a default template!\n", @hello.hi("foo") 17 | end 18 | 19 | def test_renders_passed_template 20 | assert_equal "

Hola

\n", @hello.hola("foo") 21 | end 22 | 23 | def test_renders_specified_type 24 | assert_equal "Bonjour is french!\n", @hello.bonjour("foo") 25 | end 26 | 27 | def test_renders_specified_extension 28 | assert_equal "

Hey

\n", @hello.hey("foo") 29 | end 30 | 31 | def test_renders_with_locals 32 | assert_equal "

Yo

\n

Yo

\n

Yo

\n", @hello.yo("foo") 33 | end 34 | 35 | def test_multiple_render_calls 36 | assert_equal "

Yo

\n

Yo

\n

Yo

\n", @hello.yo("foo") 37 | assert_equal "

Hey

\n", @hello.hey("foo") 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/test_responder.rb: -------------------------------------------------------------------------------- 1 | require "minitest/autorun" 2 | require "cognition" 3 | require "test_helper" 4 | 5 | class ResponderTest < Minitest::Test 6 | def test_sends_reply 7 | stub_request(:any, "http://foo.bar/path") 8 | .to_return(status: 200) 9 | responder = Cognition::Responder.new("http://foo.bar/path") 10 | 11 | assert_equal 200, responder.reply("foobar").code 12 | end 13 | 14 | def test_handles_timeouts 15 | stub_request(:any, "http://foo.bar/path").to_timeout 16 | responder = Cognition::Responder.new("http://foo.bar/path") 17 | 18 | assert_equal "Request to http://foo.bar/path timed out.", responder.reply("foobar") 19 | end 20 | end 21 | --------------------------------------------------------------------------------