slack_message 1.5.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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