lita 1.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,67 +1,40 @@
1
+ begin
2
+ require "rspec"
3
+ rescue LoadError
4
+ abort "Lita::RSpec requires both RSpec::Mocks and RSpec::Expectations."
5
+ end
6
+
7
+ major, minor, patch, *pre = RSpec::Mocks::Version::STRING.split(/\./)
8
+ if major == "2" && minor.to_i < 14
9
+ abort "RSpec::Mocks 2.14 or greater is required to use Lita::RSpec."
10
+ end
11
+
12
+ require "lita/rspec/handler"
13
+
1
14
  module Lita
15
+ # Extras for +RSpec+ that facilitate the testing of Lita code.
2
16
  module RSpec
17
+ # Causes all interaction with Redis to use a test-specific namespace. Clears
18
+ # Redis before each example. Stubs the logger to prevent log messages from
19
+ # cluttering test output. Clears Lita's global configuration.
20
+ # @param base [Object] The class including the module.
21
+ # @return [void]
3
22
  def self.included(base)
4
23
  base.class_eval do
5
- let(:robot) { Robot.new }
6
- let(:source) { Source.new(user) }
7
- let(:user) { User.new("1", name: "Test User") }
8
-
9
24
  before do
10
- allow(Lita).to receive(:handlers).and_return([described_class])
11
25
  stub_const("Lita::REDIS_NAMESPACE", "lita.test")
12
26
  keys = Lita.redis.keys("*")
13
27
  Lita.redis.del(keys) unless keys.empty?
14
- allow(robot).to receive(:send_messages)
28
+ logger = double("Logger").as_null_object
29
+ allow(Lita).to receive(:logger).and_return(logger)
30
+ Lita.clear_config
15
31
  end
16
32
  end
17
33
  end
18
-
19
- def expect_reply(*arguments, invert: false)
20
- method = invert ? :not_to : :to
21
- expect(robot).public_send(
22
- method,
23
- receive(:send_messages).with(source, *arguments)
24
- )
25
- end
26
- alias_method :expect_replies, :expect_reply
27
-
28
- def expect_no_reply(*arguments)
29
- expect_reply(*arguments, invert: true)
30
- end
31
- alias_method :expect_no_replies, :expect_no_reply
32
-
33
- def send_test_message(body)
34
- message = Message.new(robot, body, source)
35
- robot.receive(message)
36
- end
37
-
38
- def routes(message)
39
- RouteMatcher.new(self, message)
40
- end
41
-
42
- def does_not_route(message)
43
- RouteMatcher.new(self, message, invert: true)
44
- end
45
- alias_method :doesnt_route, :does_not_route
46
- end
47
-
48
- class RouteMatcher
49
- def initialize(context, message_body, invert: false)
50
- @context = context
51
- @message_body = message_body
52
- @method = invert ? :not_to : :to
53
- end
54
-
55
- def to(route)
56
- @context.expect_any_instance_of(
57
- @context.described_class
58
- ).public_send(@method, @context.receive(route))
59
-
60
- @context.send_test_message(@message_body)
61
- end
62
34
  end
63
35
  end
64
36
 
65
37
  RSpec.configure do |config|
66
38
  config.include Lita::RSpec, lita: true
39
+ config.include Lita::RSpec::Handler, lita_handler: true
67
40
  end
@@ -0,0 +1,168 @@
1
+ module Lita
2
+ module RSpec
3
+ # Extras for +RSpec+ to facilitate testing Lita handlers.
4
+ module Handler
5
+ # Sets up the RSpec environment to easily test Lita handlers.
6
+ def self.included(base)
7
+ base.class_eval do
8
+ include Lita::RSpec
9
+
10
+ let(:robot) { Robot.new }
11
+ let(:source) { Source.new(user) }
12
+ let(:user) { User.create("1", name: "Test User") }
13
+ let(:replies) { [] }
14
+
15
+ subject { described_class.new(robot) }
16
+
17
+ before do
18
+ allow(Lita).to receive(:handlers).and_return([described_class])
19
+ allow(robot).to receive(:send_messages) do |target, *strings|
20
+ replies.concat(strings)
21
+ end
22
+ allow(robot).to receive(:send_message) do |target, *strings|
23
+ replies.concat(strings)
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ # Sends a message to the robot.
30
+ # @param body [String] The message to send.
31
+ # @param as [Lita::User] The user sending the message.
32
+ # @return [void]
33
+ def send_message(body, as: user)
34
+ message = if as == user
35
+ Message.new(robot, body, source)
36
+ else
37
+ Message.new(robot, body, Source.new(as))
38
+ end
39
+
40
+ robot.receive(message)
41
+ end
42
+
43
+ # Sends a "command" message to the robot.
44
+ # @param body [String] The message to send.
45
+ # @param as [Lita::User] The user sending the message.
46
+ # @return [void]
47
+ def send_command(body, as: user)
48
+ send_message("#{robot.mention_name}: #{body}", as: as)
49
+ end
50
+
51
+ # Starts a chat routing test chain, asserting that a message should trigger
52
+ # a route.
53
+ # @param message [String] The message that should trigger the route.
54
+ # @return [RouteMatcher] A {RouteMatcher} that should have +to+ called on
55
+ # it to complete the test.
56
+ def routes(message)
57
+ RouteMatcher.new(self, message)
58
+ end
59
+
60
+ # Starts a chat routing test chain, asserting that a message should not
61
+ # trigger a route.
62
+ # @param message [String] The message that should not trigger the route.
63
+ # @return [RouteMatcher] A {RouteMatcher} that should have +to+ called on
64
+ # it to complete the test.
65
+ def does_not_route(message)
66
+ RouteMatcher.new(self, message, invert: true)
67
+ end
68
+ alias_method :doesnt_route, :does_not_route
69
+
70
+ # Starts a chat routing test chain, asserting that a "command" message
71
+ # should trigger a route.
72
+ # @param message [String] The message that should trigger the route.
73
+ # @return [RouteMatcher] A {RouteMatcher} that should have +to+ called on
74
+ # it to complete the test.
75
+ def routes_command(message)
76
+ RouteMatcher.new(self, "#{robot.mention_name}: #{message}")
77
+ end
78
+
79
+ # Starts a chat routing test chain, asserting that a "command" message
80
+ # should not trigger a route.
81
+ # @param message [String] The message that should not trigger the route.
82
+ # @return [RouteMatcher] A {RouteMatcher} that should have +to+ called on
83
+ # it to complete the test.
84
+ def does_not_route_command(message)
85
+ RouteMatcher.new(self, "#{robot.mention_name}: #{message}", invert: true)
86
+ end
87
+ alias_method :doesnt_route_command, :does_not_route_command
88
+
89
+ # Starts an HTTP routing test chain, asserting that a request to the given
90
+ # path with the given HTTP request method will trigger a route.
91
+ # @param http_method [Symbol] The HTTP request method that should trigger
92
+ # the route.
93
+ # @param path [String] The path URL component that should trigger the
94
+ # route.
95
+ # @return [HTTPRouteMatcher] An {HTTPRouteMatcher} that should have +to+
96
+ # called on it to complete the test.
97
+ def routes_http(http_method, path)
98
+ HTTPRouteMatcher.new(self, http_method, path)
99
+ end
100
+
101
+ # Starts an HTTP routing test chain, asserting that a request to the given
102
+ # path with the given HTTP request method will not trigger a route.
103
+ # @param http_method [Symbol] The HTTP request method that should not
104
+ # trigger the route.
105
+ # @param path [String] The path URL component that should not trigger the
106
+ # route.
107
+ # @return [HTTPRouteMatcher] An {HTTPRouteMatcher} that should have +to+
108
+ # called on it to complete the test.
109
+ def does_not_route_http(http_method, path)
110
+ HTTPRouteMatcher.new(self, http_method, path, invert: true)
111
+ end
112
+ alias_method :doesnt_route_http, :does_not_route_http
113
+ end
114
+
115
+ # Used to complete a chat routing test chain.
116
+ class RouteMatcher
117
+ def initialize(context, message_body, invert: false)
118
+ @context = context
119
+ @message_body = message_body
120
+ @method = invert ? :not_to : :to
121
+ end
122
+
123
+ # Sets an expectation that a route will or will not be triggered, then
124
+ # sends the message originally provided.
125
+ # @param route [Symbol] The name of the method that should or should not
126
+ # be triggered.
127
+ # @return [void]
128
+ def to(route)
129
+ m = @method
130
+ b = @message_body
131
+
132
+ @context.instance_eval do
133
+ allow(Authorization).to receive(:user_in_group?).and_return(true)
134
+ expect_any_instance_of(described_class).public_send(m, receive(route))
135
+ send_message(b)
136
+ end
137
+ end
138
+ end
139
+
140
+ # Used to complete an HTTP routing test chain.
141
+ class HTTPRouteMatcher
142
+ def initialize(context, http_method, path, invert: false)
143
+ @context = context
144
+ @http_method = http_method
145
+ @path = path
146
+ @method = invert ? :not_to : :to
147
+ end
148
+
149
+ # Sets an expectation that an HTTP route will or will not be triggered,
150
+ # then makes an HTTP request against the app with the HTTP request
151
+ # method and path originally provided.
152
+ # @param route [Symbol] The name of the method that should or should not
153
+ # be triggered.
154
+ # @return [void]
155
+ def to(route)
156
+ m = @method
157
+ h = @http_method
158
+ p = @path
159
+
160
+ @context.instance_eval do
161
+ expect_any_instance_of(described_class).public_send(m, receive(route))
162
+ env = Rack::MockRequest.env_for(p, method: h)
163
+ robot.app.call(env)
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -1,7 +1,25 @@
1
1
  module Lita
2
+ # A wrapper object representing the source of an incoming message (the user
3
+ # who sent it, and optionally the room they sent it from). If a room is set,
4
+ # the message is from a group chat room. If no room is set, the message is
5
+ # assumed to be a private message. Source objects are also used as "target"
6
+ # objects when sending an outgoing message or performing another operation
7
+ # on a user or a room.
2
8
  class Source
3
- attr_reader :user, :room
4
9
 
10
+ # The room the message came from or should be sent to.
11
+ # @return [String] A string uniquely identifying the room.
12
+ attr_reader :room
13
+
14
+ # The user who sent the message or should receive the outgoing message.
15
+ # @return [Lita::User] The user.
16
+ attr_reader :user
17
+
18
+ # @param user [Lita::User] The user who sent the message or should receive
19
+ # the outgoing message.
20
+ # @param room [String] A string uniquely identifying the room the user sent
21
+ # the message from, or the room where a reply should go. The format of
22
+ # this string will differ depending on the chat service.
5
23
  def initialize(user, room = nil)
6
24
  @user = user
7
25
  @room = room
@@ -1,10 +1,19 @@
1
1
  module Lita
2
+ # A user in the chat service. Persisted in Redis.
2
3
  class User
3
4
  class << self
5
+ # The +Redis::Namespace+ for user persistence.
6
+ # @return [Redis::Namespace] The Redis connection.
4
7
  def redis
5
8
  @redis ||= Redis::Namespace.new("users", redis: Lita.redis)
6
9
  end
7
10
 
11
+ # Finds or creates a user. Attempts to find a user with the given ID. If
12
+ # none is found, creates a user with the provided ID and metadata.
13
+ # @param id [Integer, String] A unique identifier for the user.
14
+ # @param metadata [Hash] An optional hash of metadata about the user.
15
+ # @option metadata [String] name (id) The display name of the user.
16
+ # @return [Lita::User] The user.
8
17
  def create(id, metadata = {})
9
18
  user = find_by_id(id)
10
19
  unless user
@@ -15,25 +24,47 @@ module Lita
15
24
  end
16
25
  alias_method :find, :create
17
26
 
27
+ # Finds a user by ID.
28
+ # @param id [Integer, String] The user's unique ID.
29
+ # @return [Lita::User, nil] The user or +nil+ if no such user is known.
18
30
  def find_by_id(id)
19
31
  metadata = redis.hgetall("id:#{id}")
20
32
  return new(id, metadata) if metadata.key?("name")
21
33
  end
22
34
 
35
+ # Finds a user by display name.
36
+ # @param name [String] The user's name.
37
+ # @return [Lita::User, nil] The user or +nil+ if no such user is known.
23
38
  def find_by_name(name)
24
39
  id = redis.get("name:#{name}")
25
40
  find_by_id(id) if id
26
41
  end
27
42
  end
28
43
 
29
- attr_reader :id, :name, :metadata
44
+ # The user's unique ID.
45
+ # @return [String] The user's ID.
46
+ attr_reader :id
30
47
 
48
+ # A hash of arbitrary metadata about the user.
49
+ # @return [Hash] The user's metadata.
50
+ attr_reader :metadata
51
+
52
+ # The user's name as displayed in the chat.
53
+ # @return [String] The user's name.
54
+ attr_reader :name
55
+
56
+ # @param id [Integer, String] The user's unique ID.
57
+ # @param metadata [Hash] Arbitrary user metadata.
58
+ # @option metadata [String] name (id) The user's display name.
31
59
  def initialize(id, metadata = {})
32
60
  @id = id.to_s
33
61
  @metadata = metadata
34
62
  @name = @metadata[:name] || @metadata["name"] || @id
35
63
  end
36
64
 
65
+ # Saves the user record to Redis, overwriting an previous data for the
66
+ # current ID and user name.
67
+ # @return [void]
37
68
  def save
38
69
  redis.pipelined do
39
70
  redis.hmset("id:#{id}", *metadata.to_a.flatten)
@@ -41,6 +72,10 @@ module Lita
41
72
  end
42
73
  end
43
74
 
75
+ # Compares the user against another user object to determine equality. Users
76
+ # are considered equal if they have the same ID and name.
77
+ # @param other (Lita::User) The user to compare against.
78
+ # @return [Boolean] True if users are equal, false otherwise.
44
79
  def ==(other)
45
80
  other.respond_to?(:id) && id == other.id &&
46
81
  other.respond_to?(:name) && name == other.name
@@ -48,6 +83,7 @@ module Lita
48
83
 
49
84
  private
50
85
 
86
+ # The Redis connection for user persistence.
51
87
  def redis
52
88
  self.class.redis
53
89
  end
@@ -0,0 +1,26 @@
1
+ module Lita
2
+ # Handy utilities used by other parts Lita classes.
3
+ module Util
4
+ # A regular expression for acronyms.
5
+ ACRONYM_REGEX = /(?=a)b/
6
+
7
+ class << self
8
+ # Transforms a camel-cased string into a snaked-cased string. Taken from
9
+ # +ActiveSupport.+
10
+ # @param camel_cased_word [String] The word to transform.
11
+ # @return [String] The transformed word.
12
+ def underscore(camel_cased_word)
13
+ word = camel_cased_word.to_s.dup
14
+ word.gsub!('::', '/')
15
+ word.gsub!(/(?:([A-Za-z\d])|^)(#{ACRONYM_REGEX})(?=\b|[^a-z])/) do
16
+ "#{$1}#{$1 && '_'}#{$2.downcase}"
17
+ end
18
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
19
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
20
+ word.tr!("-", "_")
21
+ word.downcase!
22
+ word
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,3 +1,4 @@
1
1
  module Lita
2
- VERSION = "1.1.2"
2
+ # The current version of Lita.
3
+ VERSION = "2.0.0"
3
4
  end
@@ -21,11 +21,16 @@ Gem::Specification.new do |spec|
21
21
  spec.required_ruby_version = ">= 2.0.0"
22
22
 
23
23
  spec.add_runtime_dependency "bundler", "~> 1.3"
24
+ spec.add_runtime_dependency "faraday", "~> 0.8.7"
25
+ spec.add_runtime_dependency "multi_json", "~> 1.7.7"
26
+ spec.add_runtime_dependency "rack", "~> 1.5.2"
24
27
  spec.add_runtime_dependency "redis-namespace", "~> 1.3.0"
28
+ spec.add_runtime_dependency "thin", "~> 1.5.1"
25
29
  spec.add_runtime_dependency "thor", "~> 0.18.1"
26
30
 
27
31
  spec.add_development_dependency "rake"
28
32
  spec.add_development_dependency "rspec", ">= 2.14.0rc1"
29
33
  spec.add_development_dependency "simplecov"
30
34
  spec.add_development_dependency "coveralls"
35
+ spec.add_development_dependency "pry"
31
36
  end
@@ -4,20 +4,28 @@ describe Lita::Adapters::Shell do
4
4
  subject { described_class.new(robot) }
5
5
 
6
6
  describe "#run" do
7
- before { allow(subject).to receive(:puts) }
7
+ before do
8
+ allow(subject).to receive(:puts)
9
+ allow(subject).to receive(:print)
10
+ allow($stdin).to receive(:gets).and_return("foo", "exit")
11
+ allow(robot).to receive(:receive)
12
+ end
8
13
 
9
14
  it "passes input to the Robot and breaks on an exit message" do
10
15
  expect(subject).to receive(:print).with("#{robot.name} > ").twice
11
- allow($stdin).to receive(:gets).and_return("foo", "exit")
12
16
  expect(robot).to receive(:receive).with(an_instance_of(Lita::Message))
13
- allow(Thread).to receive(:new) { |&block| block.call }
17
+ subject.run
18
+ end
19
+
20
+ it "marks messages as commands if config.adapter.private_chat is true" do
21
+ Lita.config.adapter.private_chat = true
22
+ expect_any_instance_of(Lita::Message).to receive(:command!)
14
23
  subject.run
15
24
  end
16
25
  end
17
26
 
18
27
  describe "#send_message" do
19
28
  it "prints its input" do
20
- expect(subject).to receive(:puts)
21
29
  expect(subject).to receive(:puts).with("bar")
22
30
  subject.send_messages(double("target"), "bar")
23
31
  end