rita 0.1.0 → 5.0.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +19 -0
  3. data/.rubocop.yml +51 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +16 -0
  6. data/CONTRIBUTING.md +18 -0
  7. data/Gemfile +5 -0
  8. data/{LICENSE.txt → LICENSE} +1 -3
  9. data/README.md +17 -24
  10. data/Rakefile +5 -5
  11. data/bin/lita +7 -0
  12. data/lib/lita/adapter.rb +147 -0
  13. data/lib/lita/adapters/shell.rb +126 -0
  14. data/lib/lita/adapters/test.rb +62 -0
  15. data/lib/lita/authorization.rb +112 -0
  16. data/lib/lita/callback.rb +39 -0
  17. data/lib/lita/cli.rb +218 -0
  18. data/lib/lita/configurable.rb +47 -0
  19. data/lib/lita/configuration_builder.rb +247 -0
  20. data/lib/lita/configuration_validator.rb +95 -0
  21. data/lib/lita/default_configuration.rb +122 -0
  22. data/lib/lita/errors.rb +25 -0
  23. data/lib/lita/handler/chat_router.rb +141 -0
  24. data/lib/lita/handler/common.rb +208 -0
  25. data/lib/lita/handler/event_router.rb +84 -0
  26. data/lib/lita/handler/http_router.rb +31 -0
  27. data/lib/lita/handler.rb +15 -0
  28. data/lib/lita/handlers/authorization.rb +129 -0
  29. data/lib/lita/handlers/help.rb +171 -0
  30. data/lib/lita/handlers/info.rb +66 -0
  31. data/lib/lita/handlers/room.rb +36 -0
  32. data/lib/lita/handlers/users.rb +37 -0
  33. data/lib/lita/http_callback.rb +46 -0
  34. data/lib/lita/http_route.rb +83 -0
  35. data/lib/lita/logger.rb +43 -0
  36. data/lib/lita/message.rb +124 -0
  37. data/lib/lita/middleware_registry.rb +39 -0
  38. data/lib/lita/namespace.rb +29 -0
  39. data/lib/lita/plugin_builder.rb +43 -0
  40. data/lib/lita/rack_app.rb +100 -0
  41. data/lib/lita/registry.rb +164 -0
  42. data/lib/lita/response.rb +65 -0
  43. data/lib/lita/robot.rb +273 -0
  44. data/lib/lita/room.rb +119 -0
  45. data/lib/lita/route_validator.rb +82 -0
  46. data/lib/lita/rspec/handler.rb +127 -0
  47. data/lib/lita/rspec/matchers/chat_route_matcher.rb +53 -0
  48. data/lib/lita/rspec/matchers/event_route_matcher.rb +29 -0
  49. data/lib/lita/rspec/matchers/http_route_matcher.rb +34 -0
  50. data/lib/lita/rspec.rb +48 -0
  51. data/lib/lita/source.rb +81 -0
  52. data/lib/lita/store.rb +23 -0
  53. data/lib/lita/target.rb +3 -0
  54. data/lib/lita/template.rb +71 -0
  55. data/lib/lita/template_resolver.rb +52 -0
  56. data/lib/lita/timer.rb +49 -0
  57. data/lib/lita/user.rb +157 -0
  58. data/lib/lita/util.rb +31 -0
  59. data/lib/lita/version.rb +6 -0
  60. data/lib/lita.rb +166 -0
  61. data/rita.gemspec +50 -0
  62. data/spec/lita/adapter_spec.rb +54 -0
  63. data/spec/lita/adapters/shell_spec.rb +99 -0
  64. data/spec/lita/authorization_spec.rb +122 -0
  65. data/spec/lita/configuration_builder_spec.rb +247 -0
  66. data/spec/lita/configuration_validator_spec.rb +114 -0
  67. data/spec/lita/default_configuration_spec.rb +242 -0
  68. data/spec/lita/handler/chat_router_spec.rb +236 -0
  69. data/spec/lita/handler/common_spec.rb +289 -0
  70. data/spec/lita/handler/event_router_spec.rb +121 -0
  71. data/spec/lita/handler/http_router_spec.rb +155 -0
  72. data/spec/lita/handler_spec.rb +62 -0
  73. data/spec/lita/handlers/authorization_spec.rb +111 -0
  74. data/spec/lita/handlers/help_spec.rb +124 -0
  75. data/spec/lita/handlers/info_spec.rb +67 -0
  76. data/spec/lita/handlers/room_spec.rb +24 -0
  77. data/spec/lita/handlers/users_spec.rb +35 -0
  78. data/spec/lita/logger_spec.rb +28 -0
  79. data/spec/lita/message_spec.rb +178 -0
  80. data/spec/lita/plugin_builder_spec.rb +41 -0
  81. data/spec/lita/response_spec.rb +62 -0
  82. data/spec/lita/robot_spec.rb +285 -0
  83. data/spec/lita/room_spec.rb +136 -0
  84. data/spec/lita/rspec/handler_spec.rb +33 -0
  85. data/spec/lita/rspec_spec.rb +162 -0
  86. data/spec/lita/source_spec.rb +68 -0
  87. data/spec/lita/store_spec.rb +23 -0
  88. data/spec/lita/template_resolver_spec.rb +42 -0
  89. data/spec/lita/template_spec.rb +52 -0
  90. data/spec/lita/timer_spec.rb +32 -0
  91. data/spec/lita/user_spec.rb +167 -0
  92. data/spec/lita/util_spec.rb +18 -0
  93. data/spec/lita_spec.rb +227 -0
  94. data/spec/spec_helper.rb +35 -0
  95. data/spec/templates/basic.erb +1 -0
  96. data/spec/templates/basic.irc.erb +1 -0
  97. data/spec/templates/helpers.erb +1 -0
  98. data/spec/templates/interpolated.erb +1 -0
  99. data/templates/locales/en.yml +137 -0
  100. data/templates/plugin/Gemfile +5 -0
  101. data/templates/plugin/README.tt +29 -0
  102. data/templates/plugin/Rakefile +8 -0
  103. data/templates/plugin/gemspec.tt +27 -0
  104. data/templates/plugin/gitignore +18 -0
  105. data/templates/plugin/lib/lita/plugin_type/plugin.tt +19 -0
  106. data/templates/plugin/lib/plugin.tt +16 -0
  107. data/templates/plugin/locales/en.yml.tt +4 -0
  108. data/templates/plugin/spec/lita/plugin_type/plugin_spec.tt +6 -0
  109. data/templates/plugin/spec/spec_helper.tt +8 -0
  110. data/templates/plugin/templates/gitkeep +0 -0
  111. data/templates/robot/Gemfile +5 -0
  112. data/templates/robot/lita_config.rb +28 -0
  113. metadata +386 -21
  114. data/.standard.yml +0 -3
  115. data/CHANGELOG.md +0 -5
  116. data/CODE_OF_CONDUCT.md +0 -132
  117. data/lib/rita/version.rb +0 -5
  118. data/lib/rita.rb +0 -8
  119. data/sig/rita.rbs +0 -4
data/lib/lita/robot.rb ADDED
@@ -0,0 +1,273 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+ require "i18n"
5
+
6
+ require "puma"
7
+
8
+ require_relative "authorization"
9
+ require_relative "rack_app"
10
+ require_relative "room"
11
+ require_relative "store"
12
+
13
+ module Lita
14
+ # The main object representing a running instance of Lita. Provides a high
15
+ # level API for the underlying adapter. Dispatches incoming messages to
16
+ # registered handlers. Can send outgoing chat messages and set the topic
17
+ # of chat rooms.
18
+ class Robot
19
+ extend Forwardable
20
+
21
+ # A +Rack+ application used for the built-in web server.
22
+ # @return [Rack::Builder] The +Rack+ app.
23
+ attr_reader :app
24
+
25
+ # The {Authorization} object for the currently running robot.
26
+ # @return [Authorization] The authorization object.
27
+ # @since 4.0.0
28
+ attr_reader :auth
29
+
30
+ # The name the robot will look for in incoming messages to determine if it's
31
+ # being addressed.
32
+ # @return [String] The mention name.
33
+ attr_accessor :mention_name
34
+
35
+ # An alias the robot will look for in incoming messages to determine if it's
36
+ # being addressed.
37
+ # @return [String, Nil] The alias, if one was set.
38
+ attr_accessor :alias
39
+
40
+ # The name of the robot as it will appear in the chat.
41
+ # @return [String] The robot's name.
42
+ attr_accessor :name
43
+
44
+ # The {Registry} for the currently running robot.
45
+ # @return [Registry] The registry.
46
+ # @since 4.0.0
47
+ attr_reader :registry
48
+
49
+ # The {Store} for handlers to persist data between instances.
50
+ # @return [Store] The store.
51
+ # @since 5.0.0
52
+ attr_reader :store
53
+
54
+ def_delegators :registry, :config, :adapters, :handlers, :hooks, :redis
55
+
56
+ # @!method chat_service
57
+ # @see Adapter#chat_service
58
+ # @since 4.6.0
59
+ # @!method mention_format(name)
60
+ # @see Adapter#mention_format
61
+ # @since 4.4.0
62
+ # @!method roster(room)
63
+ # @see Adapter#roster
64
+ # @since 4.4.1
65
+ # @!method run_concurrently
66
+ # @see Adapter#run_concurrently
67
+ # @since 5.0.0
68
+ def_delegators :adapter, :chat_service, :mention_format, :roster, :run_concurrently
69
+
70
+ # @param registry [Registry] The registry for the robot's configuration and plugins.
71
+ def initialize(registry = Lita)
72
+ @registry = registry
73
+ @name = config.robot.name
74
+ @mention_name = config.robot.mention_name || @name
75
+ @alias = config.robot.alias
76
+ @store = Store.new(Hash.new { |h, k| h[k] = Store.new })
77
+ @app = RackApp.build(self)
78
+ @auth = Authorization.new(self)
79
+ handlers.each do |handler|
80
+ handler.after_config_block&.call(config.handlers.public_send(handler.namespace))
81
+ end
82
+ trigger(:loaded, room_ids: persisted_rooms)
83
+ end
84
+
85
+ # The global logger. A convenience for +Lita.logger+.
86
+ # @return [::Logger] The logger.
87
+ def logger
88
+ Lita.logger
89
+ end
90
+
91
+ # The primary entry point from the adapter for an incoming message.
92
+ # Dispatches the message to all registered handlers.
93
+ # @param message [Message] The incoming message.
94
+ # @return [void]
95
+ def receive(message)
96
+ trigger(:message_received, message: message)
97
+
98
+ matched = handlers.map do |handler|
99
+ next unless handler.respond_to?(:dispatch)
100
+
101
+ handler.dispatch(self, message)
102
+ end.any?
103
+
104
+ trigger(:unhandled_message, message: message) unless matched
105
+ end
106
+
107
+ # Starts the robot, booting the web server and delegating to the adapter to
108
+ # connect to the chat service.
109
+ # @return [void]
110
+ def run
111
+ run_app
112
+ adapter.run
113
+ rescue Interrupt
114
+ shut_down
115
+ end
116
+
117
+ # Makes the robot join a room with the specified ID.
118
+ # @param room [Room, String] The room to join, as a {Room} object or a string identifier.
119
+ # @return [void]
120
+ # @since 3.0.0
121
+ def join(room)
122
+ room_object = find_room(room)
123
+
124
+ if room_object
125
+ redis.sadd("persisted_rooms", room_object.id)
126
+ adapter.join(room_object.id)
127
+ else
128
+ adapter.join(room)
129
+ end
130
+ end
131
+
132
+ # Makes the robot part from the room with the specified ID.
133
+ # @param room [Room, String] The room to leave, as a {Room} object or a string identifier.
134
+ # @return [void]
135
+ # @since 3.0.0
136
+ def part(room)
137
+ room_object = find_room(room)
138
+
139
+ if room_object
140
+ redis.srem("persisted_rooms", room_object.id)
141
+ adapter.part(room_object.id)
142
+ else
143
+ adapter.part(room)
144
+ end
145
+ end
146
+
147
+ # A list of room IDs the robot should join on boot.
148
+ # @return [Array<String>] An array of room IDs.
149
+ # @since 4.4.2
150
+ def persisted_rooms
151
+ redis.smembers("persisted_rooms").sort
152
+ end
153
+
154
+ # Sends one or more messages to a user or room.
155
+ # @param target [Source] The user or room to send to. If the Source
156
+ # has a room, it will choose the room. Otherwise, it will send to the
157
+ # user.
158
+ # @param strings [String, Array<String>] One or more strings to send.
159
+ # @return [void]
160
+ def send_messages(target, *strings)
161
+ adapter.send_messages(target, strings.flatten)
162
+ end
163
+ alias send_message send_messages
164
+
165
+ # Sends one or more messages to a user or room. If sending to a room,
166
+ # prefixes each message with the user's mention name.
167
+ # @param target [Source] The user or room to send to. If the Source
168
+ # has a room, it will choose the room. Otherwise, it will send to the
169
+ # user.
170
+ # @param strings [String, Array<String>] One or more strings to send.
171
+ # @return [void]
172
+ # @since 3.1.0
173
+ def send_messages_with_mention(target, *strings)
174
+ return send_messages(target, *strings) if target.private_message?
175
+
176
+ mention_name = target.user.mention_name
177
+ prefixed_strings = strings.map do |s|
178
+ "#{adapter.mention_format(mention_name).strip} #{s}"
179
+ end
180
+
181
+ send_messages(target, *prefixed_strings)
182
+ end
183
+ alias send_message_with_mention send_messages_with_mention
184
+
185
+ # Sets the topic for a chat room.
186
+ # @param target [Source] A source object specifying the room.
187
+ # @param topic [String] The new topic message to set.
188
+ # @return [void]
189
+ def set_topic(target, topic)
190
+ adapter.set_topic(target, topic)
191
+ end
192
+
193
+ # Gracefully shuts the robot down, stopping the web server and delegating
194
+ # to the adapter to perform any shut down tasks necessary for the chat
195
+ # service.
196
+ # @return [void]
197
+ def shut_down
198
+ trigger(:shut_down_started)
199
+ @server&.stop(true)
200
+ @server_thread&.join
201
+ adapter.shut_down
202
+ trigger(:shut_down_complete)
203
+ end
204
+
205
+ # Triggers an event, instructing all registered handlers to invoke any
206
+ # methods subscribed to the event, and passing them a payload hash of
207
+ # arbitrary data.
208
+ # @param event_name [String, Symbol] The name of the event to trigger.
209
+ # @param payload [Hash] An optional hash of arbitrary data.
210
+ # @return [void]
211
+ def trigger(event_name, payload = {})
212
+ handlers.each do |handler|
213
+ next unless handler.respond_to?(:trigger)
214
+
215
+ handler.trigger(self, event_name, payload)
216
+ end
217
+ end
218
+
219
+ private
220
+
221
+ # Loads and caches the adapter on first access.
222
+ def adapter
223
+ @adapter ||= load_adapter
224
+ end
225
+
226
+ # Ensure the argument is a Room.
227
+ def find_room(room_or_identifier)
228
+ case room_or_identifier
229
+ when Room
230
+ room_or_identifier
231
+ else
232
+ Room.fuzzy_find(room_or_identifier)
233
+ end
234
+ end
235
+
236
+ # Loads the selected adapter.
237
+ def load_adapter
238
+ adapter_name = config.robot.adapter
239
+ adapter_class = adapters[adapter_name.to_sym]
240
+
241
+ unless adapter_class
242
+ logger.fatal I18n.t("lita.robot.unknown_adapter", adapter: adapter_name)
243
+ exit(false)
244
+ end
245
+
246
+ adapter_class.new(self)
247
+ end
248
+
249
+ # Starts the web server.
250
+ def run_app
251
+ http_config = config.http
252
+
253
+ @server_thread = Thread.new do
254
+ @server = Puma::Server.new(app)
255
+ begin
256
+ @server.add_tcp_listener(http_config.host, http_config.port.to_i)
257
+ rescue Errno::EADDRINUSE, Errno::EACCES => e
258
+ logger.fatal I18n.t(
259
+ "lita.http.exception",
260
+ message: e.message,
261
+ backtrace: e.backtrace.join("\n")
262
+ )
263
+ exit(false)
264
+ end
265
+ @server.min_threads = http_config.min_threads
266
+ @server.max_threads = http_config.max_threads
267
+ @server.run
268
+ end
269
+
270
+ @server_thread.abort_on_exception = true
271
+ end
272
+ end
273
+ end
data/lib/lita/room.rb ADDED
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redis-namespace"
4
+
5
+ require_relative "util"
6
+
7
+ module Lita
8
+ # A room in the chat service. Persisted in Redis.
9
+ # @since 4.4.0
10
+ class Room
11
+ class << self
12
+ # Creates a new room with the given ID, or merges and saves supplied
13
+ # metadata to a room with the given ID.
14
+ # @param id [Integer, String] A unique identifier for the room.
15
+ # @param metadata [Hash] An optional hash of metadata about the room.
16
+ # @option metadata [String] name (id) The display name of the room.
17
+ # @return [Room] The room.
18
+ def create_or_update(id, metadata = {})
19
+ existing_room = find_by_id(id)
20
+ metadata = Util.stringify_keys(metadata)
21
+ metadata = existing_room.metadata.merge(metadata) if existing_room
22
+ room = new(id, metadata)
23
+ room.save
24
+ room
25
+ end
26
+
27
+ # Finds a room by ID.
28
+ # @param id [Integer, String] The room's unique ID.
29
+ # @return [Room, nil] The room or +nil+ if no such room is known.
30
+ def find_by_id(id)
31
+ metadata = redis.hgetall("id:#{id}")
32
+ new(id, metadata) if metadata.key?("name")
33
+ end
34
+
35
+ # Finds a room by display name.
36
+ # @param name [String] The room's name.
37
+ # @return [Room, nil] The room or +nil+ if no such room is known.
38
+ def find_by_name(name)
39
+ id = redis.get("name:#{name}")
40
+ find_by_id(id) if id
41
+ end
42
+
43
+ # Finds a room by ID or name
44
+ # @param identifier [Integer, String] The room's ID or name.
45
+ # @return [Room, nil] The room or +nil+ if no room was found.
46
+ def fuzzy_find(identifier)
47
+ find_by_id(identifier) || find_by_name(identifier)
48
+ end
49
+
50
+ # The +Redis::Namespace+ for room persistence.
51
+ # @return [Redis::Namespace] The Redis connection.
52
+ def redis
53
+ @redis ||= Redis::Namespace.new("rooms", redis: Lita.redis)
54
+ end
55
+ end
56
+
57
+ # The room's unique ID.
58
+ # @return [String] The room's ID.
59
+ attr_reader :id
60
+
61
+ # A hash of arbitrary metadata about the room.
62
+ # @return [Hash] The room's metadata.
63
+ attr_reader :metadata
64
+
65
+ # The room's name as displayed in a standard user interface.
66
+ # @return [String] The room's name.
67
+ attr_reader :name
68
+
69
+ # @param id [Integer, String] The room's unique ID.
70
+ # @param metadata [Hash] Arbitrary room metadata.
71
+ # @option metadata [String] name (id) The room's display name.
72
+ def initialize(id, metadata = {})
73
+ @id = id.to_s
74
+ @metadata = Util.stringify_keys(metadata)
75
+ @name = @metadata["name"] || @id
76
+ end
77
+
78
+ # Compares the room against another room object to determine equality. Rooms
79
+ # are considered equal if they have the same ID.
80
+ # @param other [Room] The room to compare against.
81
+ # @return [Boolean] True if rooms are equal, false otherwise.
82
+ def ==(other)
83
+ other.respond_to?(:id) && id == other.id
84
+ end
85
+ alias eql? ==
86
+
87
+ # Generates a +Fixnum+ hash value for this user object. Implemented to support equality.
88
+ # @return [Fixnum] The hash value.
89
+ # @see Object#hash
90
+ def hash
91
+ id.hash
92
+ end
93
+
94
+ # Saves the room record to Redis, overwriting any previous data for the current ID.
95
+ # @return [void]
96
+ def save
97
+ ensure_name_metadata_set
98
+
99
+ redis.pipelined do
100
+ redis.hmset("id:#{id}", *metadata.to_a.flatten)
101
+ redis.set("name:#{name}", id)
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ # Ensure the room's metadata contains its name, to ensure their Redis hash contains at least
108
+ # one value. It's not possible to store an empty hash key in Redis.
109
+ def ensure_name_metadata_set
110
+ room_name = metadata.delete("name")
111
+ metadata["name"] = room_name || id
112
+ end
113
+
114
+ # The Redis connection for room persistence.
115
+ def redis
116
+ self.class.redis
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lita
4
+ # Determines if an incoming message should trigger a route.
5
+ # @api private
6
+ class RouteValidator
7
+ # The handler class the route belongs to.
8
+ attr_reader :handler
9
+
10
+ # The incoming message.
11
+ attr_reader :message
12
+
13
+ # The currently running robot.
14
+ attr_reader :robot
15
+
16
+ # The route being checked.
17
+ attr_reader :route
18
+
19
+ # @param handler [Handler] The handler the route belongs to.
20
+ # @param route [Handler::ChatRouter::Route] The route being validated.
21
+ # @param message [Message] The incoming message.
22
+ # @param robot [Robot] The currently running robot.
23
+ def initialize(handler, route, message, robot)
24
+ @handler = handler
25
+ @route = route
26
+ @message = message
27
+ @robot = robot
28
+ end
29
+
30
+ # Returns a boolean indicating whether or not the route should be triggered.
31
+ # @return [Boolean] Whether or not the route should be triggered.
32
+ def call
33
+ return unless command_satisfied?(route, message)
34
+ return if from_self?(message, robot)
35
+ return unless matches_pattern?(route, message)
36
+
37
+ unless authorized?(robot, message.user, route.required_groups)
38
+ robot.trigger(
39
+ :route_authorization_failed,
40
+ message: message,
41
+ robot: robot,
42
+ route: route,
43
+ )
44
+ return
45
+ end
46
+ return unless passes_route_hooks?(route, message, robot)
47
+
48
+ true
49
+ end
50
+
51
+ private
52
+
53
+ # Message must be a command if the route requires a command
54
+ def command_satisfied?(route, message)
55
+ !route.command? || message.command?
56
+ end
57
+
58
+ # Messages from self should be ignored to prevent infinite loops
59
+ def from_self?(message, robot)
60
+ message.user.name == robot.name
61
+ end
62
+
63
+ # Message must match the pattern
64
+ def matches_pattern?(route, message)
65
+ route.pattern.match?(message.body)
66
+ end
67
+
68
+ # Allow custom route hooks to reject the route
69
+ def passes_route_hooks?(route, message, robot)
70
+ robot.hooks[:validate_route].all? do |hook|
71
+ hook.call(handler: handler, route: route, message: message, robot: robot)
72
+ end
73
+ end
74
+
75
+ # User must be in auth group if route is restricted.
76
+ def authorized?(robot, user, required_groups)
77
+ required_groups.nil? || required_groups.any? do |group|
78
+ robot.auth.user_in_group?(user, group)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ require "i18n"
6
+ require "faraday"
7
+
8
+ require_relative "../adapters/test"
9
+ require_relative "../message"
10
+ require_relative "../rspec"
11
+ require_relative "../robot"
12
+ require_relative "../source"
13
+ require_relative "../user"
14
+ require_relative "matchers/chat_route_matcher"
15
+ require_relative "matchers/http_route_matcher"
16
+ require_relative "matchers/event_route_matcher"
17
+
18
+ module Lita
19
+ module RSpec
20
+ # Extras for +RSpec+ to facilitate testing Lita handlers.
21
+ module Handler
22
+ include Matchers::ChatRouteMatcher
23
+ include Matchers::HTTPRouteMatcher
24
+ include Matchers::EventRouteMatcher
25
+
26
+ class << self
27
+ # Sets up the RSpec environment to easily test Lita handlers.
28
+ def included(base)
29
+ base.include(Lita::RSpec)
30
+
31
+ prepare_handlers(base)
32
+ prepare_adapter(base)
33
+ prepare_let_blocks(base)
34
+ prepare_subject(base)
35
+ end
36
+
37
+ private
38
+
39
+ # Register the test adapter.
40
+ def prepare_adapter(base)
41
+ base.class_eval do
42
+ before do
43
+ registry.register_adapter(:test, Lita::Adapters::Test)
44
+ registry.config.robot.adapter = :test
45
+ end
46
+ end
47
+ end
48
+
49
+ # Register the handler(s) under test.
50
+ def prepare_handlers(base)
51
+ base.class_eval do
52
+ before do
53
+ handlers = Set.new(
54
+ [described_class] + Array(base.metadata[:additional_lita_handlers])
55
+ )
56
+
57
+ handlers.each do |handler|
58
+ registry.register_handler(handler)
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ # Create common test objects.
65
+ def prepare_let_blocks(base)
66
+ base.class_eval do
67
+ let(:robot) { Robot.new(registry) }
68
+ let(:source) { Source.new(user: user) }
69
+ let(:user) { User.create("1", name: "Test User") }
70
+ end
71
+ end
72
+
73
+ # Set up a working test subject.
74
+ def prepare_subject(base)
75
+ base.class_eval do
76
+ subject { described_class.new(robot) }
77
+ end
78
+ end
79
+ end
80
+
81
+ # An array of strings that have been sent by the robot during the course of a test.
82
+ # @return [Array<String>] The replies.
83
+ def replies
84
+ robot.chat_service.sent_messages
85
+ end
86
+
87
+ # Sends a message to the robot.
88
+ # @param body [String] The message to send.
89
+ # @param as [User] The user sending the message.
90
+ # @param from [Room] The room where the message is received from.
91
+ # @return [void]
92
+ def send_message(body, as: user, from: nil, privately: false)
93
+ message = Message.new(
94
+ robot,
95
+ body,
96
+ Source.new(user: as, room: from, private_message: privately)
97
+ )
98
+
99
+ robot.receive(message)
100
+ end
101
+
102
+ # Sends a "command" message to the robot.
103
+ # @param body [String] The message to send.
104
+ # @param as [User] The user sending the message.
105
+ # @param from [Room] The room where the message is received from.
106
+ # @return [void]
107
+ def send_command(body, as: user, from: nil, privately: false)
108
+ send_message("#{robot.mention_name}: #{body}", as: as, from: from, privately: privately)
109
+ end
110
+
111
+ # Returns a Faraday connection hooked up to the currently running robot's Rack app.
112
+ # @return [Faraday::Connection] The connection.
113
+ # @since 4.0.0
114
+ def http
115
+ unless Rack.const_defined?(:Test)
116
+ begin
117
+ require "rack/test"
118
+ rescue LoadError
119
+ raise LoadError, I18n.t("lita.rspec.rack_test_required")
120
+ end
121
+ end
122
+
123
+ Faraday::Connection.new { |c| c.adapter(:rack, robot.app) }
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../message"
4
+ require_relative "../../route_validator"
5
+
6
+ module Lita
7
+ module RSpec
8
+ module Matchers
9
+ # RSpec matchers for chat routes.
10
+ # @since 4.0.0
11
+ module ChatRouteMatcher
12
+ extend ::RSpec::Matchers::DSL
13
+
14
+ matcher :route do |message_body|
15
+ match do
16
+ message = Message.new(robot, message_body, source)
17
+
18
+ if defined?(@group) && @group.to_s.casecmp("admins").zero?
19
+ robot.config.robot.admins = Array(robot.config.robot.admins) + [source.user.id]
20
+ elsif defined?(@group)
21
+ robot.auth.add_user_to_group!(source.user, @group)
22
+ end
23
+
24
+ matching_routes = described_class.routes.select do |route|
25
+ RouteValidator.new(described_class, route, message, robot).call
26
+ end
27
+
28
+ if defined?(@method_name)
29
+ matching_routes.any? { |route| route.callback.method_name == @method_name }
30
+ else
31
+ !matching_routes.empty?
32
+ end
33
+ end
34
+
35
+ chain :with_authorization_for do |group|
36
+ @group = group
37
+ end
38
+
39
+ chain :to do |method_name|
40
+ @method_name = method_name
41
+ end
42
+ end
43
+
44
+ # Sets an expectation that the provided message routes to a command.
45
+ # @param message_body [String] The body of the message.
46
+ # @return [void]
47
+ def route_command(message_body)
48
+ route("#{robot.mention_name} #{message_body}")
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lita
4
+ module RSpec
5
+ module Matchers
6
+ # RSpec matchers for event routes.
7
+ # @since 4.0.0
8
+ module EventRouteMatcher
9
+ extend ::RSpec::Matchers::DSL
10
+
11
+ matcher :route_event do |event_name|
12
+ match do
13
+ callbacks = described_class.event_subscriptions_for(event_name)
14
+
15
+ if defined?(@method_name)
16
+ callbacks.any? { |callback| callback.method_name.equal?(@method_name) }
17
+ else
18
+ !callbacks.empty?
19
+ end
20
+ end
21
+
22
+ chain :to do |method_name|
23
+ @method_name = method_name
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end