teams_connector 0.1.5 → 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +10 -13
- data/.rubocop.yml +19 -0
- data/CHANGES.md +13 -12
- data/Gemfile +10 -2
- data/README.md +11 -11
- 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 +60 -94
- data/lib/teams_connector/matchers/with.rb +18 -0
- data/lib/teams_connector/matchers.rb +5 -2
- 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 +3 -1
- 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 +16 -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,26 +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
|
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
|
6
12
|
- README update for testing
|
7
13
|
- Sometimes use testing mode internally
|
8
14
|
- Fixed code smells
|
9
15
|
|
10
|
-
0.1.4
|
11
|
-
---
|
16
|
+
## 0.1.4
|
12
17
|
- Add rudimentary testing method
|
13
18
|
|
14
|
-
0.1.3
|
15
|
-
---
|
19
|
+
## 0.1.3
|
16
20
|
- Allow sending a notification to multiple channels at the same time
|
17
21
|
|
18
|
-
0.1.2
|
19
|
-
---
|
22
|
+
## 0.1.2
|
20
23
|
- Use `TeamsConnector::Configuration#load_from_rails_credentials` to load encrypted channel URLs in your Rails environment
|
21
24
|
|
22
|
-
0.1.1
|
23
|
-
---
|
25
|
+
## 0.1.1
|
24
26
|
- Adaptive Card Notification
|
25
27
|
- Send a more complex Adaptive Card instead of the basic Message Card
|
26
28
|
- Builder
|
@@ -35,7 +37,6 @@
|
|
35
37
|
- Change constructor from numbered to named parameters
|
36
38
|
- Pretty print JSON to STDOUT with #pretty_print
|
37
39
|
|
38
|
-
0.1.0
|
39
|
-
---
|
40
|
+
## 0.1.0
|
40
41
|
- Initial commit of the Teams Connector gem
|
41
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
|
|
@@ -125,25 +125,25 @@ The request elements have the following structure:
|
|
125
125
|
```
|
126
126
|
|
127
127
|
### RSpec Matcher
|
128
|
-
TeamsConnector provides the `
|
128
|
+
TeamsConnector provides the `sent_notification_to?(channel = nil, template = nil)` matcher for RSpec.
|
129
129
|
It is available by adding `require "teams_connector/rspec"` to your `spec_helper.rb`.
|
130
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 `
|
131
|
+
There exists the alias `send_notification_to` and `have_sent_notification_to` for `sent_notification_to?`.
|
132
132
|
|
133
133
|
```ruby
|
134
134
|
it "has sent exactly one notification to the channel" do
|
135
|
-
expect { notification.deliver_later }.to
|
135
|
+
expect { notification.deliver_later }.to sent_notification_to?(:channel)
|
136
136
|
end
|
137
137
|
```
|
138
138
|
|
139
139
|
#### Expecting number of notifications
|
140
|
-
By default `
|
140
|
+
By default `sent_notification_to?` expects exactly one matching notification.
|
141
141
|
You can change the expected amount by chaining `exactly`, `at_least` or `at_most`.
|
142
142
|
|
143
143
|
Example:
|
144
144
|
```ruby
|
145
145
|
it "has sent less than 10 notifications to the channel" do
|
146
|
-
expect { notification.deliver_later }.to
|
146
|
+
expect { notification.deliver_later }.to sent_notification_to?(:channel).at_most(10)
|
147
147
|
end
|
148
148
|
```
|
149
149
|
|
@@ -163,7 +163,7 @@ Example:
|
|
163
163
|
```ruby
|
164
164
|
expect {
|
165
165
|
notification(:default, :test_card).deliver_later
|
166
|
-
}.to
|
166
|
+
}.to sent_notification_to?(:default).with { |content, notification|
|
167
167
|
expect(notification[:channel]).to eq :default
|
168
168
|
expect(notification[:template]).to eq :test_card
|
169
169
|
expect(content["sections"]).to include(hash_including("activityTitle", "activitySubtitle", "facts", "markdown" => true))
|
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
|
@@ -1,7 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'expected_number'
|
4
|
+
require_relative 'with'
|
5
|
+
|
1
6
|
module TeamsConnector
|
2
7
|
module Matchers
|
3
8
|
class HaveSentNotificationTo
|
4
9
|
include RSpec::Matchers::Composable
|
10
|
+
include TeamsConnector::Matchers::ExpectedNumber
|
11
|
+
include TeamsConnector::Matchers::With
|
12
|
+
|
13
|
+
class RelativityNotSupported < StandardError; end
|
5
14
|
|
6
15
|
def initialize(channel, template)
|
7
16
|
@filter = {
|
@@ -14,60 +23,17 @@ module TeamsConnector
|
|
14
23
|
set_expected_number(:exactly, 1)
|
15
24
|
end
|
16
25
|
|
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
26
|
def failure_message
|
60
|
-
"expected to send #{base_message}"
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
msg << "\n #{data}"
|
68
|
-
end
|
69
|
-
end
|
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}" }
|
70
34
|
end
|
35
|
+
|
36
|
+
msg
|
71
37
|
end
|
72
38
|
|
73
39
|
def failure_message_when_negated
|
@@ -75,19 +41,11 @@ module TeamsConnector
|
|
75
41
|
end
|
76
42
|
|
77
43
|
def matches?(expectation)
|
78
|
-
|
79
|
-
|
80
|
-
expectation.call
|
81
|
-
in_block_notifications = TeamsConnector.testing.requests.drop(original_count)
|
82
|
-
else
|
83
|
-
in_block_notifications = expectation
|
44
|
+
matching_notifications = in_block_notifications(expectation).select do |msg|
|
45
|
+
@filter.map { |k, v| msg[k] == v unless v.nil? }.compact.all?
|
84
46
|
end
|
85
47
|
|
86
|
-
|
87
|
-
@filter.map { |k, v| msg[k] === v unless v.nil? }.compact.all?
|
88
|
-
}
|
89
|
-
|
90
|
-
check(in_block_notifications)
|
48
|
+
check(matching_notifications)
|
91
49
|
end
|
92
50
|
|
93
51
|
def supports_block_expectations?
|
@@ -98,56 +56,64 @@ module TeamsConnector
|
|
98
56
|
|
99
57
|
def check(notifications)
|
100
58
|
@matching_ntfcts, @unmatching_ntfcts = notifications.partition do |ntfct|
|
101
|
-
|
59
|
+
check_partition(ntfct)
|
60
|
+
end
|
102
61
|
|
103
|
-
|
62
|
+
@matching_count = @matching_ntfcts.size
|
104
63
|
|
105
|
-
|
106
|
-
|
107
|
-
@block.call(decoded, ntfct)
|
108
|
-
result &= true
|
109
|
-
else
|
110
|
-
result = false
|
111
|
-
end
|
64
|
+
check_result
|
65
|
+
end
|
112
66
|
|
113
|
-
|
114
|
-
|
67
|
+
def check_partition(ntfct)
|
68
|
+
result = true
|
115
69
|
|
116
|
-
@
|
70
|
+
result &= ntfct[:template] == @template_data unless @template_data.nil?
|
117
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
|
118
82
|
case @expectation_type
|
119
83
|
when :exactly then @expected_number == @matching_count
|
120
84
|
when :at_most then @expected_number >= @matching_count
|
121
85
|
when :at_least then @expected_number <= @matching_count
|
86
|
+
else
|
87
|
+
raise RelativityNotSupported
|
122
88
|
end
|
123
89
|
end
|
124
90
|
|
125
|
-
def
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
end
|
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
|
134
99
|
end
|
135
100
|
|
136
101
|
def base_message
|
137
|
-
"#{message_expectation_modifier} #{@expected_number} notifications"
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
end
|
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}"
|
144
108
|
end
|
145
109
|
|
146
110
|
def message_expectation_modifier
|
147
111
|
case @expectation_type
|
148
|
-
when :exactly then
|
149
|
-
when :at_most then
|
150
|
-
when :at_least then
|
112
|
+
when :exactly then 'exactly'
|
113
|
+
when :at_most then 'at most'
|
114
|
+
when :at_least then 'at least'
|
115
|
+
else
|
116
|
+
raise RelativityNotSupported
|
151
117
|
end
|
152
118
|
end
|
153
119
|
|
@@ -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
|
@@ -1,11 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'teams_connector/matchers/have_sent_notification_to'
|
2
4
|
|
3
5
|
module TeamsConnector
|
4
6
|
module Matchers
|
5
|
-
def
|
7
|
+
def sent_notification_to?(channel = nil, template = nil)
|
6
8
|
HaveSentNotificationTo.new(channel, template)
|
7
9
|
end
|
8
10
|
|
9
|
-
|
11
|
+
alias have_sent_notification_to sent_notification_to?
|
12
|
+
alias send_notification_to have_sent_notification_to
|
10
13
|
end
|
11
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,15 +34,18 @@ 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
|
120
41
|
- lib/teams_connector/matchers.rb
|
42
|
+
- lib/teams_connector/matchers/expected_number.rb
|
121
43
|
- lib/teams_connector/matchers/have_sent_notification_to.rb
|
44
|
+
- lib/teams_connector/matchers/with.rb
|
122
45
|
- lib/teams_connector/notification.rb
|
123
46
|
- lib/teams_connector/notification/adaptive_card.rb
|
124
47
|
- lib/teams_connector/notification/message.rb
|
125
|
-
- lib/teams_connector/
|
48
|
+
- lib/teams_connector/post_job.rb
|
126
49
|
- lib/teams_connector/rspec.rb
|
127
50
|
- lib/teams_connector/testing.rb
|
128
51
|
- lib/teams_connector/version.rb
|
@@ -137,6 +60,7 @@ metadata:
|
|
137
60
|
homepage_uri: https://github.com/Qurasoft/teams_connector
|
138
61
|
source_code_uri: https://github.com/Qurasoft/teams_connector
|
139
62
|
changelog_uri: https://github.com/Qurasoft/teams_connector/blob/main/CHANGES.md
|
63
|
+
rubygems_mfa_required: 'true'
|
140
64
|
post_install_message:
|
141
65
|
rdoc_options: []
|
142
66
|
require_paths:
|
@@ -145,14 +69,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
145
69
|
requirements:
|
146
70
|
- - ">="
|
147
71
|
- !ruby/object:Gem::Version
|
148
|
-
version: '
|
72
|
+
version: '2.6'
|
149
73
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
150
74
|
requirements:
|
151
75
|
- - ">="
|
152
76
|
- !ruby/object:Gem::Version
|
153
77
|
version: '0'
|
154
78
|
requirements: []
|
155
|
-
rubygems_version: 3.
|
79
|
+
rubygems_version: 3.4.21
|
156
80
|
signing_key:
|
157
81
|
specification_version: 4
|
158
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
|