Commit 00e00cac authored by Douwe Maan's avatar Douwe Maan

Merge branch 'microsoft-teams-integration' into 'master'

adds initial microsoft teams integration

See merge request !10412
parents 73cb71e4 1f404065
......@@ -116,6 +116,7 @@ class Project < ActiveRecord::Base
has_one :mock_ci_service, dependent: :destroy
has_one :mock_deployment_service, dependent: :destroy
has_one :mock_monitoring_service, dependent: :destroy
has_one :microsoft_teams_service, dependent: :destroy
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
has_one :forked_from_project, through: :forked_project_link
......
......@@ -2,11 +2,23 @@ require 'slack-notifier'
module ChatMessage
class BaseMessage
attr_reader :markdown
attr_reader :user_name
attr_reader :user_avatar
attr_reader :project_name
attr_reader :project_url
def initialize(params)
raise NotImplementedError
@markdown = params[:markdown] || false
@project_name = params.dig(:project, :path_with_namespace) || params[:project_name]
@project_url = params.dig(:project, :web_url) || params[:project_url]
@user_name = params.dig(:user, :username) || params[:user_name]
@user_avatar = params.dig(:user, :avatar_url) || params[:user_avatar]
end
def pretext
return message if markdown
format(message)
end
......@@ -17,6 +29,10 @@ module ChatMessage
raise NotImplementedError
end
def activity
raise NotImplementedError
end
private
def message
......
module ChatMessage
class IssueMessage < BaseMessage
attr_reader :user_name
attr_reader :title
attr_reader :project_name
attr_reader :project_url
attr_reader :issue_iid
attr_reader :issue_url
attr_reader :action
......@@ -11,9 +8,7 @@ module ChatMessage
attr_reader :description
def initialize(params)
@user_name = params[:user][:username]
@project_name = params[:project_name]
@project_url = params[:project_url]
super
obj_attr = params[:object_attributes]
obj_attr = HashWithIndifferentAccess.new(obj_attr)
......@@ -27,15 +22,24 @@ module ChatMessage
def attachments
return [] unless opened_issue?
return description if markdown
description_message
end
def activity
{
title: "Issue #{state} by #{user_name}",
subtitle: "in #{project_link}",
text: issue_link,
image: user_avatar
}
end
private
def message
case state
when "opened"
if state == 'opened'
"[#{project_link}] Issue #{state} by #{user_name}"
else
"[#{project_link}] Issue #{issue_link} #{state} by #{user_name}"
......@@ -64,7 +68,7 @@ module ChatMessage
end
def issue_title
"##{issue_iid} #{title}"
"#{Issue.reference_prefix}#{issue_iid} #{title}"
end
end
end
module ChatMessage
class MergeMessage < BaseMessage
attr_reader :user_name
attr_reader :project_name
attr_reader :project_url
attr_reader :merge_request_id
attr_reader :merge_request_iid
attr_reader :source_branch
attr_reader :target_branch
attr_reader :state
attr_reader :title
def initialize(params)
@user_name = params[:user][:username]
@project_name = params[:project_name]
@project_url = params[:project_url]
super
obj_attr = params[:object_attributes]
obj_attr = HashWithIndifferentAccess.new(obj_attr)
@merge_request_id = obj_attr[:iid]
@merge_request_iid = obj_attr[:iid]
@source_branch = obj_attr[:source_branch]
@target_branch = obj_attr[:target_branch]
@state = obj_attr[:state]
@title = format_title(obj_attr[:title])
end
def pretext
format(message)
end
def attachments
[]
end
def activity
{
title: "Merge Request #{state} by #{user_name}",
subtitle: "in #{project_link}",
text: merge_request_link,
image: user_avatar
}
end
private
def format_title(title)
......@@ -50,11 +50,15 @@ module ChatMessage
end
def merge_request_link
link("merge request !#{merge_request_id}", merge_request_url)
link(merge_request_title, merge_request_url)
end
def merge_request_title
"#{MergeRequest.reference_prefix}#{merge_request_iid} #{title}"
end
def merge_request_url
"#{project_url}/merge_requests/#{merge_request_id}"
"#{project_url}/merge_requests/#{merge_request_iid}"
end
end
end
module ChatMessage
class NoteMessage < BaseMessage
attr_reader :message
attr_reader :user_name
attr_reader :project_name
attr_reader :project_url
attr_reader :note
attr_reader :note_url
attr_reader :title
attr_reader :target
def initialize(params)
params = HashWithIndifferentAccess.new(params)
@user_name = params[:user][:username]
@project_name = params[:project_name]
@project_url = params[:project_url]
super
params = HashWithIndifferentAccess.new(params)
obj_attr = params[:object_attributes]
obj_attr = HashWithIndifferentAccess.new(obj_attr)
@note = obj_attr[:note]
@note_url = obj_attr[:url]
noteable_type = obj_attr[:noteable_type]
case noteable_type
@target, @title = case obj_attr[:noteable_type]
when "Commit"
create_commit_note(HashWithIndifferentAccess.new(params[:commit]))
create_commit_note(params[:commit])
when "Issue"
create_issue_note(HashWithIndifferentAccess.new(params[:issue]))
create_issue_note(params[:issue])
when "MergeRequest"
create_merge_note(HashWithIndifferentAccess.new(params[:merge_request]))
create_merge_note(params[:merge_request])
when "Snippet"
create_snippet_note(HashWithIndifferentAccess.new(params[:snippet]))
create_snippet_note(params[:snippet])
end
end
def attachments
return note if markdown
description_message
end
def activity
{
title: "#{user_name} #{link('commented on ' + target, note_url)}",
subtitle: "in #{project_link}",
text: formatted_title,
image: user_avatar
}
end
private
def message
"#{user_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{formatted_title}*"
end
def format_title(title)
title.lines.first.chomp
end
def create_commit_note(commit)
commit_sha = commit[:id]
commit_sha = Commit.truncate_sha(commit_sha)
commented_on_message(
"commit #{commit_sha}",
format_title(commit[:message]))
def formatted_title
format_title(title)
end
def create_issue_note(issue)
commented_on_message(
"issue ##{issue[:iid]}",
format_title(issue[:title]))
["issue #{Issue.reference_prefix}#{issue[:iid]}", issue[:title]]
end
def create_commit_note(commit)
commit_sha = Commit.truncate_sha(commit[:id])
["commit #{commit_sha}", commit[:message]]
end
def create_merge_note(merge_request)
commented_on_message(
"merge request !#{merge_request[:iid]}",
format_title(merge_request[:title]))
["merge request #{MergeRequest.reference_prefix}#{merge_request[:iid]}", merge_request[:title]]
end
def create_snippet_note(snippet)
commented_on_message(
"snippet ##{snippet[:id]}",
format_title(snippet[:title]))
["snippet #{Snippet.reference_prefix}#{snippet[:id]}", snippet[:title]]
end
def description_message
......@@ -74,9 +78,5 @@ module ChatMessage
def project_link
link(project_name, project_url)
end
def commented_on_message(target, title)
@message = "#{user_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{title}*"
end
end
end
module ChatMessage
class PipelineMessage < BaseMessage
attr_reader :ref_type, :ref, :status, :project_name, :project_url,
:user_name, :duration, :pipeline_id
attr_reader :ref_type
attr_reader :ref
attr_reader :status
attr_reader :duration
attr_reader :pipeline_id
def initialize(data)
super
@user_name = data.dig(:user, :name) || 'API'
pipeline_attributes = data[:object_attributes]
@ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
@ref = pipeline_attributes[:ref]
@status = pipeline_attributes[:status]
@duration = pipeline_attributes[:duration]
@pipeline_id = pipeline_attributes[:id]
@project_name = data[:project][:path_with_namespace]
@project_url = data[:project][:web_url]
@user_name = (data[:user] && data[:user][:name]) || 'API'
end
def pretext
......@@ -25,17 +28,24 @@ module ChatMessage
end
def attachments
return message if markdown
[{ text: format(message), color: attachment_color }]
end
def activity
{
title: "Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status}",
subtitle: "in #{project_link}",
text: "in #{duration} #{time_measure}",
image: user_avatar || ''
}
end
private
def message
"#{project_link}: Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}"
end
def format(string)
Slack::Notifier::LinkFormatter.format(string)
"#{project_link}: Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{time_measure}"
end
def humanized_status
......@@ -74,5 +84,9 @@ module ChatMessage
def pipeline_link
"[##{pipeline_id}](#{pipeline_url})"
end
def time_measure
'second'.pluralize(duration)
end
end
end
......@@ -3,33 +3,43 @@ module ChatMessage
attr_reader :after
attr_reader :before
attr_reader :commits
attr_reader :project_name
attr_reader :project_url
attr_reader :ref
attr_reader :ref_type
attr_reader :user_name
def initialize(params)
super
@after = params[:after]
@before = params[:before]
@commits = params.fetch(:commits, [])
@project_name = params[:project_name]
@project_url = params[:project_url]
@ref_type = Gitlab::Git.tag_ref?(params[:ref]) ? 'tag' : 'branch'
@ref = Gitlab::Git.ref_name(params[:ref])
@user_name = params[:user_name]
end
def pretext
format(message)
end
def attachments
return [] if new_branch? || removed_branch?
return commit_messages if markdown
commit_message_attachments
end
def activity
action = if new_branch?
"created"
elsif removed_branch?
"removed"
else
"pushed to"
end
{
title: "#{user_name} #{action} #{ref_type}",
subtitle: "in #{project_link}",
text: compare_link,
image: user_avatar
}
end
private
def message
......@@ -59,7 +69,7 @@ module ChatMessage
end
def commit_messages
commits.map { |commit| compose_commit_message(commit) }.join("\n")
commits.map { |commit| compose_commit_message(commit) }.join("\n\n")
end
def commit_message_attachments
......
module ChatMessage
class WikiPageMessage < BaseMessage
attr_reader :user_name
attr_reader :title
attr_reader :project_name
attr_reader :project_url
attr_reader :wiki_page_url
attr_reader :action
attr_reader :description
def initialize(params)
@user_name = params[:user][:username]
@project_name = params[:project_name]
@project_url = params[:project_url]
super
obj_attr = params[:object_attributes]
obj_attr = HashWithIndifferentAccess.new(obj_attr)
......@@ -29,9 +24,20 @@ module ChatMessage
end
def attachments
return description if markdown
description_message
end
def activity
{
title: "#{user_name} #{action} #{wiki_page_link}",
subtitle: "in #{project_link}",
text: title,
image: user_avatar
}
end
private
def message
......
......@@ -49,10 +49,7 @@ class ChatNotificationService < Service
object_kind = data[:object_kind]
data = data.merge(
project_url: project_url,
project_name: project_name
)
data = custom_data(data)
# WebHook events often have an 'update' event that follows a 'open' or
# 'close' action. Ignore update events for now to prevent duplicate
......@@ -68,8 +65,7 @@ class ChatNotificationService < Service
opts[:channel] = channel_name if channel_name
opts[:username] = username if username
notifier = Slack::Notifier.new(webhook, opts)
notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
return false unless notify(message, opts)
true
end
......@@ -92,6 +88,18 @@ class ChatNotificationService < Service
private
def notify(message, opts)
Slack::Notifier.new(webhook, opts).ping(
message.pretext,
attachments: message.attachments,
fallback: message.fallback
)
end
def custom_data(data)
data.merge(project_url: project_url, project_name: project_name)
end
def get_message(object_kind, data)
case object_kind
when "push", "tag_push"
......
class MicrosoftTeamsService < ChatNotificationService
def title
'Microsoft Teams Notification'
end
def description
'Receive event notifications in Microsoft Teams'
end
def self.to_param
'microsoft_teams'
end
def help
'This service sends notifications about projects events to Microsoft Teams channels.<br />
To set up this service:
<ol>
<li><a href="https://msdn.microsoft.com/en-us/microsoft-teams/connectors">Getting started with 365 Office Connectors For Microsoft Teams</a>.</li>
<li>Paste the <strong>Webhook URL</strong> into the field below.</li>
<li>Select events below to enable notifications.</li>
</ol>'
end
def webhook_placeholder
'https://outlook.office.com/webhook/…'
end
def event_field(event)
end
def default_channel_placeholder
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{ type: 'checkbox', name: 'notify_only_default_branch' },
]
end
private
def notify(message, opts)
MicrosoftTeams::Notifier.new(webhook).ping(
title: message.project_name,
pretext: message.pretext,
activity: message.activity,
attachments: message.attachments
)
end
def custom_data(data)
super(data).merge(markdown: true)
end
end
......@@ -237,6 +237,7 @@ class Service < ActiveRecord::Base
slack_slash_commands
slack
teamcity
microsoft_teams
]
if Rails.env.development?
service_names += %w[mock_ci mock_deployment mock_monitoring]
......
---
title: Integrates Microsoft Teams webhooks with GitLab
merge_request: 10412
author:
# Microsoft Teams Service
## On Microsoft Teams
To enable Microsoft Teams integration you must create an incoming webhook integration on Microsoft Teams by following the steps described in this [document](https://msdn.microsoft.com/en-us/microsoft-teams/connectors)
## On GitLab
After you set up Microsoft Teams, it's time to set up GitLab.
Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
and select the **Microsoft Teams Notification** service to configure it.
There, you will see a checkbox with the following events that can be triggered:
- Push
- Issue
- Confidential issue
- Merge request
- Note
- Tag push
- Pipeline
- Wiki page
At the end fill in your Microsoft Teams details:
| Field | Description |
| ----- | ----------- |
| **Webhook** | The incoming webhook URL which you have to setup on Microsoft Teams. |
| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. |
After you are all done, click **Save changes** for the changes to take effect.
![Microsoft Teams configuration](img/microsoft_teams_configuration.png)
\ No newline at end of file
......@@ -488,6 +488,14 @@ module API
desc: 'The channel name'
}
],
'microsoft-teams' => [
{
required: true,
name: :webhook,
type: String,
desc: 'The Microsoft Teams webhook. e.g. https://outlook.office.com/webhook/…'
}
],
'mattermost' => [
{
required: true,
......@@ -550,6 +558,7 @@ module API
RedmineService,
SlackService,
MattermostService,
MicrosoftTeamsService,
TeamcityService,
]
......
......@@ -501,6 +501,12 @@ module API
desc: 'The channel name'
}
],
'microsoft-teams' => [
required: true,
name: :webhook,
type: String,
desc: 'The Microsoft Teams webhook. e.g. https://outlook.office.com/webhook/…'
],
'mattermost' => [
{
required: true,
......
module MicrosoftTeams
class Activity
def initialize(title:, subtitle:, text:, image:)
@title = title
@subtitle = subtitle
@text = text
@image = image
end
def prepare
{
'activityTitle' => @title,
'activitySubtitle' => @subtitle,
'activityText' => @text,
'activityImage' => @image
}
end
end
end
module MicrosoftTeams
class Notifier
def initialize(webhook)
@webhook = webhook
@header = { 'Content-type' => 'application/json' }
end
def ping(options = {})
result = false
begin
response = HTTParty.post(
@webhook.to_str,
headers: @header,
body: body(options)
)
result = true if response
rescue HTTParty::Error, StandardError => error
Rails.logger.info("#{self.class.name}: Error while connecting to #{@webhook}: #{error.message}")
end
result
end
private
def body(options = {})
result = { 'sections' => [] }
result['title'] = options[:title]
result['summary'] = options[:pretext]
result['sections'] << MicrosoftTeams::Activity.new(options[:activity]).prepare
attachments = options[:attachments]
unless attachments.blank?
result['sections'] << {
'title' => 'Details',
'facts' => [{ 'name' => 'Attachments', 'value' => attachments }]
}
end
result.to_json
end
end
end
......@@ -149,6 +149,7 @@ project:
- asana_service
- gemnasium_service
- slack_service
- microsoft_teams_service
- mattermost_service
- buildkite_service
- bamboo_service
......
require 'spec_helper'
describe MicrosoftTeams::Activity do
subject { described_class.new(title: 'title', subtitle: 'subtitle', text: 'text', image: 'image') }
describe '#prepare' do
it 'returns the correct JSON object' do
expect(subject.prepare).to eq({
'activityTitle' => 'title',
'activitySubtitle' => 'subtitle',
'activityText' => 'text',
'activityImage' => 'image'
})
end
end
end
require 'spec_helper'
describe MicrosoftTeams::Notifier do
subject { described_class.new(webhook_url) }
let(:webhook_url) { 'https://example.gitlab.com/'}
let(:header) { { 'Content-Type' => 'application/json' } }
let(:options) do
{
title: 'JohnDoe4/project2',
pretext: '[[JohnDoe4/project2](http://localhost/namespace2/gitlabhq)] Issue [#1 Awesome issue](http://localhost/namespace2/gitlabhq/issues/1) opened by user6',
activity: {
title: 'Issue opened by user6',
subtitle: 'in [JohnDoe4/project2](http://localhost/namespace2/gitlabhq)',
text: '[#1 Awesome issue](http://localhost/namespace2/gitlabhq/issues/1)',
image: 'http://someimage.com'
},
attachments: 'please fix'
}
end
let(:body) do
{
'sections' => [
{
'activityTitle' => 'Issue opened by user6',
'activitySubtitle' => 'in [JohnDoe4/project2](http://localhost/namespace2/gitlabhq)',
'activityText' => '[#1 Awesome issue](http://localhost/namespace2/gitlabhq/issues/1)',
'activityImage' => 'http://someimage.com'
},
{
'title' => 'Details',
'facts' => [
{
'name' => 'Attachments',
'value' => 'please fix'
}
]
}
],
'title' => 'JohnDoe4/project2',
'summary' => '[[JohnDoe4/project2](http://localhost/namespace2/gitlabhq)] Issue [#1 Awesome issue](http://localhost/namespace2/gitlabhq/issues/1) opened by user6'
}
end
describe '#ping' do
before do
stub_request(:post, webhook_url).with(body: JSON(body), headers: { 'Content-Type' => 'application/json' }).to_return(status: 200, body: "", headers: {})
end
it 'expects to receive successfull answer' do
expect(subject.ping(options)).to be true
end
end
end
......@@ -7,7 +7,8 @@ describe ChatMessage::IssueMessage, models: true do
{
user: {
name: 'Test User',
username: 'test.user'
username: 'test.user',
avatar_url: 'http://someavatar.com'
},
project_name: 'project_name',
project_url: 'http://somewhere.com',
......@@ -25,6 +26,7 @@ describe ChatMessage::IssueMessage, models: true do
}
end
context 'without markdown' do
let(:color) { '#C95823' }
context '#initialize' do
......@@ -64,4 +66,44 @@ describe ChatMessage::IssueMessage, models: true do
expect(subject.attachments).to be_empty
end
end
end
context 'with markdown' do
before do
args[:markdown] = true
end
context 'open' do
it 'returns a message regarding opening of issues' do
expect(subject.pretext).to eq(
'[[project_name](http://somewhere.com)] Issue opened by test.user')
expect(subject.attachments).to eq('issue description')
expect(subject.activity).to eq({
title: 'Issue opened by test.user',
subtitle: 'in [project_name](http://somewhere.com)',
text: '[#100 Issue title](http://url.com)',
image: 'http://someavatar.com'
})
end
end
context 'close' do
before do
args[:object_attributes][:action] = 'close'
args[:object_attributes][:state] = 'closed'
end
it 'returns a message regarding closing of issues' do
expect(subject.pretext). to eq(
'[[project_name](http://somewhere.com)] Issue [#100 Issue title](http://url.com) closed by test.user')
expect(subject.attachments).to be_empty
expect(subject.activity).to eq({
title: 'Issue closed by test.user',
subtitle: 'in [project_name](http://somewhere.com)',
text: '[#100 Issue title](http://url.com)',
image: 'http://someavatar.com'
})
end
end
end
end
......@@ -7,32 +7,33 @@ describe ChatMessage::MergeMessage, models: true do
{
user: {
name: 'Test User',
username: 'test.user'
username: 'test.user',
avatar_url: 'http://someavatar.com'
},
project_name: 'project_name',
project_url: 'http://somewhere.com',
object_attributes: {
title: "Issue title\nSecond line",
title: "Merge Request title\nSecond line",
id: 10,
iid: 100,
assignee_id: 1,
url: 'http://url.com',
state: 'opened',
description: 'issue description',
description: 'merge request description',
source_branch: 'source_branch',
target_branch: 'target_branch',
}
}
end
context 'without markdown' do
let(:color) { '#345' }
context 'open' do
it 'returns a message regarding opening of merge requests' do
expect(subject.pretext).to eq(
'test.user opened <http://somewhere.com/merge_requests/100|merge request !100> '\
'in <http://somewhere.com|project_name>: *Issue title*')
'test.user opened <http://somewhere.com/merge_requests/100|!100 *Merge Request title*> in <http://somewhere.com|project_name>: *Merge Request title*')
expect(subject.attachments).to be_empty
end
end
......@@ -43,9 +44,47 @@ describe ChatMessage::MergeMessage, models: true do
end
it 'returns a message regarding closing of merge requests' do
expect(subject.pretext).to eq(
'test.user closed <http://somewhere.com/merge_requests/100|merge request !100> '\
'in <http://somewhere.com|project_name>: *Issue title*')
'test.user closed <http://somewhere.com/merge_requests/100|!100 *Merge Request title*> in <http://somewhere.com|project_name>: *Merge Request title*')
expect(subject.attachments).to be_empty
end
end
end
context 'with markdown' do
before do
args[:markdown] = true
end
context 'open' do
it 'returns a message regarding opening of merge requests' do
expect(subject.pretext).to eq(
'test.user opened [!100 *Merge Request title*](http://somewhere.com/merge_requests/100) in [project_name](http://somewhere.com): *Merge Request title*')
expect(subject.attachments).to be_empty
expect(subject.activity).to eq({
title: 'Merge Request opened by test.user',
subtitle: 'in [project_name](http://somewhere.com)',
text: '[!100 *Merge Request title*](http://somewhere.com/merge_requests/100)',
image: 'http://someavatar.com'
})
end
end
context 'close' do
before do
args[:object_attributes][:state] = 'closed'
end
it 'returns a message regarding closing of merge requests' do
expect(subject.pretext).to eq(
'test.user closed [!100 *Merge Request title*](http://somewhere.com/merge_requests/100) in [project_name](http://somewhere.com): *Merge Request title*')
expect(subject.attachments).to be_empty
expect(subject.activity).to eq({
title: 'Merge Request closed by test.user',
subtitle: 'in [project_name](http://somewhere.com)',
text: '[!100 *Merge Request title*](http://somewhere.com/merge_requests/100)',
image: 'http://someavatar.com'
})
end
end
end
end
require 'spec_helper'
describe ChatMessage::NoteMessage, models: true do
let(:color) { '#345' }
subject { described_class.new(args) }
before do
@args = {
let(:color) { '#345' }
let(:args) do
{
user: {
name: 'Test User',
username: 'test.user',
......@@ -27,104 +28,163 @@ describe ChatMessage::NoteMessage, models: true do
context 'commit notes' do
before do
@args[:object_attributes][:note] = 'comment on a commit'
@args[:object_attributes][:noteable_type] = 'Commit'
@args[:commit] = {
args[:object_attributes][:note] = 'comment on a commit'
args[:object_attributes][:noteable_type] = 'Commit'
args[:commit] = {
id: '5f163b2b95e6f53cbd428f5f0b103702a52b9a23',
message: "Added a commit message\ndetails\n123\n"
}
end
context 'without markdown' do
it 'returns a message regarding notes on commits' do
message = described_class.new(@args)
expect(message.pretext).to eq("test.user <http://url.com|commented on " \
expect(subject.pretext).to eq("test.user <http://url.com|commented on " \
"commit 5f163b2b> in <http://somewhere.com|project_name>: " \
"*Added a commit message*")
expected_attachments = [
{
text: "comment on a commit",
color: color,
}
]
expect(message.attachments).to eq(expected_attachments)
expect(subject.attachments).to eq([{
text: 'comment on a commit',
color: color
}])
end
end
context 'with markdown' do
before do
args[:markdown] = true
end
it 'returns a message regarding notes on commits' do
expect(subject.pretext).to eq(
'test.user [commented on commit 5f163b2b](http://url.com) in [project_name](http://somewhere.com): *Added a commit message*'
)
expect(subject.attachments).to eq('comment on a commit')
expect(subject.activity).to eq({
title: 'test.user [commented on commit 5f163b2b](http://url.com)',
subtitle: 'in [project_name](http://somewhere.com)',
text: 'Added a commit message',
image: 'http://fakeavatar'
})
end
end
end
context 'merge request notes' do
before do
@args[:object_attributes][:note] = 'comment on a merge request'
@args[:object_attributes][:noteable_type] = 'MergeRequest'
@args[:merge_request] = {
args[:object_attributes][:note] = 'comment on a merge request'
args[:object_attributes][:noteable_type] = 'MergeRequest'
args[:merge_request] = {
id: 1,
iid: 30,
title: "merge request title\ndetails\n"
}
end
context 'without markdown' do
it 'returns a message regarding notes on a merge request' do
message = described_class.new(@args)
expect(message.pretext).to eq("test.user <http://url.com|commented on " \
expect(subject.pretext).to eq("test.user <http://url.com|commented on " \
"merge request !30> in <http://somewhere.com|project_name>: " \
"*merge request title*")
expected_attachments = [
{
text: "comment on a merge request",
color: color,
}
]
expect(message.attachments).to eq(expected_attachments)
expect(subject.attachments).to eq([{
text: 'comment on a merge request',
color: color
}])
end
end
context 'with markdown' do
before do
args[:markdown] = true
end
it 'returns a message regarding notes on a merge request' do
expect(subject.pretext).to eq(
'test.user [commented on merge request !30](http://url.com) in [project_name](http://somewhere.com): *merge request title*')
expect(subject.attachments).to eq('comment on a merge request')
expect(subject.activity).to eq({
title: 'test.user [commented on merge request !30](http://url.com)',
subtitle: 'in [project_name](http://somewhere.com)',
text: 'merge request title',
image: 'http://fakeavatar'
})
end
end
end
context 'issue notes' do
before do
@args[:object_attributes][:note] = 'comment on an issue'
@args[:object_attributes][:noteable_type] = 'Issue'
@args[:issue] = {
args[:object_attributes][:note] = 'comment on an issue'
args[:object_attributes][:noteable_type] = 'Issue'
args[:issue] = {
id: 1,
iid: 20,
title: "issue title\ndetails\n"
}
end
context 'without markdown' do
it 'returns a message regarding notes on an issue' do
message = described_class.new(@args)
expect(message.pretext).to eq(
expect(subject.pretext).to eq(
"test.user <http://url.com|commented on " \
"issue #20> in <http://somewhere.com|project_name>: " \
"*issue title*")
expected_attachments = [
{
text: "comment on an issue",
color: color,
}
]
expect(message.attachments).to eq(expected_attachments)
expect(subject.attachments).to eq([{
text: 'comment on an issue',
color: color
}])
end
end
context 'with markdown' do
before do
args[:markdown] = true
end
it 'returns a message regarding notes on an issue' do
expect(subject.pretext).to eq(
'test.user [commented on issue #20](http://url.com) in [project_name](http://somewhere.com): *issue title*')
expect(subject.attachments).to eq('comment on an issue')
expect(subject.activity).to eq({
title: 'test.user [commented on issue #20](http://url.com)',
subtitle: 'in [project_name](http://somewhere.com)',
text: 'issue title',
image: 'http://fakeavatar'
})
end
end
end
context 'project snippet notes' do
before do
@args[:object_attributes][:note] = 'comment on a snippet'
@args[:object_attributes][:noteable_type] = 'Snippet'
@args[:snippet] = {
args[:object_attributes][:note] = 'comment on a snippet'
args[:object_attributes][:noteable_type] = 'Snippet'
args[:snippet] = {
id: 5,
title: "snippet title\ndetails\n"
}
end
context 'without markdown' do
it 'returns a message regarding notes on a project snippet' do
message = described_class.new(@args)
expect(message.pretext).to eq("test.user <http://url.com|commented on " \
"snippet #5> in <http://somewhere.com|project_name>: " \
expect(subject.pretext).to eq("test.user <http://url.com|commented on " \
"snippet $5> in <http://somewhere.com|project_name>: " \
"*snippet title*")
expected_attachments = [
{
text: "comment on a snippet",
color: color,
}
]
expect(message.attachments).to eq(expected_attachments)
expect(subject.attachments).to eq([{
text: 'comment on a snippet',
color: color
}])
end
end
context 'with markdown' do
before do
args[:markdown] = true
end
it 'returns a message regarding notes on a project snippet' do
expect(subject.pretext).to eq(
'test.user [commented on snippet $5](http://url.com) in [project_name](http://somewhere.com): *snippet title*')
expect(subject.attachments).to eq('comment on a snippet')
end
end
end
end
......@@ -2,8 +2,8 @@ require 'spec_helper'
describe ChatMessage::PipelineMessage do
subject { described_class.new(args) }
let(:user) { { name: 'hacker' } }
let(:user) { { name: 'hacker' } }
let(:args) do
{
object_attributes: {
......@@ -14,14 +14,15 @@ describe ChatMessage::PipelineMessage do
status: status,
duration: duration
},
project: { path_with_namespace: 'project_name',
web_url: 'http://example.gitlab.com' },
project: {
path_with_namespace: 'project_name',
web_url: 'http://example.gitlab.com'
},
user: user
}
end
let(:message) { build_message }
context 'without markdown' do
context 'pipeline succeeded' do
let(:status) { 'success' }
let(:color) { 'good' }
......@@ -29,7 +30,9 @@ describe ChatMessage::PipelineMessage do
let(:message) { build_message('passed') }
it 'returns a message with information about succeeded build' do
verify_message
expect(subject.pretext).to be_empty
expect(subject.fallback).to eq(message)
expect(subject.attachments).to eq([text: message, color: color])
end
end
......@@ -37,9 +40,12 @@ describe ChatMessage::PipelineMessage do
let(:status) { 'failed' }
let(:color) { 'danger' }
let(:duration) { 10 }
let(:message) { build_message }
it 'returns a message with information about failed build' do
verify_message
expect(subject.pretext).to be_empty
expect(subject.fallback).to eq(message)
expect(subject.attachments).to eq([text: message, color: color])
end
context 'when triggered by API therefore lacking user' do
......@@ -47,16 +53,12 @@ describe ChatMessage::PipelineMessage do
let(:message) { build_message(status, 'API') }
it 'returns a message stating it is by API' do
verify_message
end
end
end
def verify_message
expect(subject.pretext).to be_empty
expect(subject.fallback).to eq(message)
expect(subject.attachments).to eq([text: message, color: color])
end
end
end
def build_message(status_text = status, name = user[:name])
"<http://example.gitlab.com|project_name>:" \
......@@ -64,4 +66,70 @@ describe ChatMessage::PipelineMessage do
" of <http://example.gitlab.com/commits/develop|develop> branch" \
" by #{name} #{status_text} in #{duration} #{'second'.pluralize(duration)}"
end
end
context 'with markdown' do
before do
args[:markdown] = true
end
context 'pipeline succeeded' do
let(:status) { 'success' }
let(:color) { 'good' }
let(:duration) { 10 }
let(:message) { build_markdown_message('passed') }
it 'returns a message with information about succeeded build' do
expect(subject.pretext).to be_empty
expect(subject.attachments).to eq(message)
expect(subject.activity).to eq({
title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of [develop](http://example.gitlab.com/commits/develop) branch by hacker passed',
subtitle: 'in [project_name](http://example.gitlab.com)',
text: 'in 10 seconds',
image: ''
})
end
end
context 'pipeline failed' do
let(:status) { 'failed' }
let(:color) { 'danger' }
let(:duration) { 10 }
let(:message) { build_markdown_message }
it 'returns a message with information about failed build' do
expect(subject.pretext).to be_empty
expect(subject.attachments).to eq(message)
expect(subject.activity).to eq({
title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of [develop](http://example.gitlab.com/commits/develop) branch by hacker failed',
subtitle: 'in [project_name](http://example.gitlab.com)',
text: 'in 10 seconds',
image: ''
})
end
context 'when triggered by API therefore lacking user' do
let(:user) { nil }
let(:message) { build_markdown_message(status, 'API') }
it 'returns a message stating it is by API' do
expect(subject.pretext).to be_empty
expect(subject.attachments).to eq(message)
expect(subject.activity).to eq({
title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of [develop](http://example.gitlab.com/commits/develop) branch by API failed',
subtitle: 'in [project_name](http://example.gitlab.com)',
text: 'in 10 seconds',
image: ''
})
end
end
end
def build_markdown_message(status_text = status, name = user[:name])
"[project_name](http://example.gitlab.com):" \
" Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
" of [develop](http://example.gitlab.com/commits/develop)" \
" branch by #{name} #{status_text} in #{duration} #{'second'.pluralize(duration)}"
end
end
end
......@@ -10,6 +10,7 @@ describe ChatMessage::PushMessage, models: true do
project_name: 'project_name',
ref: 'refs/heads/master',
user_name: 'test.user',
user_avatar: 'http://someavatar.com',
project_url: 'http://url.com'
}
end
......@@ -24,18 +25,36 @@ describe ChatMessage::PushMessage, models: true do
]
end
context 'without markdown' do
it 'returns a message regarding pushes' do
expect(subject.pretext).to eq(
'test.user pushed to branch <http://url.com/commits/master|master> of '\
'<http://url.com|project_name> (<http://url.com/compare/before...after|Compare changes>)'
)
expect(subject.attachments).to eq([
{
text: "<http://url1.com|abcdefgh>: message1 - author1\n"\
'<http://url.com|project_name> (<http://url.com/compare/before...after|Compare changes>)')
expect(subject.attachments).to eq([{
text: "<http://url1.com|abcdefgh>: message1 - author1\n\n"\
"<http://url2.com|12345678>: message2 - author2",
color: color,
}
])
}])
end
end
context 'with markdown' do
before do
args[:markdown] = true
end
it 'returns a message regarding pushes' do
expect(subject.pretext).to eq(
'test.user pushed to branch [master](http://url.com/commits/master) of [project_name](http://url.com) ([Compare changes](http://url.com/compare/before...after))')
expect(subject.attachments).to eq(
"[abcdefgh](http://url1.com): message1 - author1\n\n[12345678](http://url2.com): message2 - author2")
expect(subject.activity).to eq({
title: 'test.user pushed to branch',
subtitle: 'in [project_name](http://url.com)',
text: '[Compare changes](http://url.com/compare/before...after)',
image: 'http://someavatar.com'
})
end
end
end
......@@ -47,10 +66,12 @@ describe ChatMessage::PushMessage, models: true do
project_name: 'project_name',
ref: 'refs/tags/new_tag',
user_name: 'test.user',
user_avatar: 'http://someavatar.com',
project_url: 'http://url.com'
}
end
context 'without markdown' do
it 'returns a message regarding pushes' do
expect(subject.pretext).to eq('test.user pushed new tag ' \
'<http://url.com/commits/new_tag|new_tag> to ' \
......@@ -59,30 +80,87 @@ describe ChatMessage::PushMessage, models: true do
end
end
context 'with markdown' do
before do
args[:markdown] = true
end
it 'returns a message regarding pushes' do
expect(subject.pretext).to eq(
'test.user pushed new tag [new_tag](http://url.com/commits/new_tag) to [project_name](http://url.com)')
expect(subject.attachments).to be_empty
expect(subject.activity).to eq({
title: 'test.user created tag',
subtitle: 'in [project_name](http://url.com)',
text: '[Compare changes](http://url.com/compare/0000000000000000000000000000000000000000...after)',
image: 'http://someavatar.com'
})
end
end
end
context 'new branch' do
before do
args[:before] = Gitlab::Git::BLANK_SHA
end
context 'without markdown' do
it 'returns a message regarding a new branch' do
expect(subject.pretext).to eq(
'test.user pushed new branch <http://url.com/commits/master|master> to '\
'<http://url.com|project_name>'
)
'<http://url.com|project_name>')
expect(subject.attachments).to be_empty
end
end
context 'with markdown' do
before do
args[:markdown] = true
end
it 'returns a message regarding a new branch' do
expect(subject.pretext).to eq(
'test.user pushed new branch [master](http://url.com/commits/master) to [project_name](http://url.com)')
expect(subject.attachments).to be_empty
expect(subject.activity).to eq({
title: 'test.user created branch',
subtitle: 'in [project_name](http://url.com)',
text: '[Compare changes](http://url.com/compare/0000000000000000000000000000000000000000...after)',
image: 'http://someavatar.com'
})
end
end
end
context 'removed branch' do
before do
args[:after] = Gitlab::Git::BLANK_SHA
end
context 'without markdown' do
it 'returns a message regarding a removed branch' do
expect(subject.pretext).to eq(
'test.user removed branch master from <http://url.com|project_name>'
)
'test.user removed branch master from <http://url.com|project_name>')
expect(subject.attachments).to be_empty
end
end
context 'with markdown' do
before do
args[:markdown] = true
end
it 'returns a message regarding a removed branch' do
expect(subject.pretext).to eq(
'test.user removed branch master from [project_name](http://url.com)')
expect(subject.attachments).to be_empty
expect(subject.activity).to eq({
title: 'test.user removed branch',
subtitle: 'in [project_name](http://url.com)',
text: '[Compare changes](http://url.com/compare/before...0000000000000000000000000000000000000000)',
image: 'http://someavatar.com'
})
end
end
end
end
......@@ -7,7 +7,8 @@ describe ChatMessage::WikiPageMessage, models: true do
{
user: {
name: 'Test User',
username: 'test.user'
username: 'test.user',
avatar_url: 'http://someavatar.com'
},
project_name: 'project_name',
project_url: 'http://somewhere.com',
......@@ -19,6 +20,7 @@ describe ChatMessage::WikiPageMessage, models: true do
}
end
context 'without markdown' do
describe '#pretext' do
context 'when :action == "create"' do
before { args[:object_attributes][:action] = 'create' }
......@@ -70,4 +72,77 @@ describe ChatMessage::WikiPageMessage, models: true do
end
end
end
end
context 'with markdown' do
before do
args[:markdown] = true
end
describe '#pretext' do
context 'when :action == "create"' do
before { args[:object_attributes][:action] = 'create' }
it 'returns a message that a new wiki page was created' do
expect(subject.pretext).to eq(
'test.user created [wiki page](http://url.com) in [project_name](http://somewhere.com): *Wiki page title*')
end
end
context 'when :action == "update"' do
before { args[:object_attributes][:action] = 'update' }
it 'returns a message that a wiki page was updated' do
expect(subject.pretext).to eq(
'test.user edited [wiki page](http://url.com) in [project_name](http://somewhere.com): *Wiki page title*')
end
end
end
describe '#attachments' do
context 'when :action == "create"' do
before { args[:object_attributes][:action] = 'create' }
it 'returns the attachment for a new wiki page' do
expect(subject.attachments).to eq('Wiki page description')
end
end
context 'when :action == "update"' do
before { args[:object_attributes][:action] = 'update' }
it 'returns the attachment for an updated wiki page' do
expect(subject.attachments).to eq('Wiki page description')
end
end
end
describe '#activity' do
context 'when :action == "create"' do
before { args[:object_attributes][:action] = 'create' }
it 'returns the attachment for a new wiki page' do
expect(subject.activity).to eq({
title: 'test.user created [wiki page](http://url.com)',
subtitle: 'in [project_name](http://somewhere.com)',
text: 'Wiki page title',
image: 'http://someavatar.com'
})
end
end
context 'when :action == "update"' do
before { args[:object_attributes][:action] = 'update' }
it 'returns the attachment for an updated wiki page' do
expect(subject.activity).to eq({
title: 'test.user edited [wiki page](http://url.com)',
subtitle: 'in [project_name](http://somewhere.com)',
text: 'Wiki page title',
image: 'http://someavatar.com'
})
end
end
end
end
end
require 'spec_helper'
describe MicrosoftTeamsService, models: true do
let(:chat_service) { described_class.new }
let(:webhook_url) { 'https://example.gitlab.com/' }
describe "Associations" do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
end
describe 'Validations' do
context 'when service is active' do
before { subject.active = true }
it { is_expected.to validate_presence_of(:webhook) }
it_behaves_like 'issue tracker service URL attribute', :webhook
end
context 'when service is inactive' do
before { subject.active = false }
it { is_expected.not_to validate_presence_of(:webhook) }
end
end
describe "#execute" do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
before do
allow(chat_service).to receive_messages(
project: project,
project_id: project.id,
service_hook: true,
webhook: webhook_url
)
WebMock.stub_request(:post, webhook_url)
end
context 'with push events' do
let(:push_sample_data) do
Gitlab::DataBuilder::Push.build_sample(project, user)
end
it "calls Microsoft Teams API for push events" do
chat_service.execute(push_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
it 'specifies the webhook when it is configured' do
expect(MicrosoftTeams::Notifier).to receive(:new).with(webhook_url).and_return(double(:microsoft_teams_service).as_null_object)
chat_service.execute(push_sample_data)
end
end
context 'with issue events' do
let(:opts) { { title: 'Awesome issue', description: 'please fix' } }
let(:issues_sample_data) do
service = Issues::CreateService.new(project, user, opts)
issue = service.execute
service.hook_data(issue, 'open')
end
it "calls Microsoft Teams API" do
chat_service.execute(issues_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
end
context 'with merge events' do
let(:opts) do
{
title: 'Awesome merge_request',
description: 'please fix',
source_branch: 'feature',
target_branch: 'master'
}
end
let(:merge_sample_data) do
service = MergeRequests::CreateService.new(project, user, opts)
merge_request = service.execute
service.hook_data(merge_request, 'open')
end
it "calls Microsoft Teams API" do
chat_service.execute(merge_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
end
context 'with wiki page events' do
let(:opts) do
{
title: "Awesome wiki_page",
content: "Some text describing some thing or another",
format: "md",
message: "user created page: Awesome wiki_page"
}
end
let(:wiki_page_sample_data) do
service = WikiPages::CreateService.new(project, user, opts)
wiki_page = service.execute
service.hook_data(wiki_page, 'create')
end
it "calls Microsoft Teams API" do
chat_service.execute(wiki_page_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
end
end
describe "Note events" do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, creator: user) }
before do
allow(chat_service).to receive_messages(
project: project,
project_id: project.id,
service_hook: true,
webhook: webhook_url
)
WebMock.stub_request(:post, webhook_url)
end
context 'when commit comment event executed' do
let(:commit_note) do
create(:note_on_commit, author: user,
project: project,
commit_id: project.repository.commit.id,
note: 'a comment on a commit')
end
it "calls Microsoft Teams API for commit comment events" do
data = Gitlab::DataBuilder::Note.build(commit_note, user)
chat_service.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
end
context 'when merge request comment event executed' do
let(:merge_request_note) do
create(:note_on_merge_request, project: project,
note: "merge request note")
end
it "calls Microsoft Teams API for merge request comment events" do
data = Gitlab::DataBuilder::Note.build(merge_request_note, user)
chat_service.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
end
context 'when issue comment event executed' do
let(:issue_note) do
create(:note_on_issue, project: project, note: "issue note")
end
it "calls Microsoft Teams API for issue comment events" do
data = Gitlab::DataBuilder::Note.build(issue_note, user)
chat_service.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
end
context 'when snippet comment event executed' do
let(:snippet_note) do
create(:note_on_project_snippet, project: project,
note: "snippet note")
end
it "calls Microsoft Teams API for snippet comment events" do
data = Gitlab::DataBuilder::Note.build(snippet_note, user)
chat_service.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
end
end
describe 'Pipeline events' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:pipeline) do
create(:ci_pipeline,
project: project, status: status,
sha: project.commit.sha, ref: project.default_branch)
end
before do
allow(chat_service).to receive_messages(
project: project,
service_hook: true,
webhook: webhook_url
)
end
shared_examples 'call Microsoft Teams API' do
before do
WebMock.stub_request(:post, webhook_url)
end
it 'calls Microsoft Teams API for pipeline events' do
data = Gitlab::DataBuilder::Pipeline.build(pipeline)
chat_service.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
end
context 'with failed pipeline' do
let(:status) { 'failed' }
it_behaves_like 'call Microsoft Teams API'
end
context 'with succeeded pipeline' do
let(:status) { 'success' }
context 'with default to notify_only_broken_pipelines' do
it 'does not call Microsoft Teams API for pipeline events' do
data = Gitlab::DataBuilder::Pipeline.build(pipeline)
result = chat_service.execute(data)
expect(result).to be_falsy
end
end
context 'with setting notify_only_broken_pipelines to false' do
before do
chat_service.notify_only_broken_pipelines = false
end
it_behaves_like 'call Microsoft Teams API'
end
end
context 'only notify for the default branch' do
context 'when enabled' do
let(:pipeline) do
create(:ci_pipeline, project: project, status: 'failed', ref: 'not-the-default-branch')
end
before do
chat_service.notify_only_default_branch = true
end
it 'does not call the Microsoft Teams API for pipeline events' do
data = Gitlab::DataBuilder::Pipeline.build(pipeline)
result = chat_service.execute(data)
expect(result).to be_falsy
end
end
end
end
end
......@@ -22,6 +22,7 @@ describe Project, models: true do
it { is_expected.to have_many(:protected_branches).dependent(:destroy) }
it { is_expected.to have_one(:forked_project_link).dependent(:destroy) }
it { is_expected.to have_one(:slack_service).dependent(:destroy) }
it { is_expected.to have_one(:microsoft_teams_service).dependent(:destroy) }
it { is_expected.to have_one(:mattermost_service).dependent(:destroy) }
it { is_expected.to have_one(:pushover_service).dependent(:destroy) }
it { is_expected.to have_one(:asana_service).dependent(:destroy) }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment