slack_message 1.7.0 → 1.9.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: a2cd232d81a66a48c9543a5b910c335bca7a82474a60472d03f7cffab3fce0d0
4
- data.tar.gz: c27312311ff02ee414f2882461fe5ea9b5c07d9d8e99847ae7af33faf4a31283
3
+ metadata.gz: f4d33554e024d1c51aeb2bb6ee5397c16142dbcc1a314720f30e7fab3d12325d
4
+ data.tar.gz: 6e297c58c27ca6109d51fc5b267c3248928deaf7422d57e6b9e9aa0533e525b5
5
5
  SHA512:
6
- metadata.gz: 69352d5eb8d4f6837f66ba042817bfe0a51c3483356bbc64ded0cbc60fc7e72c084ea39df542a93bf79a6c0a8c4ad860cdb9791f28d807edb5b1c859b13f129f
7
- data.tar.gz: 740af84829c6b258b4788b5f83901f242a5732e98737781efb288ee426ed232726823d9fdde453da6c7d83eb55607733e1d4b55757580676921b37abba1c70ad
6
+ metadata.gz: bf1e3caebf3d58c238c46be5f3b62a925ade7d077998a7a73ac1c9fdb3d5f0859ecb6c0f338ad2cc666a7157d166cd8cda4fa91e9e5dacb3a77bf168cb9ccf50
7
+ data.tar.gz: '08de1a2fe10e3da489169e11d478e7afb93fd557e6da00d1af1a42f6697a351f3b281ea66aafd80594738f6b25457fdb18c9a5bf766caee339d2b7cc5db51e02'
data/CHANGELOG.md CHANGED
@@ -2,7 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.9.0] - 2021-10-27
6
+ - Add many validations so that trying to add e.g. empty text won't succeed.
7
+ Previously that would be accepted but return `invalid_blocks` from the API.
8
+
9
+ ## [1.8.1] - 2021-10-08
10
+ - Cleaned that rspec code a bit, added more matchers for real world use.
11
+
12
+ ## [1.8.0] - 2021-10-07
13
+ - Added the ability to test in RSpec.
14
+
15
+ ## [1.7.1] - 2021-10-06
16
+ - Fixed literally a syntax issue.
17
+ - Fixed specs.
18
+ - Fixed API to include JSON since consumers may not have loaded it.
19
+
5
20
  ## [1.7.0] - 2021-10-06
21
+ - THIS RELEASE IS BADLY BROKEN.
6
22
  - Added new error messages when API configuration is wrong / missing.
7
23
  - Fixed issue with `instance_eval` and using methods within block.
8
24
  - Fixed issue with sectionless `list_item`.
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.9.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -214,6 +214,48 @@ 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
+ expect {
242
+ SlackMessage.post_as(:schmoebot) { text "foo" }
243
+ }.to post_slack_message_as(:schmoebot)
244
+
245
+ expect {
246
+ SlackMessage.post_as(:schmoebot) { text "foo" }
247
+ }.to post_slack_message_as('Schmoe Bot')
248
+
249
+ expect {
250
+ SlackMessage.post_to('#general') { text "foo" }
251
+ }.to post_to_slack
252
+ ```
253
+
254
+ Be forewarned, I'm frankly not that great at more complicated RSpec matchers,
255
+ so I'm guessing there are some bugs. Also, because the content of a message
256
+ gets turned into a complex JSON object, matching against content isn't capable
257
+ of very complicated regexes.
258
+
217
259
  Opinionated Stances
218
260
  ------------
219
261
 
@@ -225,6 +267,8 @@ opinionated stances on how to make use of that API. For instance:
225
267
  * Generally, same goes for the `emoji` flag on almost every text element.
226
268
  * It's possible to ask for a `blank_line` in sections, even though that concept
227
269
  isn't real. In this case, a text line containing only an emspace is rendered.
270
+ * It's easy to configure a bot for consistent name / channel use. My previous
271
+ use of SlackNotifier led to frequently inconsistent names.
228
272
 
229
273
  What it Doesn't Do
230
274
  ------------
@@ -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)
@@ -44,9 +45,9 @@ class SlackMessage::Api
44
45
  channel: target,
45
46
  username: profile[:name],
46
47
  blocks: payload
47
- }.to_json
48
+ }
48
49
 
49
- response = Net::HTTP.post_form uri, { payload: params }
50
+ response = execute_post_form(uri, params, profile[:handle])
50
51
 
51
52
  # let's try to be helpful about error messages
52
53
  if response.body == "invalid_token"
@@ -63,4 +64,8 @@ class SlackMessage::Api
63
64
 
64
65
  response
65
66
  end
67
+
68
+ def self.execute_post_form(uri, params, _profile)
69
+ Net::HTTP.post_form uri, { payload: params.to_json }
70
+ end
66
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"
@@ -6,7 +6,7 @@ class SlackMessage::Dsl
6
6
  def initialize(block)
7
7
  # Delegate missing methods to caller scope. Thanks 2008:
8
8
  # https://www.dan-manges.com/blog/ruby-dsls-instance-eval-with-delegation
9
- @caller_self = eval("self", block.binding
9
+ @caller_self = eval("self", block.binding)
10
10
 
11
11
  @body = []
12
12
  @default_section = Section.new
@@ -52,6 +52,10 @@ class SlackMessage::Dsl
52
52
  def context(text)
53
53
  finalize_default_section
54
54
 
55
+ if text == "" || text.nil?
56
+ raise ArgumentError, "tried to create a context block without a value"
57
+ end
58
+
55
59
  @body.push({ type: "context", elements: [{
56
60
  type: "mrkdwn", text: text
57
61
  }]})
@@ -110,6 +114,10 @@ class SlackMessage::Dsl
110
114
  end
111
115
 
112
116
  def text(msg)
117
+ if msg == "" || msg.nil?
118
+ raise ArgumentError, "tried to create text node without a value"
119
+ end
120
+
113
121
  if @body.include?(:text)
114
122
  @body[:text][:text] << "\n#{msg}"
115
123
 
@@ -119,14 +127,16 @@ class SlackMessage::Dsl
119
127
  end
120
128
 
121
129
  def ul(elements)
122
- raise Arguments, "please pass an array" unless elements.respond_to?(:map)
130
+ raise ArgumentError, "please pass an array" unless elements.respond_to?(:map)
131
+
123
132
  text(
124
133
  elements.map { |text| "#{EMSPACE}• #{text}" }.join("\n")
125
134
  )
126
135
  end
127
136
 
128
137
  def ol(elements)
129
- raise Arguments, "please pass an array" unless elements.respond_to?(:map)
138
+ raise ArgumentError, "please pass an array" unless elements.respond_to?(:map)
139
+
130
140
  text(
131
141
  elements.map.with_index(1) { |text, idx| "#{EMSPACE}#{idx}. #{text}" }.join("\n")
132
142
  )
@@ -186,6 +196,10 @@ class SlackMessage::Dsl
186
196
  end
187
197
 
188
198
  def list_item(title, value)
199
+ if value == "" || value.nil?
200
+ raise ArgumentError, "can't create a list item for '#{title}' without a value"
201
+ end
202
+
189
203
  @list.add(title, value)
190
204
  end
191
205
 
@@ -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
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |gem|
2
2
  gem.name = 'slack_message'
3
- gem.version = "1.7.0"
3
+ gem.version = "1.9.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")
@@ -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.7.0
4
+ version: 1.9.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-06 00:00:00.000000000 Z
11
+ date: 2021-10-28 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