teams_connector 0.1.4 → 0.1.6
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/.github/workflows/ruby.yml +10 -13
- data/.rubocop.yml +19 -0
- data/CHANGES.md +18 -10
- data/Gemfile +10 -2
- data/README.md +52 -7
- data/Rakefile +9 -3
- data/bin/console +9 -8
- data/lib/core_ext/object.rb +28 -0
- data/lib/teams_connector/builder.rb +32 -16
- data/lib/teams_connector/configuration.rb +12 -9
- data/lib/teams_connector/matchers/expected_number.rb +51 -0
- data/lib/teams_connector/matchers/have_sent_notification_to.rb +129 -0
- data/lib/teams_connector/matchers/with.rb +18 -0
- data/lib/teams_connector/matchers.rb +14 -0
- data/lib/teams_connector/notification/adaptive_card.rb +15 -10
- data/lib/teams_connector/notification/message.rb +10 -6
- data/lib/teams_connector/notification.rb +23 -16
- data/lib/teams_connector/post_job.rb +16 -0
- data/lib/teams_connector/rspec.rb +7 -0
- data/lib/teams_connector/testing.rb +3 -1
- data/lib/teams_connector/version.rb +3 -1
- data/lib/teams_connector.rb +12 -10
- data/teams_connector.gemspec +18 -30
- metadata +19 -92
- data/lib/teams_connector/post_worker.rb +0 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ec351030eb8a9eb02df72d9807fec0ebb3040bb08fa2046182de96ccc01f0523
|
4
|
+
data.tar.gz: 54c5be1eeccab5fc6f0b34f0a226c896d1f96c9902d662c1c99cf88844216f4a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b2e9831c57a4961021faae608a4e2ee0b1752ad4b2ca05adc974a8ac44421ae30175217bfc3705b0810918aef694b4ac1e1323a0ad7945b79bdb313c061b77c3
|
7
|
+
data.tar.gz: c6935973c88bbdf258dbbcda1f5341209eca6ce53738579f95d3da64cbe8cbc28e7c4a499f7e21f14109a6c53d3eadc8c44f4ce5ef9359809f92c0aa6d1d5e0b
|
data/.github/workflows/ruby.yml
CHANGED
@@ -15,21 +15,18 @@ on:
|
|
15
15
|
|
16
16
|
jobs:
|
17
17
|
test:
|
18
|
-
|
19
18
|
runs-on: ubuntu-latest
|
19
|
+
name: Ruby ${{ matrix.ruby-version }}
|
20
20
|
strategy:
|
21
21
|
matrix:
|
22
|
-
ruby-version: ['2.6', '2.7', '3.0']
|
22
|
+
ruby-version: ['2.6', '2.7', '3.0', '3.2']
|
23
23
|
|
24
24
|
steps:
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
34
|
-
- name: Run tests
|
35
|
-
run: bundle exec rake
|
25
|
+
- uses: actions/checkout@v3
|
26
|
+
- name: Set up Ruby
|
27
|
+
uses: ruby/setup-ruby@v1
|
28
|
+
with:
|
29
|
+
ruby-version: ${{ matrix.ruby-version }}
|
30
|
+
bundler-cache: true
|
31
|
+
- name: Run the default task
|
32
|
+
run: bundle exec rake
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
AllCops:
|
2
|
+
NewCops: enable
|
3
|
+
TargetRubyVersion: 2.6
|
4
|
+
|
5
|
+
Layout/LineLength:
|
6
|
+
Exclude:
|
7
|
+
- spec/**/*
|
8
|
+
|
9
|
+
Metrics/BlockLength:
|
10
|
+
Exclude:
|
11
|
+
- spec/**/*
|
12
|
+
- '*.gemspec'
|
13
|
+
|
14
|
+
Style/CaseEquality:
|
15
|
+
Exclude:
|
16
|
+
- lib/teams_connector/matchers/have_sent_notification_to.rb
|
17
|
+
|
18
|
+
Style/Documentation:
|
19
|
+
Enabled: false
|
data/CHANGES.md
CHANGED
@@ -1,19 +1,28 @@
|
|
1
1
|
# Teams Connector Changelog
|
2
2
|
|
3
|
-
0.1.
|
4
|
-
|
3
|
+
## 0.1.6
|
4
|
+
- Enable Github actions for ruby 3
|
5
|
+
- Enable Rubocop linting
|
6
|
+
- Bump compatibility to sidekiq 7
|
7
|
+
- More stringent RuboCop tests
|
8
|
+
- Enable RubyGems MFA required
|
9
|
+
|
10
|
+
## 0.1.5
|
11
|
+
- RSpec Matchers for testing, thanks to [rspec-rails](https://github.com/rspec/rspec-rails) for their ActionCable `have_broadcasted_to` matcher as reference
|
12
|
+
- README update for testing
|
13
|
+
- Sometimes use testing mode internally
|
14
|
+
- Fixed code smells
|
15
|
+
|
16
|
+
## 0.1.4
|
5
17
|
- Add rudimentary testing method
|
6
18
|
|
7
|
-
0.1.3
|
8
|
-
---
|
19
|
+
## 0.1.3
|
9
20
|
- Allow sending a notification to multiple channels at the same time
|
10
21
|
|
11
|
-
0.1.2
|
12
|
-
---
|
22
|
+
## 0.1.2
|
13
23
|
- Use `TeamsConnector::Configuration#load_from_rails_credentials` to load encrypted channel URLs in your Rails environment
|
14
24
|
|
15
|
-
0.1.1
|
16
|
-
---
|
25
|
+
## 0.1.1
|
17
26
|
- Adaptive Card Notification
|
18
27
|
- Send a more complex Adaptive Card instead of the basic Message Card
|
19
28
|
- Builder
|
@@ -28,7 +37,6 @@
|
|
28
37
|
- Change constructor from numbered to named parameters
|
29
38
|
- Pretty print JSON to STDOUT with #pretty_print
|
30
39
|
|
31
|
-
0.1.0
|
32
|
-
---
|
40
|
+
## 0.1.0
|
33
41
|
- Initial commit of the Teams Connector gem
|
34
42
|
- Send messages as cards defined by JSON templates to configured Microsoft Teams channels
|
data/Gemfile
CHANGED
@@ -1,6 +1,14 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
source 'https://rubygems.org'
|
4
4
|
|
5
5
|
# Specify your gem's dependencies in teams_connector.gemspec
|
6
6
|
gemspec
|
7
|
+
|
8
|
+
gem 'bundler', '>= 1.17'
|
9
|
+
gem 'rake', '>= 10.0'
|
10
|
+
gem 'rspec', '>= 3.0'
|
11
|
+
gem 'rubocop'
|
12
|
+
gem 'sidekiq', '< 8'
|
13
|
+
gem 'simplecov'
|
14
|
+
gem 'webmock'
|
data/README.md
CHANGED
@@ -81,11 +81,11 @@ The default templates will be still available so you can mix and match.
|
|
81
81
|
|
82
82
|
#### Default templates
|
83
83
|
|
84
|
-
Template name
|
85
|
-
|
86
|
-
:adaptive_card | A card with the body of an adaptive card for more complex scenarios
|
87
|
-
:facts_card
|
88
|
-
:test_card
|
84
|
+
| Template name | Description |
|
85
|
+
|----------------|---------------------------------------------------------------------|
|
86
|
+
| :adaptive_card | A card with the body of an adaptive card for more complex scenarios |
|
87
|
+
| :facts_card | A card with title, subtitle and a list of facts |
|
88
|
+
| :test_card | A simple text message without any configurable content for testing |
|
89
89
|
|
90
90
|
#### Custom Templates
|
91
91
|
|
@@ -112,18 +112,63 @@ TeamsConnector::Notification::AdaptiveCard.new(content: builder).deliver_later
|
|
112
112
|
## Testing
|
113
113
|
|
114
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 in chronological order.
|
115
|
+
Instead of performing real HTTP requests, an array in `TeamsConnector.testing.requests` is filled with your notifications in chronological order.
|
116
116
|
|
117
117
|
The request elements have the following structure:
|
118
118
|
```ruby
|
119
119
|
{
|
120
120
|
channel: :default,
|
121
121
|
template: :facts_card,
|
122
|
-
content: "rendered content",
|
122
|
+
content: '{"rendered content": "in JSON format"}',
|
123
123
|
time: Time.now
|
124
124
|
}
|
125
125
|
```
|
126
126
|
|
127
|
+
### RSpec Matcher
|
128
|
+
TeamsConnector provides the `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` and `have_sent_notification_to` for `sent_notification_to?`.
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
it "has sent exactly one notification to the channel" do
|
135
|
+
expect { notification.deliver_later }.to sent_notification_to?(:channel)
|
136
|
+
end
|
137
|
+
```
|
138
|
+
|
139
|
+
#### Expecting number of notifications
|
140
|
+
By default `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 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 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
|
+
```
|
127
172
|
## Development
|
128
173
|
|
129
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.
|
data/Rakefile
CHANGED
@@ -1,6 +1,12 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rspec/core/rake_task'
|
3
5
|
|
4
6
|
RSpec::Core::RakeTask.new(:spec)
|
5
7
|
|
6
|
-
|
8
|
+
require 'rubocop/rake_task'
|
9
|
+
|
10
|
+
RuboCop::RakeTask.new
|
11
|
+
|
12
|
+
task default: %i[spec rubocop]
|
data/bin/console
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
|
-
require
|
4
|
-
require
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'teams_connector'
|
5
6
|
|
6
7
|
# You can add fixtures and/or initialization code here to make experimenting
|
7
8
|
# with your gem easier. You can also use a different console, if you like.
|
@@ -11,26 +12,26 @@ require "teams_connector"
|
|
11
12
|
# Pry.start
|
12
13
|
|
13
14
|
TeamsConnector.configure do |config|
|
14
|
-
if File.exist?(
|
15
|
+
if File.exist?('channels.yml')
|
15
16
|
# NOTE(Keune): The channels available in the console can be set in the file channels.yml. It contains a simple
|
16
17
|
# mapping of channel identifiers to webhook URLs. The channel identifier is loaded as a symbol.
|
17
18
|
#
|
18
19
|
# Example: default: "<INSERT YOUR WEBHOOK URL HERE>"
|
19
|
-
puts
|
20
|
+
puts 'Load channels specified by file'
|
20
21
|
|
21
|
-
require
|
22
|
-
channels = YAML.load_file
|
22
|
+
require 'yaml'
|
23
|
+
channels = YAML.load_file 'channels.yml'
|
23
24
|
channels.each do |k, v|
|
24
25
|
config.channel k.to_sym, v
|
25
26
|
end
|
26
27
|
else
|
27
28
|
# NOTE(Keune): Specify the channels you want to have available in the console
|
28
|
-
config.channel :default,
|
29
|
+
config.channel :default, '<INSERT YOUR WEBHOOK URL HERE>'
|
29
30
|
end
|
30
31
|
config.default = :default
|
31
32
|
config.always_use_default = true
|
32
33
|
config.method = :direct
|
33
34
|
end
|
34
35
|
|
35
|
-
require
|
36
|
+
require 'irb'
|
36
37
|
IRB.start(__FILE__)
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# NOTE(Keune): This core extension is taken directly from rails to support a single usage of #present?
|
4
|
+
# See rails/activesupport/lib/active_support/core_ext/object/blank.rb
|
5
|
+
class Object
|
6
|
+
# An object is blank if it's false, empty, or a whitespace string.
|
7
|
+
# For example, +nil+, '', ' ', [], {}, and +false+ are all blank.
|
8
|
+
#
|
9
|
+
# This simplifies
|
10
|
+
#
|
11
|
+
# !address || address.empty?
|
12
|
+
#
|
13
|
+
# to
|
14
|
+
#
|
15
|
+
# address.blank?
|
16
|
+
#
|
17
|
+
# @return [true, false]
|
18
|
+
def blank?
|
19
|
+
respond_to?(:empty?) ? !!empty? : false
|
20
|
+
end
|
21
|
+
|
22
|
+
# An object is present if it's not blank.
|
23
|
+
#
|
24
|
+
# @return [true, false]
|
25
|
+
def present?
|
26
|
+
!blank?
|
27
|
+
end
|
28
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module TeamsConnector
|
2
4
|
class Builder
|
3
5
|
attr_accessor :type, :content
|
@@ -15,8 +17,8 @@ module TeamsConnector
|
|
15
17
|
@content = text
|
16
18
|
end
|
17
19
|
|
18
|
-
def self.container
|
19
|
-
TeamsConnector::Builder.new { |entry| entry.container
|
20
|
+
def self.container(&block)
|
21
|
+
TeamsConnector::Builder.new { |entry| entry.container(&block) }
|
20
22
|
end
|
21
23
|
|
22
24
|
def container
|
@@ -25,8 +27,8 @@ module TeamsConnector
|
|
25
27
|
yield @content
|
26
28
|
end
|
27
29
|
|
28
|
-
def self.facts
|
29
|
-
TeamsConnector::Builder.new { |entry| entry.facts
|
30
|
+
def self.facts(&block)
|
31
|
+
TeamsConnector::Builder.new { |entry| entry.facts(&block) }
|
30
32
|
end
|
31
33
|
|
32
34
|
def facts
|
@@ -38,23 +40,37 @@ module TeamsConnector
|
|
38
40
|
def result
|
39
41
|
case @type
|
40
42
|
when :container
|
41
|
-
|
42
|
-
type: "Container",
|
43
|
-
items: @content.map { |element| element.result }
|
44
|
-
}
|
43
|
+
result_container
|
45
44
|
when :facts
|
46
|
-
|
47
|
-
type: "FactSet",
|
48
|
-
facts: @content.map { |fact| { title: fact[0], value: fact[1] } }
|
49
|
-
}
|
45
|
+
result_facts
|
50
46
|
when :text
|
51
|
-
|
52
|
-
type: "TextBlock",
|
53
|
-
text: @content
|
54
|
-
}
|
47
|
+
result_text
|
55
48
|
else
|
56
49
|
raise TypeError, "The type #{@type} is not supported by the TeamsConnector::Builder"
|
57
50
|
end
|
58
51
|
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def result_container
|
56
|
+
{
|
57
|
+
type: 'Container',
|
58
|
+
items: @content.map(&:result)
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
def result_facts
|
63
|
+
{
|
64
|
+
type: 'FactSet',
|
65
|
+
facts: @content.map { |fact| { title: fact[0], value: fact[1] } }
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def result_text
|
70
|
+
{
|
71
|
+
type: 'TextBlock',
|
72
|
+
text: @content
|
73
|
+
}
|
74
|
+
end
|
59
75
|
end
|
60
76
|
end
|
@@ -1,8 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module TeamsConnector
|
2
4
|
class Configuration
|
3
|
-
DEFAULT_TEMPLATE_DIR = %w[templates teams_connector]
|
5
|
+
DEFAULT_TEMPLATE_DIR = %w[templates teams_connector].freeze
|
4
6
|
|
5
|
-
|
7
|
+
attr_reader :default, :method
|
8
|
+
attr_accessor :channels, :always_use_default, :template_dir, :color
|
6
9
|
|
7
10
|
def initialize
|
8
11
|
@default = nil
|
@@ -10,28 +13,28 @@ module TeamsConnector
|
|
10
13
|
@always_use_default = false
|
11
14
|
@method = :direct
|
12
15
|
@template_dir = DEFAULT_TEMPLATE_DIR
|
13
|
-
@color =
|
16
|
+
@color = '3f95b5'
|
14
17
|
end
|
15
18
|
|
16
19
|
def default=(channel)
|
17
20
|
raise ArgumentError, "Desired default channel '#{channel}' is not configured" unless @channels.key?(channel)
|
21
|
+
|
18
22
|
@default = channel
|
19
23
|
end
|
20
24
|
|
21
25
|
def method=(method)
|
22
|
-
raise ArgumentError, "Method '#{method
|
23
|
-
raise ArgumentError,
|
26
|
+
raise ArgumentError, "Method '#{method}' is not supported" unless %i[direct sidekiq testing].include? method
|
27
|
+
raise ArgumentError, 'Sidekiq is not available' if method == :sidekiq && !defined? Sidekiq
|
28
|
+
|
24
29
|
@method = method
|
25
30
|
end
|
26
31
|
|
27
32
|
def channel(name, url)
|
28
|
-
@channels[name] = url
|
33
|
+
@channels[name] = url
|
29
34
|
end
|
30
35
|
|
31
36
|
def load_from_rails_credentials
|
32
|
-
unless defined? Rails
|
33
|
-
raise RuntimeError, "This method is only available in Ruby on Rails."
|
34
|
-
end
|
37
|
+
raise 'This method is only available in Ruby on Rails.' unless defined? Rails
|
35
38
|
|
36
39
|
webhook_urls = Rails.application.credentials.teams_connector!
|
37
40
|
webhook_urls.each do |entry|
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TeamsConnector
|
4
|
+
module Matchers
|
5
|
+
module ExpectedNumber
|
6
|
+
def exactly(count)
|
7
|
+
set_expected_number(:exactly, count)
|
8
|
+
self
|
9
|
+
end
|
10
|
+
|
11
|
+
def at_least(count)
|
12
|
+
set_expected_number(:at_least, count)
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def at_most(count)
|
17
|
+
set_expected_number(:at_most, count)
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def times
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def once
|
26
|
+
exactly(:once)
|
27
|
+
end
|
28
|
+
|
29
|
+
def twice
|
30
|
+
exactly(:twice)
|
31
|
+
end
|
32
|
+
|
33
|
+
def thrice
|
34
|
+
exactly(:thrice)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def set_expected_number(relativity, count)
|
40
|
+
@expectation_type = relativity
|
41
|
+
@expected_number =
|
42
|
+
case count
|
43
|
+
when :once then 1
|
44
|
+
when :twice then 2
|
45
|
+
when :thrice then 3
|
46
|
+
else Integer(count)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'expected_number'
|
4
|
+
require_relative 'with'
|
5
|
+
|
6
|
+
module TeamsConnector
|
7
|
+
module Matchers
|
8
|
+
class HaveSentNotificationTo
|
9
|
+
include RSpec::Matchers::Composable
|
10
|
+
include TeamsConnector::Matchers::ExpectedNumber
|
11
|
+
include TeamsConnector::Matchers::With
|
12
|
+
|
13
|
+
class RelativityNotSupported < StandardError; end
|
14
|
+
|
15
|
+
def initialize(channel, template)
|
16
|
+
@filter = {
|
17
|
+
channel: channel,
|
18
|
+
template: template
|
19
|
+
}
|
20
|
+
@block = proc {}
|
21
|
+
@data = nil
|
22
|
+
@template_data = nil
|
23
|
+
set_expected_number(:exactly, 1)
|
24
|
+
end
|
25
|
+
|
26
|
+
def failure_message
|
27
|
+
msg = "expected to send #{base_message}"
|
28
|
+
if @unmatching_ntfcts.any?
|
29
|
+
msg += "\nSent notifications"
|
30
|
+
msg += " to #{@filter[:channel]}" if @filter[:channel]
|
31
|
+
msg += " of #{@filter[:template]}" if @filter[:template]
|
32
|
+
msg += ':'
|
33
|
+
@unmatching_ntfcts.each { |data| msg += "\n #{data}" }
|
34
|
+
end
|
35
|
+
|
36
|
+
msg
|
37
|
+
end
|
38
|
+
|
39
|
+
def failure_message_when_negated
|
40
|
+
"expected not to send #{base_message}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def matches?(expectation)
|
44
|
+
matching_notifications = in_block_notifications(expectation).select do |msg|
|
45
|
+
@filter.map { |k, v| msg[k] == v unless v.nil? }.compact.all?
|
46
|
+
end
|
47
|
+
|
48
|
+
check(matching_notifications)
|
49
|
+
end
|
50
|
+
|
51
|
+
def supports_block_expectations?
|
52
|
+
true
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def check(notifications)
|
58
|
+
@matching_ntfcts, @unmatching_ntfcts = notifications.partition do |ntfct|
|
59
|
+
check_partition(ntfct)
|
60
|
+
end
|
61
|
+
|
62
|
+
@matching_count = @matching_ntfcts.size
|
63
|
+
|
64
|
+
check_result
|
65
|
+
end
|
66
|
+
|
67
|
+
def check_partition(ntfct)
|
68
|
+
result = true
|
69
|
+
|
70
|
+
result &= ntfct[:template] == @template_data unless @template_data.nil?
|
71
|
+
|
72
|
+
decoded = JSON.parse(ntfct[:content])
|
73
|
+
if @data.nil? || @data === decoded
|
74
|
+
@block.call(decoded, ntfct)
|
75
|
+
result & true
|
76
|
+
else
|
77
|
+
false
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def check_result
|
82
|
+
case @expectation_type
|
83
|
+
when :exactly then @expected_number == @matching_count
|
84
|
+
when :at_most then @expected_number >= @matching_count
|
85
|
+
when :at_least then @expected_number <= @matching_count
|
86
|
+
else
|
87
|
+
raise RelativityNotSupported
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def in_block_notifications(expectation)
|
92
|
+
if expectation.is_a? Proc
|
93
|
+
original_count = TeamsConnector.testing.requests.size
|
94
|
+
expectation.call
|
95
|
+
TeamsConnector.testing.requests.drop(original_count)
|
96
|
+
else
|
97
|
+
expectation
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def base_message
|
102
|
+
msg = String("#{message_expectation_modifier} #{@expected_number} notifications")
|
103
|
+
msg += " to #{@filter[:channel]}" if @filter[:channel]
|
104
|
+
msg += " of #{@filter[:template]}" if @filter[:template]
|
105
|
+
msg += " with template #{@template_data}" if @template_data
|
106
|
+
msg += " with content #{data_description(@data)}" if @data
|
107
|
+
msg + ", but sent #{@matching_count}"
|
108
|
+
end
|
109
|
+
|
110
|
+
def message_expectation_modifier
|
111
|
+
case @expectation_type
|
112
|
+
when :exactly then 'exactly'
|
113
|
+
when :at_most then 'at most'
|
114
|
+
when :at_least then 'at least'
|
115
|
+
else
|
116
|
+
raise RelativityNotSupported
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def data_description(data)
|
121
|
+
if RSpec::Support.is_a_matcher?(data) && data.respond_to?(:description)
|
122
|
+
data.description
|
123
|
+
else
|
124
|
+
data
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TeamsConnector
|
4
|
+
module Matchers
|
5
|
+
module With
|
6
|
+
def with(data = nil, &block)
|
7
|
+
@data = data
|
8
|
+
@block = block if block
|
9
|
+
self
|
10
|
+
end
|
11
|
+
|
12
|
+
def with_template(template = nil)
|
13
|
+
@template_data = template
|
14
|
+
self
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'teams_connector/matchers/have_sent_notification_to'
|
4
|
+
|
5
|
+
module TeamsConnector
|
6
|
+
module Matchers
|
7
|
+
def sent_notification_to?(channel = nil, template = nil)
|
8
|
+
HaveSentNotificationTo.new(channel, template)
|
9
|
+
end
|
10
|
+
|
11
|
+
alias have_sent_notification_to sent_notification_to?
|
12
|
+
alias send_notification_to have_sent_notification_to
|
13
|
+
end
|
14
|
+
end
|
@@ -1,15 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module TeamsConnector
|
2
|
-
class Notification
|
3
|
-
|
4
|
+
class Notification
|
5
|
+
class AdaptiveCard < Notification
|
6
|
+
attr_accessor :content
|
4
7
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
8
|
+
def initialize(template: :adaptive_card, content: {}, channel: TeamsConnector.configuration.default)
|
9
|
+
super(template: template, channels: channel)
|
10
|
+
@content =
|
11
|
+
if content.instance_of? TeamsConnector::Builder
|
12
|
+
{
|
13
|
+
card: [content.result]
|
14
|
+
}
|
15
|
+
else
|
16
|
+
content
|
17
|
+
end
|
13
18
|
end
|
14
19
|
end
|
15
20
|
end
|
@@ -1,11 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module TeamsConnector
|
2
|
-
class Notification
|
3
|
-
|
4
|
+
class Notification
|
5
|
+
class Message < Notification
|
6
|
+
attr_accessor :summary, :content
|
4
7
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
8
|
+
def initialize(template, summary, content = {}, channel = TeamsConnector.configuration.default)
|
9
|
+
super(template: template, channels: channel)
|
10
|
+
@summary = summary
|
11
|
+
@content = content
|
12
|
+
end
|
9
13
|
end
|
10
14
|
end
|
11
15
|
end
|
@@ -1,7 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'erb'
|
2
4
|
require 'json'
|
3
5
|
require 'net/http'
|
4
|
-
require 'teams_connector/
|
6
|
+
require 'teams_connector/post_job' if defined? Sidekiq
|
5
7
|
|
6
8
|
module TeamsConnector
|
7
9
|
class Notification
|
@@ -22,17 +24,7 @@ module TeamsConnector
|
|
22
24
|
|
23
25
|
channels = TeamsConnector.configuration.always_use_default ? [TeamsConnector.configuration.default] : @channels
|
24
26
|
channels.each do |channel|
|
25
|
-
|
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
|
27
|
+
deliver_channel(channel, content)
|
36
28
|
end
|
37
29
|
end
|
38
30
|
|
@@ -48,11 +40,26 @@ module TeamsConnector
|
|
48
40
|
|
49
41
|
private
|
50
42
|
|
51
|
-
def
|
52
|
-
|
53
|
-
|
54
|
-
|
43
|
+
def deliver_channel(channel, content)
|
44
|
+
url = TeamsConnector.configuration.channels[channel]
|
45
|
+
raise ArgumentError, "The Teams channel '#{channel}' is not available in the configuration." if url.nil?
|
46
|
+
|
47
|
+
case TeamsConnector.configuration.method
|
48
|
+
when :sidekiq then TeamsConnector::PostJob.perform_async(url, content)
|
49
|
+
when :testing then TeamsConnector.testing.perform_request channel, @template, content
|
50
|
+
else
|
51
|
+
response = Net::HTTP.post(URI(url), content, { 'Content-Type' => 'application/json' })
|
52
|
+
response.value
|
55
53
|
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def find_template
|
57
|
+
path = File.join(TeamsConnector.project_root, *TeamsConnector.configuration.template_dir, "#{@template}.json.erb")
|
58
|
+
return path if File.exist? path
|
59
|
+
|
60
|
+
path = File.join(TeamsConnector.gem_root,
|
61
|
+
*TeamsConnector::Configuration::DEFAULT_TEMPLATE_DIR,
|
62
|
+
"#{@template}.json.erb")
|
56
63
|
raise ArgumentError, "The template '#{@template}' is not available." unless File.exist? path
|
57
64
|
|
58
65
|
path
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'sidekiq/job'
|
5
|
+
|
6
|
+
module TeamsConnector
|
7
|
+
class PostJob
|
8
|
+
# === Includes ===
|
9
|
+
include Sidekiq::Job
|
10
|
+
|
11
|
+
def perform(url, content)
|
12
|
+
response = Net::HTTP.post(URI(url), content, { 'Content-Type' => 'application/json' })
|
13
|
+
response.value
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module TeamsConnector
|
2
4
|
class Testing
|
3
5
|
attr_reader :requests
|
@@ -7,7 +9,7 @@ module TeamsConnector
|
|
7
9
|
end
|
8
10
|
|
9
11
|
def perform_request(channel, template, content)
|
10
|
-
@requests.push({channel: channel, content: content, template: template, time: Time.now})
|
12
|
+
@requests.push({ channel: channel, content: content, template: template, time: Time.now })
|
11
13
|
end
|
12
14
|
end
|
13
15
|
end
|
data/lib/teams_connector.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'core_ext/object'
|
1
4
|
require 'teams_connector/configuration'
|
2
5
|
require 'teams_connector/version'
|
3
6
|
require 'teams_connector/notification'
|
@@ -7,7 +10,7 @@ require 'teams_connector/builder'
|
|
7
10
|
|
8
11
|
module TeamsConnector
|
9
12
|
class << self
|
10
|
-
|
13
|
+
attr_writer :configuration, :testing
|
11
14
|
end
|
12
15
|
|
13
16
|
def self.configuration
|
@@ -33,19 +36,18 @@ module TeamsConnector
|
|
33
36
|
end
|
34
37
|
|
35
38
|
def self.project_root
|
36
|
-
if defined?(Rails)
|
37
|
-
|
38
|
-
end
|
39
|
-
|
40
|
-
if defined?(Bundler)
|
41
|
-
return Bundler.root
|
42
|
-
end
|
39
|
+
return Rails.root if defined?(Rails)
|
40
|
+
return Bundler.root if defined?(Bundler)
|
43
41
|
|
44
42
|
Dir.pwd
|
45
43
|
end
|
46
44
|
|
47
45
|
def self.gem_root
|
48
|
-
spec = Gem::Specification.find_by_name(
|
49
|
-
|
46
|
+
spec = Gem::Specification.find_by_name('teams_connector')
|
47
|
+
begin
|
48
|
+
spec.gem_dir
|
49
|
+
rescue NoMethodError
|
50
|
+
project_root
|
51
|
+
end
|
50
52
|
end
|
51
53
|
end
|
data/teams_connector.gemspec
CHANGED
@@ -1,42 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
|
2
|
-
|
3
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require "teams_connector/version"
|
3
|
+
require_relative 'lib/teams_connector/version'
|
5
4
|
|
6
5
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
6
|
+
spec.name = 'teams_connector'
|
8
7
|
spec.version = TeamsConnector::VERSION
|
9
|
-
spec.
|
10
|
-
spec.
|
8
|
+
spec.required_ruby_version = '>= 2.6'
|
9
|
+
spec.authors = ['Lucas Keune']
|
10
|
+
spec.email = ['lucas.keune@qurasoft.de']
|
11
11
|
|
12
|
-
spec.summary =
|
13
|
-
spec.description =
|
14
|
-
spec.homepage =
|
15
|
-
spec.license =
|
12
|
+
spec.summary = 'Simple connector to send messages and cards to Microsoft Teams channels'
|
13
|
+
spec.description = 'Send templated messages or adaptive cards to Microsoft Teams channels'
|
14
|
+
spec.homepage = 'https://github.com/Qurasoft/teams_connector'
|
15
|
+
spec.license = 'MIT'
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
spec.metadata["source_code_uri"] = "https://github.com/Qurasoft/teams_connector"
|
22
|
-
spec.metadata["changelog_uri"] = "https://github.com/Qurasoft/teams_connector/blob/main/CHANGES.md"
|
23
|
-
else
|
24
|
-
raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
25
|
-
end
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
18
|
+
spec.metadata['source_code_uri'] = 'https://github.com/Qurasoft/teams_connector'
|
19
|
+
spec.metadata['changelog_uri'] = 'https://github.com/Qurasoft/teams_connector/blob/main/CHANGES.md'
|
20
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
26
21
|
|
27
22
|
# Specify which files should be added to the gem when it is released.
|
28
23
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
29
|
-
spec.files
|
24
|
+
spec.files = Dir.chdir(__dir__) do
|
30
25
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
31
26
|
end
|
32
|
-
spec.bindir =
|
33
|
-
spec.executables = spec.files.grep(%r{^
|
34
|
-
spec.require_paths = [
|
35
|
-
|
36
|
-
spec.add_development_dependency "bundler", ">= 1.17"
|
37
|
-
spec.add_development_dependency "rake", ">= 10.0"
|
38
|
-
spec.add_development_dependency "rspec", ">= 3.0"
|
39
|
-
spec.add_development_dependency "webmock"
|
40
|
-
spec.add_development_dependency "sidekiq"
|
41
|
-
spec.add_development_dependency "simplecov"
|
27
|
+
spec.bindir = 'bin'
|
28
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
29
|
+
spec.require_paths = ['lib']
|
42
30
|
end
|
metadata
CHANGED
@@ -1,103 +1,22 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: teams_connector
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lucas Keune
|
8
8
|
autorequire:
|
9
|
-
bindir:
|
9
|
+
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
12
|
-
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: bundler
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '1.17'
|
20
|
-
type: :development
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - ">="
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '1.17'
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: rake
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - ">="
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '10.0'
|
34
|
-
type: :development
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - ">="
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '10.0'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: rspec
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - ">="
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '3.0'
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - ">="
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '3.0'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: webmock
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - ">="
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '0'
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - ">="
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '0'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: sidekiq
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - ">="
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: '0'
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - ">="
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: '0'
|
83
|
-
- !ruby/object:Gem::Dependency
|
84
|
-
name: simplecov
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - ">="
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: '0'
|
90
|
-
type: :development
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - ">="
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: '0'
|
11
|
+
date: 2023-12-01 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
97
13
|
description: Send templated messages or adaptive cards to Microsoft Teams channels
|
98
14
|
email:
|
99
15
|
- lucas.keune@qurasoft.de
|
100
|
-
executables:
|
16
|
+
executables:
|
17
|
+
- ".gitignore"
|
18
|
+
- console
|
19
|
+
- setup
|
101
20
|
extensions: []
|
102
21
|
extra_rdoc_files: []
|
103
22
|
files:
|
@@ -105,6 +24,7 @@ files:
|
|
105
24
|
- ".github/workflows/ruby.yml"
|
106
25
|
- ".gitignore"
|
107
26
|
- ".rspec"
|
27
|
+
- ".rubocop.yml"
|
108
28
|
- ".travis.yml"
|
109
29
|
- CHANGES.md
|
110
30
|
- Gemfile
|
@@ -114,13 +34,19 @@ files:
|
|
114
34
|
- bin/.gitignore
|
115
35
|
- bin/console
|
116
36
|
- bin/setup
|
37
|
+
- lib/core_ext/object.rb
|
117
38
|
- lib/teams_connector.rb
|
118
39
|
- lib/teams_connector/builder.rb
|
119
40
|
- lib/teams_connector/configuration.rb
|
41
|
+
- lib/teams_connector/matchers.rb
|
42
|
+
- lib/teams_connector/matchers/expected_number.rb
|
43
|
+
- lib/teams_connector/matchers/have_sent_notification_to.rb
|
44
|
+
- lib/teams_connector/matchers/with.rb
|
120
45
|
- lib/teams_connector/notification.rb
|
121
46
|
- lib/teams_connector/notification/adaptive_card.rb
|
122
47
|
- lib/teams_connector/notification/message.rb
|
123
|
-
- lib/teams_connector/
|
48
|
+
- lib/teams_connector/post_job.rb
|
49
|
+
- lib/teams_connector/rspec.rb
|
124
50
|
- lib/teams_connector/testing.rb
|
125
51
|
- lib/teams_connector/version.rb
|
126
52
|
- teams_connector.gemspec
|
@@ -134,6 +60,7 @@ metadata:
|
|
134
60
|
homepage_uri: https://github.com/Qurasoft/teams_connector
|
135
61
|
source_code_uri: https://github.com/Qurasoft/teams_connector
|
136
62
|
changelog_uri: https://github.com/Qurasoft/teams_connector/blob/main/CHANGES.md
|
63
|
+
rubygems_mfa_required: 'true'
|
137
64
|
post_install_message:
|
138
65
|
rdoc_options: []
|
139
66
|
require_paths:
|
@@ -142,14 +69,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
142
69
|
requirements:
|
143
70
|
- - ">="
|
144
71
|
- !ruby/object:Gem::Version
|
145
|
-
version: '
|
72
|
+
version: '2.6'
|
146
73
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
147
74
|
requirements:
|
148
75
|
- - ">="
|
149
76
|
- !ruby/object:Gem::Version
|
150
77
|
version: '0'
|
151
78
|
requirements: []
|
152
|
-
rubygems_version: 3.
|
79
|
+
rubygems_version: 3.4.21
|
153
80
|
signing_key:
|
154
81
|
specification_version: 4
|
155
82
|
summary: Simple connector to send messages and cards to Microsoft Teams channels
|
@@ -1,14 +0,0 @@
|
|
1
|
-
require 'net/http'
|
2
|
-
require 'sidekiq/worker'
|
3
|
-
|
4
|
-
module TeamsConnector
|
5
|
-
class PostWorker
|
6
|
-
# === Includes ===
|
7
|
-
include Sidekiq::Worker
|
8
|
-
|
9
|
-
def perform(url, content)
|
10
|
-
response = Net::HTTP.post(URI(url), content, { "Content-Type": "application/json" })
|
11
|
-
response.value
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|