teams_connector 0.1.1 → 0.1.5

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: c6a72ee260e0976efb58e9871414f88ea25090fef32d9786a9d0b2b5dad252be
4
- data.tar.gz: 8a17c4cf19aa3313bd2361a2a2ca5524ba354d6be8079f594ef065209fd60c44
3
+ metadata.gz: 9ed38230a562264b9117ecea0aef7cd66b1bb7d947056f5b892e88cfaed906af
4
+ data.tar.gz: e3d01cfa5b8de620938e3f8bbd0c012591dc1ecbaf5b9dc742b03801b20c4d00
5
5
  SHA512:
6
- metadata.gz: 51b2a71c1d50f252f1804a037e546d9e240707553f7052128b31191e1c28316177a95afb598eadfff11d1a7dd414eaa7d53294dd64da859fc9d3c6f73112af74
7
- data.tar.gz: 9f39957ce3655246af7404d99b1f7281c0cc200257faf3ccf4c64ec02d45cd83ef08264e47f8769c0fc2affab1a7cfedf9ce10e60a876a8b9cff21aed2fd6a89
6
+ metadata.gz: 683b6a1095609d9f13263c71d7d17c4c466bd3041c334675985d13d4f2e9fd6f9b484354f21779c07b1ea4574fb11e3b10b6fae5e9d5f8fe9fdf2b34f72c27ea
7
+ data.tar.gz: c0144642fff244b53410dc78f4f6f59570f6354af3f431c6d429878c2fd7c9af377edcafe55a020b08765e40db11bac5a64819321a350d4d1ed05d92379645f6
data/CHANGES.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Teams Connector Changelog
2
2
 
3
+ 0.1.5
4
+ ---
5
+ - RSpec Matchers for testing, thanks to [rspec-rails](https://github.com/rspec/rspec-rails) for their ActionCable `have_broadcasted_to` matcher as reference
6
+ - README update for testing
7
+ - Sometimes use testing mode internally
8
+ - Fixed code smells
9
+
10
+ 0.1.4
11
+ ---
12
+ - Add rudimentary testing method
13
+
14
+ 0.1.3
15
+ ---
16
+ - Allow sending a notification to multiple channels at the same time
17
+
18
+ 0.1.2
19
+ ---
20
+ - Use `TeamsConnector::Configuration#load_from_rails_credentials` to load encrypted channel URLs in your Rails environment
21
+
3
22
  0.1.1
4
23
  ---
5
24
  - Adaptive Card Notification
data/README.md CHANGED
@@ -25,16 +25,18 @@ Or install it yourself as:
25
25
  $ gem install teams_connector
26
26
 
27
27
  ## Usage
28
- After setting up the Incoming Webhook Connector four your Microsoft Teams channel, it is as simple as configuring the channel and creating a new `TeamsConnector::Notification`.
28
+ After setting up the Incoming Webhook Connector for your Microsoft Teams channel, it is as simple as configuring the channel and creating a new `TeamsConnector::Notification`.
29
+
30
+ The `channels` parameter can either be a single channel identifier or an array of multiple channel identifiers, that each will receive the notification.
29
31
 
30
32
  ```ruby
31
- # Configuration
33
+ # TeamsConnector initializer
32
34
  TeamsConnector.configure do |config|
33
35
  config.channel :channel_id, "https://<YOUR COMPLETE WEBHOOK URL GOES HERE>"
34
36
  end
35
37
 
36
38
  # Send a test card to your channel
37
- TeamsConnector::Notification.new(:test_card, :channel_id).deliver_later
39
+ TeamsConnector::Notification.new(template: :test_card, channels: :channel_id).deliver_later
38
40
 
39
41
  # Send a card with a list of facts
40
42
  content = {
@@ -46,9 +48,38 @@ content = {
46
48
  }
47
49
  TeamsConnector::Notification::Message.new(:facts_card, "This is a summary", content).deliver_later
48
50
  ```
49
- This gem provides some basic templates in its default template path. You can also define your own templates in your own path. The default templates will be still available so you can mix and match.
50
51
 
51
- ### Default templates
52
+ ### Secure Channel Configuration
53
+ Since the Incoming Webhook Connector does not allow any authentication at the endpoint it is crucial that you keep your channel urls secret.
54
+ At best nobody finds the url but it can also lead to spam or even faking of critical messages.
55
+
56
+ In Rails provides the credentials functionality for [environmental security](https://edgeguides.rubyonrails.org/security.html#environmental-security). This mechanism can be used by TeamsConnector to load channels from an encrypted file. This also allows easy separation of production and development channel URLs.
57
+ All channels are defined under the top-level entry `teams_connector` and will be identified by their key.
58
+ ```yaml
59
+ # $ bin/rails credentials:edit
60
+ teams_connector:
61
+ default: "<INSERT DEFAULT URL HERE>"
62
+ sales: "<INSERT URL FOR THE :sales CHANNEL HERE>"
63
+ ```
64
+
65
+ After configuration of the credentials you can load the channels in your initializer.
66
+ Since `#load_from_rails_configuration` is a wrapper around `#channel` both methods can be used together.
67
+
68
+ ```ruby
69
+ # TeamsConnector initializer
70
+ TeamsConnector.configure do |config|
71
+ config.load_from_rails_credentials
72
+ # After loading the :default channel is available and can be set as the default
73
+ config.default = :default
74
+ config.channel :another_channel, "<URL>"
75
+ end
76
+ ```
77
+
78
+ ### Templates
79
+ This gem provides some basic templates in its default template path. You can also define your own templates in your own path.
80
+ The default templates will be still available so you can mix and match.
81
+
82
+ #### Default templates
52
83
 
53
84
  Template name | Description
54
85
  -----|-------
@@ -56,13 +87,13 @@ Template name | Description
56
87
  :facts_card | A card with title, subtitle and a list of facts
57
88
  :test_card | A simple text message without any configurable content for testing
58
89
 
59
- ### Custom Templates
90
+ #### Custom Templates
60
91
 
61
92
  Custom templates are stored in the directory specified by the configuration option `template_dir`. As an array of strings, describing the path relative to the project root. When using Rails or Bundler their root is used, otherwise it is the current working directory.
62
93
 
63
94
  Templates are json files with the extension `.json.erb`. The file is parsed and populated by the ruby ERB module.
64
95
 
65
- ### Builder
96
+ #### Builder
66
97
 
67
98
  You can use TeamsConnector::Builder to create Adaptive Cards directly in ruby. YOu can output the result of the builder as JSON for future use with `TeamsController::Notification::AdaptiveCard#pretty_print`.
68
99
 
@@ -78,6 +109,66 @@ end
78
109
  TeamsConnector::Notification::AdaptiveCard.new(content: builder).deliver_later
79
110
  ```
80
111
 
112
+ ## Testing
113
+
114
+ To test TeamsConnector integration in your application you can use the `:testing` method.
115
+ Instead of performing real HTTP requests, an array in `TeamsConnector.testing.requests` is filled with your notifications in chronological order.
116
+
117
+ The request elements have the following structure:
118
+ ```ruby
119
+ {
120
+ channel: :default,
121
+ template: :facts_card,
122
+ content: '{"rendered content": "in JSON format"}',
123
+ time: Time.now
124
+ }
125
+ ```
126
+
127
+ ### RSpec Matcher
128
+ TeamsConnector provides the `have_sent_notification_to(channel = nil, template = nil)` matcher for RSpec.
129
+ It is available by adding `require "teams_connector/rspec"` to your `spec_helper.rb`.
130
+ The matcher supports filtering notifications by channel and template. If one is not given, it does not filter the notifications by it.
131
+ There exists the alias `send_notification_to` for `have_sent_notification_to`.
132
+
133
+ ```ruby
134
+ it "has sent exactly one notification to the channel" do
135
+ expect { notification.deliver_later }.to have_sent_notification_to(:channel)
136
+ end
137
+ ```
138
+
139
+ #### Expecting number of notifications
140
+ By default `have_sent_notification_to` expects exactly one matching notification.
141
+ You can change the expected amount by chaining `exactly`, `at_least` or `at_most`.
142
+
143
+ Example:
144
+ ```ruby
145
+ it "has sent less than 10 notifications to the channel" do
146
+ expect { notification.deliver_later }.to have_sent_notification_to(:channel).at_most(10)
147
+ end
148
+ ```
149
+
150
+ You can also use `once`, `twice` and `thrice` as an alias for `exactly(1..3)`.
151
+ For more readable expectations `times` can be chained.
152
+
153
+ #### Expecting templates
154
+ The template argument in the matcher does filter the notifications.
155
+ If you expect a template instead, you can chain with `with_template(:template)`.
156
+
157
+ #### Expecting content
158
+ To expect specific content, you can chain with `with(data = nil, &block)`.
159
+ Data supports other RSpec matchers like `hash_including`.
160
+ The block is called for every notification with the notification content hash and the raw notification itself.
161
+
162
+ Example:
163
+ ```ruby
164
+ expect {
165
+ notification(:default, :test_card).deliver_later
166
+ }.to have_sent_notification_to(:default).with { |content, notification|
167
+ expect(notification[:channel]).to eq :default
168
+ expect(notification[:template]).to eq :test_card
169
+ expect(content["sections"]).to include(hash_including("activityTitle", "activitySubtitle", "facts", "markdown" => true))
170
+ }
171
+ ```
81
172
  ## Development
82
173
 
83
174
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -19,7 +19,7 @@ module TeamsConnector
19
19
  end
20
20
 
21
21
  def method=(method)
22
- raise ArgumentError, "Method '#{method.to_s}' is not supported" unless [:direct, :sidekiq].include? method
22
+ raise ArgumentError, "Method '#{method.to_s}' is not supported" unless [:direct, :sidekiq, :testing].include? method
23
23
  raise ArgumentError, "Sidekiq is not available" if method == :sidekiq && !defined? Sidekiq
24
24
  @method = method
25
25
  end
@@ -27,5 +27,16 @@ module TeamsConnector
27
27
  def channel(name, url)
28
28
  @channels[name] = url;
29
29
  end
30
+
31
+ def load_from_rails_credentials
32
+ unless defined? Rails
33
+ raise RuntimeError, "This method is only available in Ruby on Rails."
34
+ end
35
+
36
+ webhook_urls = Rails.application.credentials.teams_connector!
37
+ webhook_urls.each do |entry|
38
+ channel(entry[0], entry[1])
39
+ end
40
+ end
30
41
  end
31
42
  end
@@ -0,0 +1,163 @@
1
+ module TeamsConnector
2
+ module Matchers
3
+ class HaveSentNotificationTo
4
+ include RSpec::Matchers::Composable
5
+
6
+ def initialize(channel, template)
7
+ @filter = {
8
+ channel: channel,
9
+ template: template
10
+ }
11
+ @block = proc {}
12
+ @data = nil
13
+ @template_data = nil
14
+ set_expected_number(:exactly, 1)
15
+ end
16
+
17
+ def with(data = nil, &block)
18
+ @data = data
19
+ @block = block if block
20
+ self
21
+ end
22
+
23
+ def with_template(template = nil)
24
+ @template_data = template
25
+ self
26
+ end
27
+
28
+ def exactly(count)
29
+ set_expected_number(:exactly, count)
30
+ self
31
+ end
32
+
33
+ def at_least(count)
34
+ set_expected_number(:at_least, count)
35
+ self
36
+ end
37
+
38
+ def at_most(count)
39
+ set_expected_number(:at_most, count)
40
+ self
41
+ end
42
+
43
+ def times
44
+ self
45
+ end
46
+
47
+ def once
48
+ exactly(:once)
49
+ end
50
+
51
+ def twice
52
+ exactly(:twice)
53
+ end
54
+
55
+ def thrice
56
+ exactly(:thrice)
57
+ end
58
+
59
+ def failure_message
60
+ "expected to send #{base_message}".tap do |msg|
61
+ if @unmatching_ntfcts.any?
62
+ msg << "\nSent notifications"
63
+ msg << " to #{@filter[:channel]}" if @filter[:channel]
64
+ msg << " of #{@filter[:template]}" if @filter[:template]
65
+ msg << ":"
66
+ @unmatching_ntfcts.each do |data|
67
+ msg << "\n #{data}"
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ def failure_message_when_negated
74
+ "expected not to send #{base_message}"
75
+ end
76
+
77
+ def matches?(expectation)
78
+ if Proc === expectation
79
+ original_count = TeamsConnector.testing.requests.size
80
+ expectation.call
81
+ in_block_notifications = TeamsConnector.testing.requests.drop(original_count)
82
+ else
83
+ in_block_notifications = expectation
84
+ end
85
+
86
+ in_block_notifications = in_block_notifications.select { |msg|
87
+ @filter.map { |k, v| msg[k] === v unless v.nil? }.compact.all?
88
+ }
89
+
90
+ check(in_block_notifications)
91
+ end
92
+
93
+ def supports_block_expectations?
94
+ true
95
+ end
96
+
97
+ private
98
+
99
+ def check(notifications)
100
+ @matching_ntfcts, @unmatching_ntfcts = notifications.partition do |ntfct|
101
+ result = true
102
+
103
+ result &= ntfct[:template] == @template_data unless @template_data.nil?
104
+
105
+ decoded = JSON.parse(ntfct[:content])
106
+ if @data.nil? || @data === decoded
107
+ @block.call(decoded, ntfct)
108
+ result &= true
109
+ else
110
+ result = false
111
+ end
112
+
113
+ result
114
+ end
115
+
116
+ @matching_count = @matching_ntfcts.size
117
+
118
+ case @expectation_type
119
+ when :exactly then @expected_number == @matching_count
120
+ when :at_most then @expected_number >= @matching_count
121
+ when :at_least then @expected_number <= @matching_count
122
+ end
123
+ end
124
+
125
+ def set_expected_number(relativity, count)
126
+ @expectation_type = relativity
127
+ @expected_number =
128
+ case count
129
+ when :once then 1
130
+ when :twice then 2
131
+ when :thrice then 3
132
+ else Integer(count)
133
+ end
134
+ end
135
+
136
+ def base_message
137
+ "#{message_expectation_modifier} #{@expected_number} notifications".tap do |msg|
138
+ msg << " to #{@filter[:channel]}" if @filter[:channel]
139
+ msg << " of #{@filter[:template]}" if @filter[:template]
140
+ msg << " with template #{@template_data}" if @template_data
141
+ msg << " with content #{data_description(@data)}" if @data
142
+ msg << ", but sent #{@matching_count}"
143
+ end
144
+ end
145
+
146
+ def message_expectation_modifier
147
+ case @expectation_type
148
+ when :exactly then "exactly"
149
+ when :at_most then "at most"
150
+ when :at_least then "at least"
151
+ end
152
+ end
153
+
154
+ def data_description(data)
155
+ if RSpec::Support.is_a_matcher?(data) && data.respond_to?(:description)
156
+ data.description
157
+ else
158
+ data
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,11 @@
1
+ require 'teams_connector/matchers/have_sent_notification_to'
2
+
3
+ module TeamsConnector
4
+ module Matchers
5
+ def have_sent_notification_to(channel = nil, template = nil)
6
+ HaveSentNotificationTo.new(channel, template)
7
+ end
8
+
9
+ alias_method :send_notification_to, :have_sent_notification_to
10
+ end
11
+ end
@@ -3,7 +3,7 @@ module TeamsConnector
3
3
  attr_accessor :content
4
4
 
5
5
  def initialize(template: :adaptive_card, content: {}, channel: TeamsConnector.configuration.default)
6
- super(template: template, channel: channel)
6
+ super(template: template, channels: channel)
7
7
  if content.instance_of? TeamsConnector::Builder
8
8
  @content = {
9
9
  card: [content.result]
@@ -3,7 +3,7 @@ module TeamsConnector
3
3
  attr_accessor :summary, :content
4
4
 
5
5
  def initialize(template, summary, content = {}, channel = TeamsConnector.configuration.default)
6
- super(template: template, channel: channel)
6
+ super(template: template, channels: channel)
7
7
  @summary = summary
8
8
  @content = content
9
9
  end
@@ -5,11 +5,11 @@ require 'teams_connector/post_worker' if defined? Sidekiq
5
5
 
6
6
  module TeamsConnector
7
7
  class Notification
8
- attr_accessor :template, :channel
8
+ attr_accessor :template, :channels
9
9
 
10
- def initialize(template: nil, channel: TeamsConnector.configuration.default)
10
+ def initialize(template: nil, channels: TeamsConnector.configuration.default)
11
11
  @template = template
12
- @channel = channel
12
+ @channels = channels.instance_of?(Array) ? channels : [channels]
13
13
  end
14
14
 
15
15
  def deliver_later
@@ -18,17 +18,21 @@ module TeamsConnector
18
18
  renderer = ERB.new(File.read(template_path))
19
19
  renderer.location = [template_path.to_s, 0]
20
20
 
21
- url = TeamsConnector.configuration.channels[@channel]
22
- url = TeamsConnector.configuration.channels[TeamsConnector.configuration.default] if TeamsConnector.configuration.always_use_default
23
- raise ArgumentError, "The Teams channel '#{@channel}' is not available in the configuration." if url.nil?
24
-
25
21
  content = renderer.result(binding)
26
22
 
27
- if TeamsConnector.configuration.method == :sidekiq
28
- TeamsConnector::PostWorker.perform_async(url, content)
29
- else
30
- response = Net::HTTP.post(URI(url), content, { "Content-Type": "application/json" })
31
- response.value
23
+ channels = TeamsConnector.configuration.always_use_default ? [TeamsConnector.configuration.default] : @channels
24
+ channels.each do |channel|
25
+ url = TeamsConnector.configuration.channels[channel]
26
+ raise ArgumentError, "The Teams channel '#{channel}' is not available in the configuration." if url.nil?
27
+
28
+ if TeamsConnector.configuration.method == :sidekiq
29
+ TeamsConnector::PostWorker.perform_async(url, content)
30
+ elsif TeamsConnector.configuration.method == :testing
31
+ TeamsConnector.testing.perform_request channel, @template, content
32
+ else
33
+ response = Net::HTTP.post(URI(url), content, { "Content-Type" => "application/json" })
34
+ response.value
35
+ end
32
36
  end
33
37
  end
34
38
 
@@ -0,0 +1,5 @@
1
+ require "teams_connector/matchers"
2
+
3
+ RSpec.configure do |config|
4
+ config.include TeamsConnector::Matchers
5
+ end
@@ -0,0 +1,13 @@
1
+ module TeamsConnector
2
+ class Testing
3
+ attr_reader :requests
4
+
5
+ def initialize
6
+ @requests = []
7
+ end
8
+
9
+ def perform_request(channel, template, content)
10
+ @requests.push({channel: channel, content: content, template: template, time: Time.now})
11
+ end
12
+ end
13
+ end
@@ -1,3 +1,3 @@
1
1
  module TeamsConnector
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.5"
3
3
  end
@@ -7,7 +7,7 @@ require 'teams_connector/builder'
7
7
 
8
8
  module TeamsConnector
9
9
  class << self
10
- attr_accessor :configuration
10
+ attr_accessor :configuration, :testing
11
11
  end
12
12
 
13
13
  def self.configuration
@@ -22,6 +22,16 @@ module TeamsConnector
22
22
  yield configuration
23
23
  end
24
24
 
25
+ def self.testing
26
+ require 'teams_connector/testing'
27
+ @testing ||= Testing.new
28
+ end
29
+
30
+ def self.reset_testing
31
+ require 'teams_connector/testing'
32
+ @testing = Testing.new
33
+ end
34
+
25
35
  def self.project_root
26
36
  if defined?(Rails)
27
37
  return Rails.root
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: teams_connector
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lucas Keune
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-11-15 00:00:00.000000000 Z
11
+ date: 2021-12-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -117,10 +117,14 @@ files:
117
117
  - lib/teams_connector.rb
118
118
  - lib/teams_connector/builder.rb
119
119
  - lib/teams_connector/configuration.rb
120
+ - lib/teams_connector/matchers.rb
121
+ - lib/teams_connector/matchers/have_sent_notification_to.rb
120
122
  - lib/teams_connector/notification.rb
121
123
  - lib/teams_connector/notification/adaptive_card.rb
122
124
  - lib/teams_connector/notification/message.rb
123
125
  - lib/teams_connector/post_worker.rb
126
+ - lib/teams_connector/rspec.rb
127
+ - lib/teams_connector/testing.rb
124
128
  - lib/teams_connector/version.rb
125
129
  - teams_connector.gemspec
126
130
  - templates/teams_connector/adaptive_card.json.erb