slack_message 1.5.0 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5a3275f61d16198e8a8283d22f1258dc9480ad7b66b977b6505fcfe79ef69fd
4
- data.tar.gz: 8cf6cdddbfbfcd1cb476aaa6384de6f94a7030fd813bdeef4aeda70e34811d2a
3
+ metadata.gz: 455886af2c6e775c37db510da7d5003abd131711107e9a18af2e10a714f9c154
4
+ data.tar.gz: '029d322491f3e63df60cd61eb2c6528132cce722a4ca0fa2db47fd790702fa04'
5
5
  SHA512:
6
- metadata.gz: 5a2395b51855660a9eba01115f3dbef9859c554c6365ede4d12c74353e72b117a763c0c526fe211f44b1489281fccb2a8fa3e53cc2883aa8cbc935d932598df8
7
- data.tar.gz: af50cc1f1bd0d93124115f34eae72bbe90e9309d79ec862b0a62e4d8b6c7bb0bc5e32e950bc9c7bc86f22fa30787c54d110974dcdd2f320d85b7afc0fb26dcc0
6
+ metadata.gz: dd4bc35189956140e5409d1d2e325e2cb6b9423e23fbcb6f81399281b10e4447b8c1bed8e8447f7dac59b5282e9c39b4907231225f0858abe6116fe9419b9269
7
+ data.tar.gz: d91718d74c17d0b1c79485de6311a70a579b1f80916c4328afa813800735b12d29e1069cfdc1721f1954839a6b55d25c0fa292ed41d02350ac0c1665a3475aa2
data/CHANGELOG.md CHANGED
@@ -2,11 +2,28 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.8.0] - 2021-10-07
6
+ - Added the ability to test in RSpec
7
+
8
+ ## [1.7.1] - 2021-10-06
9
+ - Fixed literally a syntax issue.
10
+ - Fixed specs.
11
+ - Fixed API to include JSON since consumers may not have loaded it.
12
+
13
+ ## [1.7.0] - 2021-10-06
14
+ - THIS RELEASE IS BADLY BROKEN.
15
+ - Added new error messages when API configuration is wrong / missing.
16
+ - Fixed issue with `instance_eval` and using methods within block.
17
+ - Fixed issue with sectionless `list_item`.
18
+
19
+ ## [1.6.0] - 2021-10-04
20
+ - Added `:default_channel` and `post_as` to deal with repetitive channel usage.
21
+
5
22
  ## [1.5.0] - 2021-10-01
6
- - Added `ol` and `ul` to sections w/ some formatting
23
+ - Added `ol` and `ul` to sections w/ some formatting.
7
24
 
8
25
  ## [1.4.0] - 2021-09-27
9
- - Moved image to accessory_image to differentiate between the image block
26
+ - Changed `image` to `accessory_image` to differentiate between the image block
10
27
  and the accessory image within a block.
11
28
 
12
29
  ## [1.3.0] - 2021-09-27
@@ -15,13 +32,12 @@
15
32
  - Added warnings for potentially invalid URLs.
16
33
 
17
34
  ## [1.2.0] - 2021-09-26
18
- - Turns out gemspec was broken. Fixed that.
35
+ - Fixed gemspec, which was entirely broken.
19
36
 
20
37
  ## [1.1.0] - 2021-09-26
21
38
  - Expanded the README significantly w/ usage instructions.
22
39
  - Added lots of error handling to requests.
23
40
 
24
41
  ## [1.0.0] - 2021-09-25
25
-
26
42
  - Added the base gem w/ a DSL for constructing blocks using sections.
27
43
  - Added a changelog, apparently.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- slack_message (1.2.0)
4
+ slack_message (1.7.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -55,6 +55,28 @@ SlackMessage.configure do |config|
55
55
  end
56
56
  ```
57
57
 
58
+ If you frequently ping the same channel with the same bot, and don't want to
59
+ continually specify the channel name, you can specify a default channel and
60
+ post using the `post_as` method. It is otherwise identical to `post_to`, but
61
+ allows you to omit the channel argument:
62
+
63
+ ```ruby
64
+ SlackMessage.configure do |config|
65
+ config.add_profile(:prod_alert_bot,
66
+ name: 'Prod Alert Bot',
67
+ url: ENV['SLACK_PROD_ALERT_WEBHOOK_URL'],
68
+ default_channel: '#red_alerts'
69
+ )
70
+ end
71
+
72
+ SlackMessage.post_as(:prod_alert_bot) do
73
+ text ":ambulance: weeooo weeooo something went wrong"
74
+ end
75
+ ```
76
+
77
+ Note that `post_as` does not allow you to choose a channel (because that's just
78
+ the same as using `post_to`), so you really do have to specify `default_channel`.
79
+
58
80
  #### Configuring User Search
59
81
 
60
82
  Slack's API no longer allows you to send DMs to users by username. You need to
@@ -192,6 +214,36 @@ SlackMessage.post_to('#general') do
192
214
  end
193
215
  ```
194
216
 
217
+ ### Testing
218
+
219
+ You can do some basic testing against SlackMessage, at least if you use RSpec!
220
+ You'll need to require and include the testing behavior like this, in your
221
+ spec_helper file:
222
+
223
+ ```ruby
224
+ require 'slack_message/rspec'
225
+
226
+ RSpec.configure do |config|
227
+ include SlackMessage::RSpec
228
+
229
+ # your other config
230
+ end
231
+ ```
232
+
233
+ This will stop API calls for posting messages, and will allow you access to
234
+ some custom matchers:
235
+
236
+ ```ruby
237
+ expect {
238
+ SlackMessage.post_to('#general') { text "foo" }
239
+ }.to post_slack_message_to('#general').with_content_matching(/foo/)
240
+ ```
241
+
242
+ Be forewarned, I'm frankly not that great at more complicated RSpec matchers,
243
+ so I'm guessing there are some bugs. Also, because the content of a message
244
+ gets turned into a complex JSON object, matching against content isn't capable
245
+ of very complicated regexes.
246
+
195
247
  Opinionated Stances
196
248
  ------------
197
249
 
@@ -203,6 +255,8 @@ opinionated stances on how to make use of that API. For instance:
203
255
  * Generally, same goes for the `emoji` flag on almost every text element.
204
256
  * It's possible to ask for a `blank_line` in sections, even though that concept
205
257
  isn't real. In this case, a text line containing only an emspace is rendered.
258
+ * It's easy to configure a bot for consistent name / channel use. My previous
259
+ use of SlackNotifier led to frequently inconsistent names.
206
260
 
207
261
  What it Doesn't Do
208
262
  ------------
@@ -218,11 +272,11 @@ DSL to include more of the block API itself.
218
272
 
219
273
  Also, some behaviors that are still planned but not yet added:
220
274
 
275
+ * some API documentation amirite?
221
276
  * allow custom http_options in configuration
222
277
  * more of BlockKit's options
223
278
  * any interactive elements at all (I don't understand them yet)
224
279
  * more interesting return types for your message
225
- * some way to specify default channel for a given profile (and omit param to post_to)
226
280
  * richer text formatting (ul is currently a hack)
227
281
 
228
282
  Contributing
@@ -1,5 +1,6 @@
1
1
  require 'net/http'
2
2
  require 'net/https'
3
+ require 'json'
3
4
 
4
5
  class SlackMessage::Api
5
6
  def self.user_id_for(email)
@@ -17,9 +18,16 @@ class SlackMessage::Api
17
18
 
18
19
  if response.code != "200"
19
20
  raise "Got an error back from the Slack API (HTTP #{response.code}):\n#{response.body}"
21
+ elsif response.body == ""
22
+ raise "Received empty 200 response from Slack when looking up user info. Check your API key."
23
+ end
24
+
25
+ begin
26
+ payload = JSON.parse(response.body)
27
+ rescue
28
+ raise "Unable to parse JSON response from Slack API\n#{response.body}"
20
29
  end
21
30
 
22
- payload = JSON.parse(response.body)
23
31
  if payload.include?("error") && payload["error"] == "invalid_auth"
24
32
  raise "Received an error because your authentication token isn't properly configured:\n#{response.body}"
25
33
  elsif payload.include?("error")
@@ -37,9 +45,9 @@ class SlackMessage::Api
37
45
  channel: target,
38
46
  username: profile[:name],
39
47
  blocks: payload
40
- }.to_json
48
+ }
41
49
 
42
- response = Net::HTTP.post_form uri, { payload: params }
50
+ response = execute_post_form(uri, params, profile[:handle])
43
51
 
44
52
  # let's try to be helpful about error messages
45
53
  if response.body == "invalid_token"
@@ -48,10 +56,16 @@ class SlackMessage::Api
48
56
  raise "Tried to send Slack message to non-existent channel or user '#{target}'"
49
57
  elsif response.body == "missing_text_or_fallback_or_attachments"
50
58
  raise "Tried to send Slack message with invalid payload."
59
+ elsif response.code == "302"
60
+ raise "Got 302 response while posting to Slack. Check your webhook URL for '#{profile[:handle]}'."
51
61
  elsif response.code != "200"
52
62
  raise "Got an error back from the Slack API (HTTP #{response.code}):\n#{response.body}"
53
63
  end
54
64
 
55
65
  response
56
66
  end
67
+
68
+ def self.execute_post_form(uri, params, _profile)
69
+ Net::HTTP.post_form uri, { payload: params.to_json }
70
+ end
57
71
  end
@@ -27,12 +27,16 @@ module SlackMessage::Configuration
27
27
 
28
28
  ###
29
29
 
30
- def self.add_profile(handle = :default, name:, url:)
30
+ def self.clear_profiles! # test harness, mainly
31
+ @@profiles = {}
32
+ end
33
+
34
+ def self.add_profile(handle = :default, name:, url:, default_channel: nil)
31
35
  if @@profiles.include?(handle)
32
36
  warn "WARNING: Overriding profile '#{handle}' in SlackMessage config"
33
37
  end
34
38
 
35
- @@profiles[handle] = { name: name, url: url, handle: handle }
39
+ @@profiles[handle] = { name: name, url: url, handle: handle, default_channel: default_channel }
36
40
  end
37
41
 
38
42
  def self.profile(handle, custom_name: nil)
@@ -3,7 +3,11 @@ class SlackMessage::Dsl
3
3
 
4
4
  EMSPACE = " " # unicode emspace
5
5
 
6
- def initialize
6
+ def initialize(block)
7
+ # Delegate missing methods to caller scope. Thanks 2008:
8
+ # https://www.dan-manges.com/blog/ruby-dsls-instance-eval-with-delegation
9
+ @caller_self = eval("self", block.binding)
10
+
7
11
  @body = []
8
12
  @default_section = Section.new
9
13
  @custom_bot_name = nil
@@ -81,13 +85,17 @@ class SlackMessage::Dsl
81
85
  @body
82
86
  end
83
87
 
88
+ def method_missing(meth, *args, &blk)
89
+ @caller_self.send meth, *args, &blk
90
+ end
91
+
84
92
  private
85
93
 
86
94
  # when doing things that would generate new top-levels, first try
87
95
  # to finish the implicit section.
88
96
  def finalize_default_section
89
97
  if default_section.has_content?
90
- @body.push(default_section.body)
98
+ @body.push(default_section.render)
91
99
  end
92
100
 
93
101
  @default_section = Section.new
@@ -0,0 +1,108 @@
1
+ require 'rspec/expectations'
2
+ require 'rspec/mocks'
3
+
4
+ # Honestly, this code is what happens when you do not understand the RSpec
5
+ # custom expectation API really at all, but you really want to create your
6
+ # matcher. This code is soo baaad.
7
+
8
+ module SlackMessage::RSpec
9
+ extend RSpec::Matchers::DSL
10
+
11
+ @@listeners = []
12
+
13
+ def self.register_expectation_listener(expectation_instance)
14
+ @@listeners << expectation_instance
15
+ end
16
+
17
+ def self.unregister_expectation_listener(expectation_instance)
18
+ @@listeners.delete(expectation_instance)
19
+ end
20
+
21
+ FauxResponse = Struct.new(:code, :body)
22
+
23
+ def self.included(_)
24
+ SlackMessage::Api.singleton_class.undef_method(:execute_post_form)
25
+ SlackMessage::Api.define_singleton_method(:execute_post_form) do |uri, params, profile|
26
+ @@listeners.each do |listener|
27
+ listener.record_call(params.merge(profile: profile, uri: uri))
28
+ end
29
+
30
+ return FauxResponse.new('200', 'ok')
31
+ end
32
+ end
33
+
34
+ matcher :post_slack_message_to do |expected|
35
+ match do |actual|
36
+ @instance ||= PostTo.new
37
+ @instance.with_channel(expected)
38
+
39
+ actual.call
40
+ @instance.enforce_expectations
41
+ end
42
+
43
+ chain :with_content_matching do |content_expectation|
44
+ @instance ||= PostTo.new
45
+ @instance.with_content_matching(content_expectation)
46
+ end
47
+
48
+ failure_message { @instance.failure_message }
49
+ failure_message_when_negated { @instance.failure_message_when_negated }
50
+
51
+ supports_block_expectations
52
+ end
53
+
54
+ class PostTo
55
+ def initialize
56
+ @captured_calls = []
57
+ @content_expectation = nil
58
+ @channel = nil
59
+
60
+ SlackMessage::RSpec.register_expectation_listener(self)
61
+ end
62
+
63
+ def record_call(deets)
64
+ @captured_calls.push(deets)
65
+ end
66
+
67
+ def with_channel(channel)
68
+ @channel = channel
69
+ end
70
+
71
+ def with_content_matching(content_expectation)
72
+ raise ArgumentError unless content_expectation.is_a? Regexp
73
+ @content_expectation = content_expectation
74
+ end
75
+
76
+ def enforce_expectations
77
+ SlackMessage::RSpec.unregister_expectation_listener(self)
78
+ matching_messages.any? { |msg| body_matches_expectation?(msg.fetch(:blocks)) }
79
+ end
80
+
81
+ def matching_messages
82
+ @captured_calls.select { |c| c[:channel] == @channel }
83
+ end
84
+
85
+ def body_matches_expectation?(sent)
86
+ return true unless @content_expectation
87
+
88
+ sent.to_s =~ @content_expectation
89
+ end
90
+
91
+ def failure_message
92
+ if @content_expectation
93
+ "expected block to post slack message to '#{@channel}' with content matching #{@content_expectation.inspect}"
94
+ else
95
+ "expected block to post slack message to '#{@channel}'"
96
+ end
97
+ end
98
+
99
+ # TODO: does content_matching even make sense for negated test?
100
+ def failure_message_when_negated
101
+ if @content_expectation
102
+ "expected block not to post slack message to '#{@channel}' with content matching #{@content_expectation.inspect}"
103
+ else
104
+ "expected block not to post slack message to '#{@channel}'"
105
+ end
106
+ end
107
+ end
108
+ end
data/lib/slack_message.rb CHANGED
@@ -11,12 +11,12 @@ module SlackMessage
11
11
  configuration.configure(&block)
12
12
  end
13
13
 
14
- def self.user_id_for(email)
14
+ def self.user_id_for(email) # spooky undocumented public method 👻
15
15
  Api::user_id_for(email)
16
16
  end
17
17
 
18
18
  def self.post_to(target, as: :default, &block)
19
- payload = Dsl.new.tap do |instance|
19
+ payload = Dsl.new(block).tap do |instance|
20
20
  instance.instance_eval(&block)
21
21
  end
22
22
 
@@ -26,8 +26,24 @@ module SlackMessage
26
26
  Api.post(payload.render, target, profile)
27
27
  end
28
28
 
29
+ def self.post_as(profile_name, &block)
30
+ payload = Dsl.new(block).tap do |instance|
31
+ instance.instance_eval(&block)
32
+ end
33
+
34
+ profile = Configuration.profile(profile_name, custom_name: payload.custom_bot_name)
35
+ if profile[:default_channel].nil?
36
+ raise ArgumentError, "Sorry, you need to specify a default_channel for profile #{profile_name} to use post_as"
37
+ end
38
+
39
+ target = profile[:default_channel]
40
+ target = user_id_for(target) if target =~ /^\S{1,}@\S{2,}\.\S{2,}$/
41
+
42
+ Api.post(payload.render, target, profile)
43
+ end
44
+
29
45
  def self.build(&block)
30
- Dsl.new.tap do |instance|
46
+ Dsl.new(block).tap do |instance|
31
47
  instance.instance_eval(&block)
32
48
  end.send(:render)
33
49
  end
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |gem|
2
2
  gem.name = 'slack_message'
3
- gem.version = "1.5.0"
3
+ gem.version = "1.8.0"
4
4
  gem.summary = "A nice DSL for composing rich messages in Slack"
5
5
  gem.authors = ["Joe Mastey"]
6
6
  gem.email = 'hello@joemastey.com'
@@ -1,15 +1,12 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  RSpec.describe SlackMessage do
4
- it "includes a bunch of stuff" do
5
- expect(SlackMessage).to respond_to(:post_to)
6
- end
7
-
8
4
  describe "API convenience" do
9
5
  it "can grab user IDs" do
10
- allow(SlackMessage::Api).to receive(:api_request)
11
- .with(/hello@joemastey.com/)
12
- .and_return({ "user" => { "id" => "ABC123" }})
6
+ SlackMessage.configure { |c| c.api_token = "asdf" }
7
+ allow(Net::HTTP).to receive(:start).and_return(
8
+ double(code: "200", body: '{ "user": { "id": "ABC123" }}')
9
+ )
13
10
 
14
11
  result = SlackMessage.user_id_for("hello@joemastey.com")
15
12
  expect(result).to eq("ABC123")
@@ -63,7 +60,6 @@ RSpec.describe SlackMessage do
63
60
  config.add_profile(:nonstandard, name: 'another profile', url: 'http://hooks.slack.com/1234/')
64
61
  end
65
62
 
66
-
67
63
  expect(SlackMessage.configuration.profile(:default)[:name]).to eq('default profile')
68
64
  expect(SlackMessage.configuration.profile(:nonstandard)[:name]).to eq('another profile')
69
65
 
@@ -72,4 +68,31 @@ RSpec.describe SlackMessage do
72
68
  }.to raise_error(ArgumentError)
73
69
  end
74
70
  end
71
+
72
+ describe "custom expectations" do
73
+ before do
74
+ SlackMessage.configure do |config|
75
+ config.clear_profiles!
76
+ config.add_profile(name: 'default profile', url: 'http://hooks.slack.com/1234/')
77
+ end
78
+ end
79
+
80
+ it "can assert expectations against posts" do
81
+ expect {
82
+ SlackMessage.post_to('#lieutenant') { text "foo" }
83
+ }.not_to post_slack_message_to('#general')
84
+
85
+ expect {
86
+ SlackMessage.post_to('#general') { text "foo" }
87
+ }.to post_slack_message_to('#general').with_content_matching(/foo/)
88
+ end
89
+
90
+ it "is not stateful" do
91
+ expect {
92
+ SlackMessage.post_to('#general') { text "foo" }
93
+ }.to post_slack_message_to('#general')
94
+
95
+ expect { }.not_to post_slack_message_to('#general')
96
+ end
97
+ end
75
98
  end
data/spec/spec_helper.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require_relative '../lib/slack_message'
2
+ require_relative '../lib/slack_message/rspec'
2
3
 
3
4
  # This file was generated by the `rspec --init` command. Conventionally, all
4
5
  # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
@@ -49,4 +50,6 @@ RSpec.configure do |config|
49
50
  # test failures related to randomization by passing the same `--seed` value
50
51
  # as the one that triggered the failure.
51
52
  Kernel.srand config.seed
53
+
54
+ include SlackMessage::RSpec
52
55
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slack_message
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joe Mastey
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-10-01 00:00:00.000000000 Z
11
+ date: 2021-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -70,6 +70,7 @@ files:
70
70
  - lib/slack_message/api.rb
71
71
  - lib/slack_message/configuration.rb
72
72
  - lib/slack_message/dsl.rb
73
+ - lib/slack_message/rspec.rb
73
74
  - slack_message.gemspec
74
75
  - spec/slack_message_spec.rb
75
76
  - spec/spec_helper.rb