slack-ruby-client 0.13.1 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
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