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.
- checksums.yaml +4 -4
- data/README.md +202 -45
- data/lib/lita.rb +45 -24
- data/lib/lita/adapter.rb +35 -0
- data/lib/lita/adapters/shell.rb +14 -3
- data/lib/lita/authorization.rb +24 -0
- data/lib/lita/cli.rb +1 -0
- data/lib/lita/config.rb +52 -19
- data/lib/lita/handler.rb +100 -36
- data/lib/lita/handlers/authorization.rb +38 -27
- data/lib/lita/handlers/help.rb +25 -22
- data/lib/lita/handlers/web.rb +25 -0
- data/lib/lita/http_route.rb +64 -0
- data/lib/lita/logger.rb +34 -0
- data/lib/lita/message.rb +41 -7
- data/lib/lita/rack_app_builder.rb +110 -0
- data/lib/lita/response.rb +30 -0
- data/lib/lita/robot.rb +56 -1
- data/lib/lita/rspec.rb +23 -50
- data/lib/lita/rspec/handler.rb +168 -0
- data/lib/lita/source.rb +19 -1
- data/lib/lita/user.rb +37 -1
- data/lib/lita/util.rb +26 -0
- data/lib/lita/version.rb +2 -1
- data/lita.gemspec +5 -0
- data/spec/lita/adapters/shell_spec.rb +12 -4
- data/spec/lita/config_spec.rb +18 -2
- data/spec/lita/handler_spec.rb +43 -46
- data/spec/lita/handlers/authorization_spec.rb +24 -31
- data/spec/lita/handlers/help_spec.rb +10 -8
- data/spec/lita/handlers/web_spec.rb +19 -0
- data/spec/lita/logger_spec.rb +26 -0
- data/spec/lita/message_spec.rb +16 -11
- data/spec/lita/robot_spec.rb +25 -2
- data/spec/lita/rspec_spec.rb +32 -20
- data/spec/lita/util_spec.rb +9 -0
- data/spec/lita_spec.rb +0 -51
- data/spec/spec_helper.rb +2 -1
- metadata +85 -3
- data/CHANGELOG.md +0 -21
data/lib/lita/rspec.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/lita/source.rb
CHANGED
@@ -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
|
data/lib/lita/user.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/lita/util.rb
ADDED
@@ -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
|
data/lib/lita/version.rb
CHANGED
data/lita.gemspec
CHANGED
@@ -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
|
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
|
-
|
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
|