slack_message 1.6.0 → 1.8.1

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: e66bdedefd2ac86c6c49d974e5f5d0474cc9b3756ce3a64e52df66ccc6e39cf4
4
- data.tar.gz: b085edf556d957516d1cbbc9b260b7aff0e5d34b4561d27ae0d5f6b2dda9c93b
3
+ metadata.gz: 94e7433c5374c318cd955ad43f6a02705b62876498253c4e7587a7ba5ae5277f
4
+ data.tar.gz: 0ca96c1266c9dc55af30c6df7f07e2ea86a64bc5fad99258a10efe10e4127e9b
5
5
  SHA512:
6
- metadata.gz: ea935db68ca4771e11b40828d5cbd2fef02cf10c248fab3ede05ea82aef6a3a6d7ad96953debdc9214833ee0f281222e5e09adefd923c70d21090c996dba31a0
7
- data.tar.gz: 46d2163aa9ec0e9df582700575d0c8b47b389b808f93275129025bc277086eb02fbeb4caa0754408dbc6d6fba9f06b6d8720493280550e0d3664bc735059f7c1
6
+ metadata.gz: 2f4c15c91c6a7741c1f387b820cc36689cd7530233e9582fb3db73407d9f39eb69656a071b65789cd33ec359d5fa1478efe212336a3834202c30f96269885895
7
+ data.tar.gz: ed86a645c481c0c91510acedc5b0abd2bf3923f9ea33e9a23671db0e107005c07f64a1dfc52f4f5f47f826630712756bd5ce43ddfd06622a10ed24afa7731465
data/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.8.1] - 2021-10-08
6
+ - Cleaned that rspec code a bit, added more matchers for real world use.
7
+
8
+ ## [1.8.0] - 2021-10-07
9
+ - Added the ability to test in RSpec.
10
+
11
+ ## [1.7.1] - 2021-10-06
12
+ - Fixed literally a syntax issue.
13
+ - Fixed specs.
14
+ - Fixed API to include JSON since consumers may not have loaded it.
15
+
16
+ ## [1.7.0] - 2021-10-06
17
+ - THIS RELEASE IS BADLY BROKEN.
18
+ - Added new error messages when API configuration is wrong / missing.
19
+ - Fixed issue with `instance_eval` and using methods within block.
20
+ - Fixed issue with sectionless `list_item`.
21
+
5
22
  ## [1.6.0] - 2021-10-04
6
23
  - Added `:default_channel` and `post_as` to deal with repetitive channel usage.
7
24
 
@@ -9,7 +26,7 @@
9
26
  - Added `ol` and `ul` to sections w/ some formatting.
10
27
 
11
28
  ## [1.4.0] - 2021-09-27
12
- - Moved image to accessory_image to differentiate between the image block
29
+ - Changed `image` to `accessory_image` to differentiate between the image block
13
30
  and the accessory image within a block.
14
31
 
15
32
  ## [1.3.0] - 2021-09-27
@@ -18,13 +35,12 @@
18
35
  - Added warnings for potentially invalid URLs.
19
36
 
20
37
  ## [1.2.0] - 2021-09-26
21
- - Turns out gemspec was broken. Fixed that.
38
+ - Fixed gemspec, which was entirely broken.
22
39
 
23
40
  ## [1.1.0] - 2021-09-26
24
41
  - Expanded the README significantly w/ usage instructions.
25
42
  - Added lots of error handling to requests.
26
43
 
27
44
  ## [1.0.0] - 2021-09-25
28
-
29
45
  - Added the base gem w/ a DSL for constructing blocks using sections.
30
46
  - 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.8.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -214,6 +214,36 @@ SlackMessage.post_to('#general') do
214
214
  end
215
215
  ```
216
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
+
217
247
  Opinionated Stances
218
248
  ------------
219
249
 
@@ -225,6 +255,8 @@ opinionated stances on how to make use of that API. For instance:
225
255
  * Generally, same goes for the `emoji` flag on almost every text element.
226
256
  * It's possible to ask for a `blank_line` in sections, even though that concept
227
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.
228
260
 
229
261
  What it Doesn't Do
230
262
  ------------
@@ -240,11 +272,11 @@ DSL to include more of the block API itself.
240
272
 
241
273
  Also, some behaviors that are still planned but not yet added:
242
274
 
275
+ * some API documentation amirite?
243
276
  * allow custom http_options in configuration
244
277
  * more of BlockKit's options
245
278
  * any interactive elements at all (I don't understand them yet)
246
279
  * more interesting return types for your message
247
- * some way to specify default channel for a given profile (and omit param to post_to)
248
280
  * richer text formatting (ul is currently a hack)
249
281
 
250
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,6 +27,10 @@ module SlackMessage::Configuration
27
27
 
28
28
  ###
29
29
 
30
+ def self.clear_profiles! # test harness, mainly
31
+ @@profiles = {}
32
+ end
33
+
30
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"
@@ -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,168 @@
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
+ # We override API calls by entirely replacing the low-level API method. Then we
9
+ # use our overridden version to capture and record calls. When someone creates
10
+ # a new expectation, an object is created, so we allow that object to register
11
+ # itself to receive notification when a slack message _would have_ been posted.
12
+ #
13
+ # Then once the expectation is fulfilled, that class unregisters itself so that
14
+ # it can be cleaned up properly.
15
+ #
16
+
17
+ module SlackMessage::RSpec
18
+ extend RSpec::Matchers::DSL
19
+
20
+ @@listeners = []
21
+
22
+ def self.register_expectation_listener(expectation_instance)
23
+ @@listeners << expectation_instance
24
+ end
25
+
26
+ def self.unregister_expectation_listener(expectation_instance)
27
+ @@listeners.delete(expectation_instance)
28
+ end
29
+
30
+ FauxResponse = Struct.new(:code, :body)
31
+
32
+ def self.included(_)
33
+ SlackMessage::Api.singleton_class.undef_method(:execute_post_form)
34
+ SlackMessage::Api.define_singleton_method(:execute_post_form) do |uri, params, profile|
35
+ @@listeners.each do |listener|
36
+ listener.record_call(params.merge(profile: profile, uri: uri))
37
+ end
38
+
39
+ return FauxResponse.new('200', 'ok')
40
+ end
41
+ end
42
+
43
+ # w/ channel
44
+ matcher :post_slack_message_to do |expected|
45
+ match do |actual|
46
+ @instance ||= PostTo.new
47
+ @instance.with_channel(expected)
48
+
49
+ actual.call
50
+ @instance.enforce_expectations
51
+ end
52
+
53
+ chain :with_content_matching do |content|
54
+ @instance ||= PostTo.new
55
+ @instance.with_content_matching(content)
56
+ end
57
+
58
+ failure_message { @instance.failure_message }
59
+ failure_message_when_negated { @instance.failure_message_when_negated }
60
+
61
+ supports_block_expectations
62
+ end
63
+
64
+ # no channel
65
+ matcher :post_to_slack do |expected|
66
+ match do |actual|
67
+ @instance ||= PostTo.new
68
+
69
+ actual.call
70
+ @instance.enforce_expectations
71
+ end
72
+
73
+ chain :with_content_matching do |content|
74
+ @instance ||= PostTo.new
75
+ @instance.with_content_matching(content)
76
+ end
77
+
78
+ failure_message { @instance.failure_message }
79
+ failure_message_when_negated { @instance.failure_message_when_negated }
80
+
81
+ supports_block_expectations
82
+ end
83
+
84
+ # name / profile matcher
85
+ matcher :post_slack_message_as do |expected|
86
+ match do |actual|
87
+ @instance ||= PostTo.new
88
+ @instance.with_profile(expected)
89
+
90
+ actual.call
91
+ @instance.enforce_expectations
92
+ end
93
+
94
+ chain :with_content_matching do |content|
95
+ @instance ||= PostTo.new
96
+ @instance.with_content_matching(content)
97
+ end
98
+
99
+ failure_message { @instance.failure_message }
100
+ failure_message_when_negated { @instance.failure_message_when_negated }
101
+
102
+ supports_block_expectations
103
+ end
104
+
105
+ class PostTo
106
+ def initialize
107
+ @captured_calls = []
108
+ @content = nil
109
+ @channel = nil
110
+ @profile = nil
111
+
112
+ SlackMessage::RSpec.register_expectation_listener(self)
113
+ end
114
+
115
+ def record_call(deets)
116
+ @captured_calls.push(deets)
117
+ end
118
+
119
+ def with_channel(channel)
120
+ @channel = channel
121
+ end
122
+
123
+ def with_content_matching(content)
124
+ raise ArgumentError unless content.is_a? Regexp
125
+ @content = content
126
+ end
127
+
128
+ def with_profile(profile)
129
+ @profile = profile
130
+ end
131
+
132
+ def enforce_expectations
133
+ SlackMessage::RSpec.unregister_expectation_listener(self)
134
+
135
+ @captured_calls
136
+ .filter { |call| !@channel || call[:channel] == @channel }
137
+ .filter { |call| !@profile || [call[:profile], call[:username]].include?(@profile) }
138
+ .filter { |call| !@content || call.fetch(:blocks).to_s =~ @content }
139
+ .any?
140
+ end
141
+
142
+ def failure_message
143
+ "expected block to #{failure_expression}"
144
+ end
145
+
146
+ def failure_message_when_negated
147
+ "expected block not to #{failure_expression}"
148
+ end
149
+
150
+ def failure_expression
151
+ concat = []
152
+
153
+ if @channel
154
+ concat << "post a slack message to '#{@channel}'"
155
+ elsif @profile
156
+ concat << "post a slack message as '#{@profile}'"
157
+ else
158
+ concat << "post a slack message"
159
+ end
160
+
161
+ if @content
162
+ concat << "with content matching #{@content.inspect}"
163
+ end
164
+
165
+ concat.join " "
166
+ end
167
+ end
168
+ end
data/lib/slack_message.rb CHANGED
@@ -16,7 +16,7 @@ module SlackMessage
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
 
@@ -27,7 +27,7 @@ module SlackMessage
27
27
  end
28
28
 
29
29
  def self.post_as(profile_name, &block)
30
- payload = Dsl.new.tap do |instance|
30
+ payload = Dsl.new(block).tap do |instance|
31
31
  instance.instance_eval(&block)
32
32
  end
33
33
 
@@ -43,7 +43,7 @@ module SlackMessage
43
43
  end
44
44
 
45
45
  def self.build(&block)
46
- Dsl.new.tap do |instance|
46
+ Dsl.new(block).tap do |instance|
47
47
  instance.instance_eval(&block)
48
48
  end.send(:render)
49
49
  end
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |gem|
2
2
  gem.name = 'slack_message'
3
- gem.version = "1.6.0"
3
+ gem.version = "1.8.1"
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")
@@ -49,7 +46,7 @@ RSpec.describe SlackMessage do
49
46
 
50
47
  it "raises errors for missing configuration" do
51
48
  SlackMessage.configure do |config|
52
- #config.api_token = "abc123"
49
+ config.api_token = nil
53
50
  end
54
51
 
55
52
  expect {
@@ -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,55 @@ 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 "resets state properly" 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
+
98
+ it "lets you assert by profile name" do
99
+ SlackMessage.configure do |config|
100
+ config.add_profile(:schmoebot, name: 'Schmoe', url: 'http://hooks.slack.com/1234/', default_channel: '#schmoes')
101
+ end
102
+
103
+ expect {
104
+ SlackMessage.post_as(:schmoebot) { text "foo" }
105
+ }.to post_slack_message_to('#schmoes')
106
+
107
+ expect {
108
+ SlackMessage.post_as(:schmoebot) { text "foo" }
109
+ }.to post_slack_message_as(:schmoebot)
110
+
111
+ expect {
112
+ SlackMessage.post_as(:schmoebot) { text "foo" }
113
+ }.to post_slack_message_as('Schmoe').with_content_matching(/foo/)
114
+ end
115
+
116
+ it "can assert more generally too tbh" do
117
+ expect {
118
+ SlackMessage.post_to('#general') { text "foo" }
119
+ }.to post_to_slack.with_content_matching(/foo/)
120
+ end
121
+ end
75
122
  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.6.0
4
+ version: 1.8.1
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-04 00:00:00.000000000 Z
11
+ date: 2021-10-08 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