slack-ruby-client 0.13.1 → 0.14.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/.rubocop_todo.yml +9 -32
  4. data/.travis.yml +4 -4
  5. data/CHANGELOG.md +10 -0
  6. data/Dangerfile +1 -0
  7. data/Gemfile +1 -3
  8. data/LICENSE.md +1 -1
  9. data/README.md +100 -13
  10. data/bin/commands.rb +1 -0
  11. data/bin/commands/apps.rb +14 -0
  12. data/bin/commands/chat.rb +5 -1
  13. data/bin/commands/conversations.rb +1 -0
  14. data/bin/commands/files.rb +8 -9
  15. data/bin/commands/reactions.rb +2 -2
  16. data/bin/slack +1 -1
  17. data/lib/slack-ruby-client.rb +4 -0
  18. data/lib/slack/events/config.rb +31 -0
  19. data/lib/slack/events/request.rb +60 -0
  20. data/lib/slack/real_time/client.rb +35 -7
  21. data/lib/slack/real_time/concurrency/async.rb +34 -2
  22. data/lib/slack/real_time/concurrency/celluloid.rb +28 -9
  23. data/lib/slack/real_time/concurrency/eventmachine.rb +25 -4
  24. data/lib/slack/real_time/socket.rb +19 -0
  25. data/lib/slack/real_time/stores/store.rb +2 -0
  26. data/lib/slack/version.rb +1 -1
  27. data/lib/slack/web/api/endpoints.rb +2 -0
  28. data/lib/slack/web/api/endpoints/apps.rb +26 -0
  29. data/lib/slack/web/api/endpoints/chat.rb +30 -4
  30. data/lib/slack/web/api/endpoints/conversations.rb +2 -0
  31. data/lib/slack/web/api/endpoints/files.rb +8 -9
  32. data/lib/slack/web/api/endpoints/reactions.rb +2 -2
  33. data/lib/slack/web/api/patches/chat.6.block-kit-support.patch +69 -0
  34. data/lib/slack/web/pagination/cursor.rb +3 -0
  35. data/lib/tasks/real_time.rake +2 -0
  36. data/lib/tasks/web.rake +1 -0
  37. data/slack-ruby-client.gemspec +3 -2
  38. data/spec/integration/integration_spec.rb +64 -6
  39. data/spec/slack/events/config_spec.rb +29 -0
  40. data/spec/slack/events/request_spec.rb +121 -0
  41. data/spec/slack/real_time/client_spec.rb +36 -1
  42. data/spec/slack/real_time/concurrency/eventmachine_spec.rb +1 -0
  43. data/spec/slack/web/api/endpoints/apps_spec.rb +15 -0
  44. data/spec/slack/web/api/endpoints/custom_specs/chat_spec.rb +45 -24
  45. data/spec/spec_helper.rb +1 -0
  46. data/spec/support/queue_with_timeout.rb +4 -4
  47. metadata +29 -4
@@ -82,18 +82,22 @@ module Slack
82
82
  # @option options [user] :user
83
83
  # id of the user who will receive the ephemeral message. The user should be in the channel specified by the channel argument.
84
84
  # @option options [Object] :as_user
85
- # Pass true to post the message as the authed bot. Defaults to false.
85
+ # Pass true to post the message as the authed user. Defaults to true if the chat:write:bot scope is not included. Otherwise, defaults to false.
86
86
  # @option options [Object] :attachments
87
87
  # A JSON-based array of structured attachments, presented as a URL-encoded string.
88
+ # @option options [Object] :blocks
89
+ # A JSON-based array of structured blocks, presented as a URL-encoded string.
88
90
  # @option options [Object] :link_names
89
91
  # Find and link channel names and usernames.
90
92
  # @option options [Object] :parse
91
93
  # Change how messages are treated. Defaults to none. See below.
94
+ # @option options [Object] :thread_ts
95
+ # Provide another message's ts value to make this message a reply. Avoid using a reply's ts value; use its parent instead.
92
96
  # @see https://api.slack.com/methods/chat.postEphemeral
93
97
  # @see https://github.com/slack-ruby/slack-api-ref/blob/master/methods/chat/chat.postEphemeral.json
94
98
  def chat_postEphemeral(options = {})
95
99
  throw ArgumentError.new('Required arguments :channel missing') if options[:channel].nil?
96
- throw ArgumentError.new('Required arguments :text or :attachments missing') if options[:text].nil? && options[:attachments].nil?
100
+ throw ArgumentError.new('Required arguments :text, :attachments or :blocks missing') if options[:text].nil? && options[:attachments].nil? && options[:blocks].nil?
97
101
  throw ArgumentError.new('Required arguments :user missing') if options[:user].nil?
98
102
  options = options.merge(user: users_id(options)['user']['id']) if options[:user]
99
103
  # attachments must be passed as an encoded JSON string
@@ -102,6 +106,12 @@ module Slack
102
106
  attachments = JSON.dump(attachments) unless attachments.is_a?(String)
103
107
  options = options.merge(attachments: attachments)
104
108
  end
109
+ # blocks must be passed as an encoded JSON string
110
+ if options.key?(:blocks)
111
+ blocks = options[:blocks]
112
+ blocks = JSON.dump(blocks) unless blocks.is_a?(String)
113
+ options = options.merge(blocks: blocks)
114
+ end
105
115
  post('chat.postEphemeral', options)
106
116
  end
107
117
 
@@ -116,6 +126,8 @@ module Slack
116
126
  # Pass true to post the message as the authed user, instead of as a bot. Defaults to false. See authorship below.
117
127
  # @option options [Object] :attachments
118
128
  # A JSON-based array of structured attachments, presented as a URL-encoded string.
129
+ # @option options [Object] :blocks
130
+ # A JSON-based array of structured blocks, presented as a URL-encoded string.
119
131
  # @option options [Object] :icon_emoji
120
132
  # Emoji to use as the icon for this message. Overrides icon_url. Must be used in conjunction with as_user set to false, otherwise ignored. See authorship below.
121
133
  # @option options [Object] :icon_url
@@ -140,13 +152,19 @@ module Slack
140
152
  # @see https://github.com/slack-ruby/slack-api-ref/blob/master/methods/chat/chat.postMessage.json
141
153
  def chat_postMessage(options = {})
142
154
  throw ArgumentError.new('Required arguments :channel missing') if options[:channel].nil?
143
- throw ArgumentError.new('Required arguments :text or :attachments missing') if options[:text].nil? && options[:attachments].nil?
155
+ throw ArgumentError.new('Required arguments :text, :attachments or :blocks missing') if options[:text].nil? && options[:attachments].nil? && options[:blocks].nil?
144
156
  # attachments must be passed as an encoded JSON string
145
157
  if options.key?(:attachments)
146
158
  attachments = options[:attachments]
147
159
  attachments = JSON.dump(attachments) unless attachments.is_a?(String)
148
160
  options = options.merge(attachments: attachments)
149
161
  end
162
+ # blocks must be passed as an encoded JSON string
163
+ if options.key?(:blocks)
164
+ blocks = options[:blocks]
165
+ blocks = JSON.dump(blocks) unless blocks.is_a?(String)
166
+ options = options.merge(blocks: blocks)
167
+ end
150
168
  post('chat.postMessage', options)
151
169
  end
152
170
 
@@ -188,6 +206,8 @@ module Slack
188
206
  # Pass true to update the message as the authed user. Bot users in this context are considered authed users.
189
207
  # @option options [Object] :attachments
190
208
  # A JSON-based array of structured attachments, presented as a URL-encoded string. This field is required when not presenting text.
209
+ # @option options [Object] :blocks
210
+ # A JSON-based array of structured blocks, presented as a URL-encoded string.
191
211
  # @option options [Object] :link_names
192
212
  # Find and link channel names and usernames. Defaults to none. See below.
193
213
  # @option options [Object] :parse
@@ -196,7 +216,7 @@ module Slack
196
216
  # @see https://github.com/slack-ruby/slack-api-ref/blob/master/methods/chat/chat.update.json
197
217
  def chat_update(options = {})
198
218
  throw ArgumentError.new('Required arguments :channel missing') if options[:channel].nil?
199
- throw ArgumentError.new('Required arguments :text or :attachments missing') if options[:text].nil? && options[:attachments].nil?
219
+ throw ArgumentError.new('Required arguments :text, :attachments or :blocks missing') if options[:text].nil? && options[:attachments].nil? && options[:blocks].nil?
200
220
  throw ArgumentError.new('Required arguments :ts missing') if options[:ts].nil?
201
221
  options = options.merge(channel: channels_id(options)['channel']['id']) if options[:channel]
202
222
  # attachments must be passed as an encoded JSON string
@@ -205,6 +225,12 @@ module Slack
205
225
  attachments = JSON.dump(attachments) unless attachments.is_a?(String)
206
226
  options = options.merge(attachments: attachments)
207
227
  end
228
+ # blocks must be passed as an encoded JSON string
229
+ if options.key?(:blocks)
230
+ blocks = options[:blocks]
231
+ blocks = JSON.dump(blocks) unless blocks.is_a?(String)
232
+ options = options.merge(blocks: blocks)
233
+ end
208
234
  post('chat.update', options)
209
235
  end
210
236
  end
@@ -83,6 +83,8 @@ module Slack
83
83
  # Conversation ID to learn more about.
84
84
  # @option options [Object] :include_locale
85
85
  # Set this to true to receive the locale for this conversation. Defaults to false.
86
+ # @option options [Object] :include_num_members
87
+ # Set to true to include the member count for the specified conversation. Defaults to false.
86
88
  # @see https://api.slack.com/methods/conversations.info
87
89
  # @see https://github.com/slack-ruby/slack-api-ref/blob/master/methods/conversations/conversations.info.json
88
90
  def conversations_info(options = {})
@@ -67,17 +67,16 @@ module Slack
67
67
  # Filter files created before this timestamp (inclusive).
68
68
  # @option options [Object] :types
69
69
  # Filter files by type:
70
+ # * `all` - All files
71
+ # * `spaces` - Posts
72
+ # * `snippets` - Snippets
73
+ # * `images` - Image files
74
+ # * `gdocs` - Google docs
75
+ # * `zips` - Zip files
76
+ # * `pdfs` - PDF files
70
77
  #
71
- # all - All files
72
- # spaces - Posts
73
- # snippets - Snippets
74
- # images - Image files
75
- # gdocs - Google docs
76
- # zips - Zip files
77
- # pdfs - PDF files
78
+ # You can pass multiple values in the types argument, like `types=spaces,snippets`.The default value is `all`, which does not filter the list.
78
79
  #
79
- #
80
- # You can pass multiple values in the types argument, like types=spaces,snippets.The default value is all, which does not filter the list.
81
80
  # .
82
81
  # @option options [user] :user
83
82
  # Filter files created by a single user.
@@ -13,9 +13,9 @@ module Slack
13
13
  # @option options [channel] :channel
14
14
  # Channel where the message to add reaction to was posted.
15
15
  # @option options [file] :file
16
- # File to add reaction to.
16
+ # File to add reaction to. Now that file threads work the way you'd expect, this argument is deprecated. Specify the timestamp and channel of the message associated with a file instead.
17
17
  # @option options [Object] :file_comment
18
- # File comment to add reaction to.
18
+ # File comment to add reaction to. Now that file threads work the way you'd expect, this argument is deprecated. Specify the timestamp and channel of the message associated with a file instead.
19
19
  # @option options [Object] :timestamp
20
20
  # Timestamp of the message to add reaction to.
21
21
  # @see https://api.slack.com/methods/reactions.add
@@ -0,0 +1,69 @@
1
+ diff --git a/lib/slack/web/api/endpoints/chat.rb b/lib/slack/web/api/endpoints/chat.rb
2
+ index 54a7db1..c535bb5 100644
3
+ --- a/lib/slack/web/api/endpoints/chat.rb
4
+ +++ b/lib/slack/web/api/endpoints/chat.rb
5
+ @@ -97,7 +97,7 @@ module Slack
6
+ # @see https://github.com/slack-ruby/slack-api-ref/blob/master/methods/chat/chat.postEphemeral.json
7
+ def chat_postEphemeral(options = {})
8
+ throw ArgumentError.new('Required arguments :channel missing') if options[:channel].nil?
9
+ - throw ArgumentError.new('Required arguments :text or :attachments missing') if options[:text].nil? && options[:attachments].nil?
10
+ + throw ArgumentError.new('Required arguments :text, :attachments or :blocks missing') if options[:text].nil? && options[:attachments].nil? && options[:blocks].nil?
11
+ throw ArgumentError.new('Required arguments :user missing') if options[:user].nil?
12
+ options = options.merge(user: users_id(options)['user']['id']) if options[:user]
13
+ # attachments must be passed as an encoded JSON string
14
+ @@ -106,6 +106,12 @@ module Slack
15
+ attachments = JSON.dump(attachments) unless attachments.is_a?(String)
16
+ options = options.merge(attachments: attachments)
17
+ end
18
+ + # blocks must be passed as an encoded JSON string
19
+ + if options.key?(:blocks)
20
+ + blocks = options[:blocks]
21
+ + blocks = JSON.dump(blocks) unless blocks.is_a?(String)
22
+ + options = options.merge(blocks: blocks)
23
+ + end
24
+ post('chat.postEphemeral', options)
25
+ end
26
+
27
+ @@ -146,13 +152,19 @@ module Slack
28
+ # @see https://github.com/slack-ruby/slack-api-ref/blob/master/methods/chat/chat.postMessage.json
29
+ def chat_postMessage(options = {})
30
+ throw ArgumentError.new('Required arguments :channel missing') if options[:channel].nil?
31
+ - throw ArgumentError.new('Required arguments :text or :attachments missing') if options[:text].nil? && options[:attachments].nil?
32
+ + throw ArgumentError.new('Required arguments :text, :attachments or :blocks missing') if options[:text].nil? && options[:attachments].nil? && options[:blocks].nil?
33
+ # attachments must be passed as an encoded JSON string
34
+ if options.key?(:attachments)
35
+ attachments = options[:attachments]
36
+ attachments = JSON.dump(attachments) unless attachments.is_a?(String)
37
+ options = options.merge(attachments: attachments)
38
+ end
39
+ + # blocks must be passed as an encoded JSON string
40
+ + if options.key?(:blocks)
41
+ + blocks = options[:blocks]
42
+ + blocks = JSON.dump(blocks) unless blocks.is_a?(String)
43
+ + options = options.merge(blocks: blocks)
44
+ + end
45
+ post('chat.postMessage', options)
46
+ end
47
+
48
+ @@ -204,7 +216,7 @@ module Slack
49
+ # @see https://github.com/slack-ruby/slack-api-ref/blob/master/methods/chat/chat.update.json
50
+ def chat_update(options = {})
51
+ throw ArgumentError.new('Required arguments :channel missing') if options[:channel].nil?
52
+ - throw ArgumentError.new('Required arguments :text or :attachments missing') if options[:text].nil? && options[:attachments].nil?
53
+ + throw ArgumentError.new('Required arguments :text, :attachments or :blocks missing') if options[:text].nil? && options[:attachments].nil? && options[:blocks].nil?
54
+ throw ArgumentError.new('Required arguments :ts missing') if options[:ts].nil?
55
+ options = options.merge(channel: channels_id(options)['channel']['id']) if options[:channel]
56
+ # attachments must be passed as an encoded JSON string
57
+ @@ -213,6 +225,12 @@ module Slack
58
+ attachments = JSON.dump(attachments) unless attachments.is_a?(String)
59
+ options = options.merge(attachments: attachments)
60
+ end
61
+ + # blocks must be passed as an encoded JSON string
62
+ + if options.key?(:blocks)
63
+ + blocks = options[:blocks]
64
+ + blocks = JSON.dump(blocks) unless blocks.is_a?(String)
65
+ + options = options.merge(blocks: blocks)
66
+ + end
67
+ post('chat.update', options)
68
+ end
69
+ end
@@ -28,6 +28,7 @@ module Slack
28
28
  response = client.send(verb, query)
29
29
  rescue Slack::Web::Api::Errors::TooManyRequestsError => e
30
30
  raise e if retry_count >= max_retries
31
+
31
32
  client.logger.debug("#{self.class}##{__method__}") { e.to_s }
32
33
  retry_count += 1
33
34
  sleep(e.retry_after.seconds)
@@ -35,8 +36,10 @@ module Slack
35
36
  end
36
37
  yield response
37
38
  break unless response.response_metadata
39
+
38
40
  next_cursor = response.response_metadata.next_cursor
39
41
  break if next_cursor.blank?
42
+
40
43
  retry_count = 0
41
44
  sleep(sleep_interval) if sleep_interval
42
45
  end
@@ -15,12 +15,14 @@ namespace :slack do
15
15
  parsed = JSON.parse(File.read(path))
16
16
  JSON::Validator.validate(event_schema, parsed, insert_defaults: true)
17
17
  next if %w[message hello].include?(name)
18
+
18
19
  result[name] = parsed
19
20
  end
20
21
 
21
22
  event_handler_template = Erubis::Eruby.new(File.read('lib/slack/real_time/api/templates/event_handler.erb'))
22
23
  Dir.glob('lib/slack/real_time/stores/**/*.rb').each do |store_file|
23
24
  next if File.basename(store_file) == 'base.rb'
25
+
24
26
  STDOUT.write "#{File.basename(store_file)}:"
25
27
 
26
28
  store_file_contents = File.read(store_file)
@@ -53,6 +53,7 @@ namespace :slack do
53
53
  end
54
54
  # command
55
55
  raise "Missing group #{group}" unless groups.key?(group)
56
+
56
57
  rendered_command = command_template.result(group: groups[group], names: names)
57
58
  File.write "bin/commands/#{snaked_group}.rb", rendered_command
58
59
  end
@@ -1,4 +1,4 @@
1
- $LOAD_PATH.push File.expand_path('../lib', __FILE__)
1
+ $LOAD_PATH.push File.expand_path('lib', __dir__)
2
2
  require 'slack/version'
3
3
 
4
4
  Gem::Specification.new do |s|
@@ -26,7 +26,8 @@ Gem::Specification.new do |s|
26
26
  s.add_development_dependency 'json-schema'
27
27
  s.add_development_dependency 'rake', '~> 10'
28
28
  s.add_development_dependency 'rspec'
29
- s.add_development_dependency 'rubocop', '0.58.2'
29
+ s.add_development_dependency 'rubocop', '0.61.1'
30
+ s.add_development_dependency 'timecop'
30
31
  s.add_development_dependency 'vcr'
31
32
  s.add_development_dependency 'webmock'
32
33
  end
@@ -9,7 +9,7 @@ RSpec.describe 'integration test', skip: (!ENV['SLACK_API_TOKEN'] || !ENV['CONCU
9
9
 
10
10
  let(:logger) do
11
11
  logger = Logger.new(STDOUT)
12
- logger.level = Logger::DEBUG
12
+ logger.level = Logger::INFO
13
13
  logger
14
14
  end
15
15
 
@@ -19,16 +19,18 @@ RSpec.describe 'integration test', skip: (!ENV['SLACK_API_TOKEN'] || !ENV['CONCU
19
19
  Slack.configure do |slack|
20
20
  slack.logger = logger
21
21
  end
22
+
23
+ @queue = QueueWithTimeout.new
22
24
  end
23
25
 
24
26
  after do
25
27
  Slack.config.reset
26
28
  end
27
29
 
28
- let(:queue) { QueueWithTimeout.new }
29
-
30
30
  let(:client) { Slack::RealTime::Client.new(token: ENV['SLACK_API_TOKEN']) }
31
31
 
32
+ let(:queue) { @queue }
33
+
32
34
  def start
33
35
  # starts the client and pushes an item on a queue when connected
34
36
  client.start_async do |driver|
@@ -45,13 +47,14 @@ RSpec.describe 'integration test', skip: (!ENV['SLACK_API_TOKEN'] || !ENV['CONCU
45
47
  end
46
48
 
47
49
  client.on :close do
50
+ logger.info 'Disconnecting ...'
48
51
  # pushes another item to the queue when disconnected
49
- queue.push nil
52
+ queue.push nil if @queue
50
53
  end
51
54
  end
52
55
 
53
56
  def start_server
54
- dt = rand(5) + 2
57
+ dt = rand(2..6)
55
58
  logger.debug "#start_server, waiting #{dt} second(s)"
56
59
  sleep dt # prevent Slack 429 rate limit errors
57
60
  # start server and wait for on :open
@@ -61,9 +64,12 @@ RSpec.describe 'integration test', skip: (!ENV['SLACK_API_TOKEN'] || !ENV['CONCU
61
64
  end
62
65
 
63
66
  def wait_for_server
67
+ return unless @queue
68
+
64
69
  logger.debug '#wait_for_server'
65
70
  queue.pop_with_timeout(5)
66
71
  logger.debug '#wait_for_server, joined'
72
+ @queue = nil
67
73
  end
68
74
 
69
75
  def stop_server
@@ -82,7 +88,7 @@ RSpec.describe 'integration test', skip: (!ENV['SLACK_API_TOKEN'] || !ENV['CONCU
82
88
  start_server
83
89
  end
84
90
 
85
- let(:channel) { "@#{client.self.name}" }
91
+ let(:channel) { "@#{client.self.id}" }
86
92
 
87
93
  it 'responds to message' do
88
94
  message = SecureRandom.hex
@@ -91,6 +97,7 @@ RSpec.describe 'integration test', skip: (!ENV['SLACK_API_TOKEN'] || !ENV['CONCU
91
97
  logger.debug data
92
98
  # concurrent execution of tests causes messages to arrive in any order
93
99
  next unless data.text == message
100
+
94
101
  expect(data.text).to eq message
95
102
  expect(data.subtype).to eq 'bot_message'
96
103
  logger.debug 'client.stop!'
@@ -118,6 +125,57 @@ RSpec.describe 'integration test', skip: (!ENV['SLACK_API_TOKEN'] || !ENV['CONCU
118
125
  start_server
119
126
  end
120
127
 
128
+ context 'with websocket_ping set' do
129
+ before do
130
+ client.websocket_ping = 2
131
+ end
132
+ it 'sends pings' do
133
+ @reply_to = nil
134
+ client.on :pong do |data|
135
+ @reply_to = data.reply_to
136
+ queue.push nil
137
+ client.stop!
138
+ end
139
+ start_server
140
+ queue.pop_with_timeout(5)
141
+ expect(@reply_to).to be 1
142
+ end
143
+ it 'rebuilds the websocket connection when dropped' do
144
+ @reply_to = nil
145
+ client.on :pong do |data|
146
+ @reply_to = data.reply_to
147
+ if @reply_to == 1
148
+ client.instance_variable_get(:@socket).close
149
+ else
150
+ expect(@reply_to).to be 2
151
+ queue.push nil
152
+ client.stop!
153
+ end
154
+ end
155
+ start_server
156
+ queue.pop_with_timeout(10)
157
+ queue.pop_with_timeout(10)
158
+ end
159
+ end
160
+
161
+ context 'with websocket_ping not set' do
162
+ before do
163
+ client.websocket_ping = 0
164
+ end
165
+ it 'does not send pings' do
166
+ @reply_to = nil
167
+ client.on :pong do |data|
168
+ @reply_to = data.reply_to
169
+ end
170
+ client.on :hello do
171
+ client.stop!
172
+ end
173
+ start_server
174
+ wait_for_server
175
+ expect(@reply_to).to be nil
176
+ end
177
+ end
178
+
121
179
  it 'gets close, followed by closed' do
122
180
  client.on :hello do
123
181
  expect(client.started?).to be true
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Slack::Events::Config do
4
+ before do
5
+ ENV['SLACK_SIGNING_SECRET'] = 'secret'
6
+ Slack::Events::Config.reset
7
+ end
8
+ it 'defaults signing secret to ENV[SLACK_SIGNING_SECRET]' do
9
+ expect(Slack::Events.config.signing_secret).to eq 'secret'
10
+ end
11
+ it 'defaults signature expiration to 5 minutes' do
12
+ expect(Slack::Events.config.signature_expires_in).to eq 5 * 60
13
+ end
14
+ context 'configured' do
15
+ before do
16
+ Slack::Events.configure do |config|
17
+ config.signing_secret = 'custom'
18
+ config.signature_expires_in = 45
19
+ end
20
+ end
21
+ it 'uses the configured values' do
22
+ expect(Slack::Events.config.signing_secret).to eq 'custom'
23
+ expect(Slack::Events.config.signature_expires_in).to eq 45
24
+ end
25
+ end
26
+ after do
27
+ ENV.delete 'SLACK_SIGNING_SECRET'
28
+ end
29
+ end
@@ -0,0 +1,121 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Slack::Events::Request do
4
+ before do
5
+ Slack::Events.configure do |config|
6
+ config.signing_secret = 'ade6ca762ade4db0e7d31484cd616b9c'
7
+ config.signature_expires_in = 30
8
+ end
9
+ end
10
+ let(:signature) { 'v0=91177eea054d65de0fc0f9b4ec57714307bc0ce2c5f3bf0d28b1b720c8f92ba2' }
11
+ let(:timestamp) { '1547933148' }
12
+ let(:body) { '{"token":"X34FAqCu8tmGEkEEpoDncnja","challenge":"P7sFXA4o3HV2hTx4zb4zcQ9yrvuQs8pDh6EacOxmMRj0tJaXfQFF","type":"url_verification"}' }
13
+ let(:http_request) do
14
+ double(
15
+ headers: {
16
+ 'X-Slack-Request-Timestamp' => timestamp,
17
+ 'X-Slack-Signature' => signature
18
+ },
19
+ body: double(
20
+ read: body
21
+ )
22
+ )
23
+ end
24
+ subject do
25
+ Slack::Events::Request.new(http_request)
26
+ end
27
+ it 'reads http request' do
28
+ expect(subject.signature).to eq signature
29
+ expect(subject.body).to eq body
30
+ expect(subject.timestamp).to eq timestamp
31
+ expect(subject.version).to eq 'v0'
32
+ end
33
+ context 'time' do
34
+ after do
35
+ Timecop.return
36
+ end
37
+ context 'with an invalid signature' do
38
+ let(:signature) { 'v0=invalid' }
39
+ before do
40
+ Timecop.freeze(Time.at(timestamp.to_i))
41
+ end
42
+ it 'is invalid but not expired' do
43
+ expect(subject).to_not be_valid
44
+ expect(subject).to_not be_expired
45
+ end
46
+ end
47
+ context 'with an invalid body' do
48
+ let(:body) { 'invalid' }
49
+ before do
50
+ Timecop.freeze(Time.at(timestamp.to_i))
51
+ end
52
+ it 'is invalid but not expired' do
53
+ expect(subject).to_not be_valid
54
+ expect(subject).to_not be_expired
55
+ end
56
+ end
57
+ context 'with an invalid signing secret' do
58
+ before do
59
+ Slack::Events.configure do |config|
60
+ config.signing_secret = 'invalid'
61
+ end
62
+ Timecop.freeze(Time.at(timestamp.to_i))
63
+ end
64
+ it 'is invalid but not expired' do
65
+ expect(subject).to_not be_valid
66
+ expect(subject).to_not be_expired
67
+ end
68
+ end
69
+ context 'within time window' do
70
+ before do
71
+ Timecop.freeze(Time.at(timestamp.to_i) + Slack::Events.config.signature_expires_in - 1)
72
+ end
73
+ it 'is valid' do
74
+ expect(subject).to be_valid
75
+ expect(subject).to_not be_expired
76
+ end
77
+ it 'does not raise an error and returns true' do
78
+ expect(subject.verify!).to be true
79
+ end
80
+ end
81
+ context 'after time window' do
82
+ before do
83
+ Timecop.freeze(Time.at(timestamp.to_i) + Slack::Events.config.signature_expires_in + 1)
84
+ end
85
+ it 'is valid but expired' do
86
+ expect(subject).to be_valid
87
+ expect(subject).to be_expired
88
+ end
89
+ it 'raises an error on verify!' do
90
+ expect { subject.verify! }.to raise_error Slack::Events::Request::TimestampExpired
91
+ end
92
+ end
93
+ context 'before time but within window' do
94
+ before do
95
+ Timecop.freeze(Time.at(timestamp.to_i) - Slack::Events.config.signature_expires_in + 1)
96
+ end
97
+ it 'is valid and not expired' do
98
+ expect(subject).to be_valid
99
+ expect(subject).to_not be_expired
100
+ end
101
+ it 'does not raise an error on verify!' do
102
+ expect(subject.verify!).to be true
103
+ end
104
+ end
105
+ context 'before time window' do
106
+ before do
107
+ Timecop.freeze(Time.at(timestamp.to_i) - Slack::Events.config.signature_expires_in - 1)
108
+ end
109
+ it 'is valid but expired' do
110
+ expect(subject).to be_valid
111
+ expect(subject).to be_expired
112
+ end
113
+ it 'raises an error on verify!' do
114
+ expect { subject.verify! }.to raise_error Slack::Events::Request::TimestampExpired
115
+ end
116
+ end
117
+ end
118
+ after do
119
+ Slack::Events.config.reset
120
+ end
121
+ end