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 +4 -4
- data/CHANGELOG.md +20 -4
- data/Gemfile.lock +1 -1
- data/README.md +55 -1
- data/lib/slack_message/api.rb +17 -3
- data/lib/slack_message/configuration.rb +6 -2
- data/lib/slack_message/dsl.rb +10 -2
- data/lib/slack_message/rspec.rb +108 -0
- data/lib/slack_message.rb +19 -3
- data/slack_message.gemspec +1 -1
- data/spec/slack_message_spec.rb +31 -8
- data/spec/spec_helper.rb +3 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 455886af2c6e775c37db510da7d5003abd131711107e9a18af2e10a714f9c154
|
4
|
+
data.tar.gz: '029d322491f3e63df60cd61eb2c6528132cce722a4ca0fa2db47fd790702fa04'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
-
|
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
|
-
-
|
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
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
|
data/lib/slack_message/api.rb
CHANGED
@@ -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
|
-
}
|
48
|
+
}
|
41
49
|
|
42
|
-
response =
|
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.
|
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)
|
data/lib/slack_message/dsl.rb
CHANGED
@@ -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.
|
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
|
data/slack_message.gemspec
CHANGED
data/spec/slack_message_spec.rb
CHANGED
@@ -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
|
-
|
11
|
-
|
12
|
-
|
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.
|
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-
|
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
|