Commit c646f407 authored by Luke Duncalfe's avatar Luke Duncalfe

Move chat integrations to `Integrations::`

https://gitlab.com/gitlab-org/gitlab/-/issues/201855
parent c866f1f5
......@@ -1648,20 +1648,13 @@ Gitlab/NamespacedClass:
- 'app/models/project_services/alerts_service.rb'
- 'app/models/project_services/alerts_service_data.rb'
- 'app/models/project_services/chat_notification_service.rb'
- 'app/models/project_services/discord_service.rb'
- 'app/models/project_services/hangouts_chat_service.rb'
- 'app/models/project_services/mattermost_service.rb'
- 'app/models/project_services/mattermost_slash_commands_service.rb'
- 'app/models/project_services/microsoft_teams_service.rb'
- 'app/models/project_services/mock_monitoring_service.rb'
- 'app/models/project_services/monitoring_service.rb'
- 'app/models/project_services/prometheus_service.rb'
- 'app/models/project_services/pushover_service.rb'
- 'app/models/project_services/slack_service.rb'
- 'app/models/project_services/slack_slash_commands_service.rb'
- 'app/models/project_services/slash_commands_service.rb'
- 'app/models/project_services/unify_circuit_service.rb'
- 'app/models/project_services/webex_teams_service.rb'
- 'app/models/project_setting.rb'
- 'app/models/project_snippet.rb'
- 'app/models/project_statistics.rb'
......
......@@ -7,8 +7,8 @@ class ChatTeam < ApplicationRecord
belongs_to :namespace
def remove_mattermost_team(current_user)
Mattermost::Team.new(current_user).destroy(team_id: team_id)
rescue Mattermost::ClientError => e
::Mattermost::Team.new(current_user).destroy(team_id: team_id)
rescue ::Mattermost::ClientError => e
# Either the group is not found, or the user doesn't have the proper
# access on the mattermost instance. In the first case, we're done either way
# in the latter case, we can't recover by retrying, so we just log what happened
......
......@@ -2,7 +2,7 @@
# Concern handling functionality around deciding whether to send notification
# for activities on a specified branch or not. Will be included in
# ChatNotificationService and PipelinesEmailService classes.
# Integrations::BaseChatNotification and PipelinesEmailService classes.
module NotificationBranchSelection
extend ActiveSupport::Concern
......
# frozen_string_literal: true
# Base class for Chat notifications services
# This class is not meant to be used directly, but only to inherit from.
module Integrations
class BaseChatNotification < Integration
include ChatMessage
include NotificationBranchSelection
SUPPORTED_EVENTS = %w[
push issue confidential_issue merge_request note confidential_note
tag_push pipeline wiki_page deployment
].freeze
SUPPORTED_EVENTS_FOR_LABEL_FILTER = %w[issue confidential_issue merge_request note confidential_note].freeze
EVENT_CHANNEL = proc { |event| "#{event}_channel" }
LABEL_NOTIFICATION_BEHAVIOURS = [
MATCH_ANY_LABEL = 'match_any',
MATCH_ALL_LABELS = 'match_all'
].freeze
default_value_for :category, 'chat'
prop_accessor :webhook, :username, :channel, :branches_to_be_notified, :labels_to_be_notified, :labels_to_be_notified_behavior
# Custom serialized properties initialization
prop_accessor(*SUPPORTED_EVENTS.map { |event| EVENT_CHANNEL[event] })
boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch
validates :webhook, presence: true, public_url: true, if: :activated?
validates :labels_to_be_notified_behavior, inclusion: { in: LABEL_NOTIFICATION_BEHAVIOURS }, allow_blank: true
def initialize_properties
if properties.nil?
self.properties = {}
self.notify_only_broken_pipelines = true
self.branches_to_be_notified = "default"
self.labels_to_be_notified_behavior = MATCH_ANY_LABEL
elsif !self.notify_only_default_branch.nil?
# In older versions, there was only a boolean property named
# `notify_only_default_branch`. Now we have a string property named
# `branches_to_be_notified`. Instead of doing a background migration, we
# opted to set a value for the new property based on the old one, if
# users haven't specified one already. When users edit the service and
# select a value for this new property, it will override everything.
self.branches_to_be_notified ||= notify_only_default_branch? ? "default" : "all"
end
end
def confidential_issue_channel
properties['confidential_issue_channel'].presence || properties['issue_channel']
end
def confidential_note_channel
properties['confidential_note_channel'].presence || properties['note_channel']
end
def self.supported_events
SUPPORTED_EVENTS
end
def fields
default_fields + build_event_channels
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}", required: true }.freeze,
{ type: 'text', name: 'username', placeholder: 'GitLab-integration' }.freeze,
{ type: 'checkbox', name: 'notify_only_broken_pipelines', help: 'Do not send notifications for successful pipelines.' }.freeze,
{ type: 'select', name: 'branches_to_be_notified', choices: branch_choices }.freeze,
{
type: 'text',
name: 'labels_to_be_notified',
placeholder: '~backend,~frontend',
help: 'Send notifications for issue, merge request, and comment events with the listed labels only. Leave blank to receive notifications for all events.'
}.freeze,
{
type: 'select',
name: 'labels_to_be_notified_behavior',
choices: [
['Match any of the labels', MATCH_ANY_LABEL],
['Match all of the labels', MATCH_ALL_LABELS]
]
}.freeze
].freeze
end
def execute(data)
return unless supported_events.include?(data[:object_kind])
return unless webhook.present?
object_kind = data[:object_kind]
data = custom_data(data)
return unless notify_label?(data)
# WebHook events often have an 'update' event that follows a 'open' or
# 'close' action. Ignore update events for now to prevent duplicate
# messages from arriving.
message = get_message(object_kind, data)
return false unless message
event_type = data[:event_type] || object_kind
channel_names = get_channel_field(event_type).presence || channel.presence
channels = channel_names&.split(',')&.map(&:strip)
opts = {}
opts[:channel] = channels if channels.present?
opts[:username] = username if username
if notify(message, opts)
log_usage(event_type, user_id_from_hook_data(data))
return true
end
false
end
def event_channel_names
supported_events.map { |event| event_channel_name(event) }
end
def event_field(event)
fields.find { |field| field[:name] == event_channel_name(event) }
end
def global_fields
fields.reject { |field| field[:name].end_with?('channel') }
end
def default_channel_placeholder
raise NotImplementedError
end
private
def log_usage(_, _)
# Implement in child class
end
def labels_to_be_notified_list
return [] if labels_to_be_notified.nil?
labels_to_be_notified.delete('~').split(',').map(&:strip)
end
def notify_label?(data)
return true unless SUPPORTED_EVENTS_FOR_LABEL_FILTER.include?(data[:object_kind]) && labels_to_be_notified.present?
labels = data[:labels] || data.dig(:issue, :labels) || data.dig(:merge_request, :labels) || data.dig(:object_attributes, :labels)
return false if labels.blank?
matching_labels = labels_to_be_notified_list & labels.pluck(:title)
if labels_to_be_notified_behavior == MATCH_ALL_LABELS
labels_to_be_notified_list.difference(matching_labels).empty?
else
matching_labels.any?
end
end
def user_id_from_hook_data(data)
data.dig(:user, :id) || data[:user_id]
end
# every notifier must implement this independently
def notify(message, opts)
raise NotImplementedError
end
def custom_data(data)
data.merge(project_url: project_url, project_name: project_name).with_indifferent_access
end
def get_message(object_kind, data)
case object_kind
when "push", "tag_push"
Integrations::ChatMessage::PushMessage.new(data) if notify_for_ref?(data)
when "issue"
Integrations::ChatMessage::IssueMessage.new(data) unless update?(data)
when "merge_request"
Integrations::ChatMessage::MergeMessage.new(data) unless update?(data)
when "note"
Integrations::ChatMessage::NoteMessage.new(data)
when "pipeline"
Integrations::ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
when "wiki_page"
Integrations::ChatMessage::WikiPageMessage.new(data)
when "deployment"
Integrations::ChatMessage::DeploymentMessage.new(data)
end
end
def get_channel_field(event)
field_name = event_channel_name(event)
self.public_send(field_name) # rubocop:disable GitlabSecurity/PublicSend
end
def build_event_channels
supported_events.reduce([]) do |channels, event|
channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel_placeholder }
end
end
def event_channel_name(event)
EVENT_CHANNEL[event]
end
def project_name
project.full_name
end
def project_url
project.web_url
end
def update?(data)
data[:object_attributes][:action] == 'update'
end
def should_pipeline_be_notified?(data)
notify_for_ref?(data) && notify_for_pipeline?(data)
end
def notify_for_ref?(data)
return true if data[:object_kind] == 'tag_push'
return true if data.dig(:object_attributes, :tag)
notify_for_branch?(data)
end
def notify_for_pipeline?(data)
case data[:object_attributes][:status]
when 'success'
!notify_only_broken_pipelines?
when 'failed'
true
else
false
end
end
end
end
......@@ -58,7 +58,7 @@ module Integrations
end
def format(string)
Slack::Messenger::Util::LinkFormatter.format(format_relative_links(string))
::Slack::Messenger::Util::LinkFormatter.format(format_relative_links(string))
end
def format_relative_links(string)
......
......@@ -105,7 +105,7 @@ module Integrations
def failed_stages_field
{
title: s_("ChatMessage|Failed stage").pluralize(failed_stages.length),
value: Slack::Messenger::Util::LinkFormatter.format(failed_stages_links),
value: ::Slack::Messenger::Util::LinkFormatter.format(failed_stages_links),
short: true
}
end
......@@ -113,7 +113,7 @@ module Integrations
def failed_jobs_field
{
title: s_("ChatMessage|Failed job").pluralize(failed_jobs.length),
value: Slack::Messenger::Util::LinkFormatter.format(failed_jobs_links),
value: ::Slack::Messenger::Util::LinkFormatter.format(failed_jobs_links),
short: true
}
end
......@@ -130,12 +130,12 @@ module Integrations
fields = [
{
title: ref_type == "tag" ? s_("ChatMessage|Tag") : s_("ChatMessage|Branch"),
value: Slack::Messenger::Util::LinkFormatter.format(ref_link),
value: ::Slack::Messenger::Util::LinkFormatter.format(ref_link),
short: true
},
{
title: s_("ChatMessage|Commit"),
value: Slack::Messenger::Util::LinkFormatter.format(commit_link),
value: ::Slack::Messenger::Util::LinkFormatter.format(commit_link),
short: true
}
]
......
......@@ -49,7 +49,7 @@ module Integrations
end
def format(string)
Slack::Messenger::Util::LinkFormatter.format(string)
::Slack::Messenger::Util::LinkFormatter.format(string)
end
def commit_messages
......
# frozen_string_literal: true
require "discordrb/webhooks"
module Integrations
class Discord < BaseChatNotification
include ActionView::Helpers::UrlHelper
ATTACHMENT_REGEX = /: (?<entry>.*?)\n - (?<name>.*)\n*/.freeze
def title
s_("DiscordService|Discord Notifications")
end
def description
s_("DiscordService|Send notifications about project events to a Discord channel.")
end
def self.to_param
"discord"
end
def help
docs_link = link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/discord_notifications'), target: '_blank', rel: 'noopener noreferrer'
s_('Send notifications about project events to a Discord channel. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def event_field(event)
# No-op.
end
def default_channel_placeholder
# No-op.
end
def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page]
end
def default_fields
[
{ type: "text", name: "webhook", placeholder: "https://discordapp.com/api/webhooks/…", help: "URL to the webhook for the Discord channel." },
{ type: "checkbox", name: "notify_only_broken_pipelines" },
{ type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
]
end
private
def notify(message, opts)
client = Discordrb::Webhooks::Client.new(url: webhook)
client.execute do |builder|
builder.add_embed do |embed|
embed.author = Discordrb::Webhooks::EmbedAuthor.new(name: message.user_name, icon_url: message.user_avatar)
embed.description = (message.pretext + "\n" + Array.wrap(message.attachments).join("\n")).gsub(ATTACHMENT_REGEX, " \\k<entry> - \\k<name>\n")
end
end
rescue RestClient::Exception => error
log_error(error.message)
false
end
def custom_data(data)
super(data).merge(markdown: true)
end
end
end
# frozen_string_literal: true
require 'hangouts_chat'
module Integrations
class HangoutsChat < BaseChatNotification
include ActionView::Helpers::UrlHelper
def title
'Google Chat'
end
def description
'Send notifications from GitLab to a room in Google Chat.'
end
def self.to_param
'hangouts_chat'
end
def help
docs_link = link_to _('How do I set up a Google Chat webhook?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/hangouts_chat'), target: '_blank', rel: 'noopener noreferrer'
s_('Before enabling this integration, create a webhook for the room in Google Chat where you want to receive notifications from this project. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def event_field(event)
end
def default_channel_placeholder
end
def webhook_placeholder
'https://chat.googleapis.com/v1/spaces…'
end
def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push
pipeline wiki_page]
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}" },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{ type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
]
end
private
def notify(message, opts)
simple_text = parse_simple_text_message(message)
::HangoutsChat::Sender.new(webhook).simple(simple_text)
end
def parse_simple_text_message(message)
header = message.pretext
return header if message.attachments.empty?
attachment = message.attachments.first
title = format_attachment_title(attachment)
body = attachment[:text]
[header, title, body].compact.join("\n")
end
def format_attachment_title(attachment)
return attachment[:title] unless attachment[:title_link]
"<#{attachment[:title_link]}|#{attachment[:title]}>"
end
end
end
# frozen_string_literal: true
module Integrations
class Mattermost < BaseChatNotification
include SlackMattermost::Notifier
include ActionView::Helpers::UrlHelper
def title
s_('Mattermost notifications')
end
def description
s_('Send notifications about project events to Mattermost channels.')
end
def self.to_param
'mattermost'
end
def help
docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/mattermost'), target: '_blank', rel: 'noopener noreferrer'
s_('Send notifications about project events to Mattermost channels. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def default_channel_placeholder
'my-channel'
end
def webhook_placeholder
'http://mattermost.example.com/hooks/'
end
end
end
# frozen_string_literal: true
module Integrations
class MicrosoftTeams < BaseChatNotification
def title
'Microsoft Teams notifications'
end
def description
'Send notifications about project events to Microsoft Teams.'
end
def self.to_param
'microsoft_teams'
end
def help
'<p>Use this service to send notifications about events in GitLab projects to your Microsoft Teams channels. <a href="https://docs.gitlab.com/ee/user/project/integrations/microsoft_teams.html">How do I configure this integration?</a></p>'
end
def webhook_placeholder
'https://outlook.office.com/webhook/…'
end
def event_field(event)
end
def default_channel_placeholder
end
def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push
pipeline wiki_page]
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}" },
{ type: 'checkbox', name: 'notify_only_broken_pipelines', help: 'If selected, successful pipelines do not trigger a notification event.' },
{ type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
]
end
private
def notify(message, opts)
::MicrosoftTeams::Notifier.new(webhook).ping(
title: message.project_name,
summary: message.summary,
activity: message.activity,
attachments: message.attachments
)
end
def custom_data(data)
super(data).merge(markdown: true)
end
end
end
# frozen_string_literal: true
module Integrations
class Slack < BaseChatNotification
include SlackMattermost::Notifier
extend ::Gitlab::Utils::Override
SUPPORTED_EVENTS_FOR_USAGE_LOG = %w[
push issue confidential_issue merge_request note confidential_note
tag_push wiki_page deployment
].freeze
prop_accessor EVENT_CHANNEL['alert']
def title
'Slack notifications'
end
def description
'Send notifications about project events to Slack.'
end
def self.to_param
'slack'
end
def default_channel_placeholder
_('general, development')
end
def webhook_placeholder
'https://hooks.slack.com/services/…'
end
def supported_events
additional = []
additional << 'alert'
super + additional
end
def get_message(object_kind, data)
return Integrations::ChatMessage::AlertMessage.new(data) if object_kind == 'alert'
super
end
override :log_usage
def log_usage(event, user_id)
return unless user_id
return unless SUPPORTED_EVENTS_FOR_USAGE_LOG.include?(event)
key = "i_ecosystem_slack_service_#{event}_notification"
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(key, values: user_id)
end
end
end
# frozen_string_literal: true
module Integrations
class UnifyCircuit < BaseChatNotification
def title
'Unify Circuit'
end
def description
s_('Integrations|Send notifications about project events to Unify Circuit.')
end
def self.to_param
'unify_circuit'
end
def help
'This service sends notifications about projects events to a Unify Circuit conversation.<br />
To set up this service:
<ol>
<li><a href="https://www.circuit.com/unifyportalfaqdetail?articleId=164448">Set up an incoming webhook for your conversation</a>. All notifications will come to this conversation.</li>
<li>Paste the <strong>Webhook URL</strong> into the field below.</li>
<li>Select events below to enable notifications.</li>
</ol>'
end
def event_field(event)
end
def default_channel_placeholder
end
def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push
pipeline wiki_page]
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: "e.g. https://circuit.com/rest/v2/webhooks/incoming/…", required: true },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{ type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
]
end
private
def notify(message, opts)
response = Gitlab::HTTP.post(webhook, body: {
subject: message.project_name,
text: message.summary,
markdown: true
}.to_json)
response if response.success?
end
def custom_data(data)
super(data).merge(markdown: true)
end
end
end
# frozen_string_literal: true
module Integrations
class WebexTeams < BaseChatNotification
include ActionView::Helpers::UrlHelper
def title
s_("WebexTeamsService|Webex Teams")
end
def description
s_("WebexTeamsService|Send notifications about project events to Webex Teams.")
end
def self.to_param
'webex_teams'
end
def help
docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/webex_teams'), target: '_blank', rel: 'noopener noreferrer'
s_("WebexTeamsService|Send notifications about project events to a Webex Teams conversation. %{docs_link}") % { docs_link: docs_link.html_safe }
end
def event_field(event)
end
def default_channel_placeholder
end
def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push
pipeline wiki_page]
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: "https://api.ciscospark.com/v1/webhooks/incoming/...", required: true },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{ type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
]
end
private
def notify(message, opts)
header = { 'Content-Type' => 'application/json' }
response = Gitlab::HTTP.post(webhook, headers: header, body: { markdown: message.summary }.to_json)
response if response.success?
end
def custom_data(data)
super(data).merge(markdown: true)
end
end
end
......@@ -166,33 +166,33 @@ class Project < ApplicationRecord
has_one :confluence_service, class_name: 'Integrations::Confluence'
has_one :custom_issue_tracker_service, class_name: 'Integrations::CustomIssueTracker'
has_one :datadog_service, class_name: 'Integrations::Datadog'
has_one :discord_service, class_name: 'Integrations::Discord'
has_one :drone_ci_service, class_name: 'Integrations::DroneCi'
has_one :emails_on_push_service, class_name: 'Integrations::EmailsOnPush'
has_one :ewm_service, class_name: 'Integrations::Ewm'
has_one :external_wiki_service, class_name: 'Integrations::ExternalWiki'
has_one :flowdock_service, class_name: 'Integrations::Flowdock'
has_one :hangouts_chat_service, class_name: 'Integrations::HangoutsChat'
has_one :irker_service, class_name: 'Integrations::Irker'
has_one :jenkins_service, class_name: 'Integrations::Jenkins'
has_one :jira_service, class_name: 'Integrations::Jira'
has_one :mattermost_service, class_name: 'Integrations::Mattermost'
has_one :microsoft_teams_service, class_name: 'Integrations::MicrosoftTeams'
has_one :mock_ci_service, class_name: 'Integrations::MockCi'
has_one :packagist_service, class_name: 'Integrations::Packagist'
has_one :pipelines_email_service, class_name: 'Integrations::PipelinesEmail'
has_one :pivotaltracker_service, class_name: 'Integrations::Pivotaltracker'
has_one :redmine_service, class_name: 'Integrations::Redmine'
has_one :slack_service, class_name: 'Integrations::Slack'
has_one :teamcity_service, class_name: 'Integrations::Teamcity'
has_one :unify_circuit_service, class_name: 'Integrations::UnifyCircuit'
has_one :webex_teams_service, class_name: 'Integrations::WebexTeams'
has_one :youtrack_service, class_name: 'Integrations::Youtrack'
has_one :discord_service
has_one :mattermost_slash_commands_service
has_one :mattermost_service
has_one :slack_slash_commands_service
has_one :slack_service
has_one :pushover_service
has_one :prometheus_service, inverse_of: :project
has_one :mock_monitoring_service
has_one :microsoft_teams_service
has_one :hangouts_chat_service
has_one :unify_circuit_service
has_one :webex_teams_service
has_one :root_of_fork_network,
foreign_key: 'root_project_id',
......
# frozen_string_literal: true
# Base class for Chat notifications services
# This class is not meant to be used directly, but only to inherit from.
class ChatNotificationService < Integration
include ChatMessage
include NotificationBranchSelection
SUPPORTED_EVENTS = %w[
push issue confidential_issue merge_request note confidential_note
tag_push pipeline wiki_page deployment
].freeze
SUPPORTED_EVENTS_FOR_LABEL_FILTER = %w[issue confidential_issue merge_request note confidential_note].freeze
EVENT_CHANNEL = proc { |event| "#{event}_channel" }
LABEL_NOTIFICATION_BEHAVIOURS = [
MATCH_ANY_LABEL = 'match_any',
MATCH_ALL_LABELS = 'match_all'
].freeze
default_value_for :category, 'chat'
prop_accessor :webhook, :username, :channel, :branches_to_be_notified, :labels_to_be_notified, :labels_to_be_notified_behavior
# Custom serialized properties initialization
prop_accessor(*SUPPORTED_EVENTS.map { |event| EVENT_CHANNEL[event] })
boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch
validates :webhook, presence: true, public_url: true, if: :activated?
validates :labels_to_be_notified_behavior, inclusion: { in: LABEL_NOTIFICATION_BEHAVIOURS }, allow_blank: true
def initialize_properties
if properties.nil?
self.properties = {}
self.notify_only_broken_pipelines = true
self.branches_to_be_notified = "default"
self.labels_to_be_notified_behavior = MATCH_ANY_LABEL
elsif !self.notify_only_default_branch.nil?
# In older versions, there was only a boolean property named
# `notify_only_default_branch`. Now we have a string property named
# `branches_to_be_notified`. Instead of doing a background migration, we
# opted to set a value for the new property based on the old one, if
# users hasn't specified one already. When users edit the service and
# selects a value for this new property, it will override everything.
self.branches_to_be_notified ||= notify_only_default_branch? ? "default" : "all"
end
end
def confidential_issue_channel
properties['confidential_issue_channel'].presence || properties['issue_channel']
end
def confidential_note_channel
properties['confidential_note_channel'].presence || properties['note_channel']
end
def self.supported_events
SUPPORTED_EVENTS
end
def fields
default_fields + build_event_channels
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}", required: true }.freeze,
{ type: 'text', name: 'username', placeholder: 'GitLab-integration' }.freeze,
{ type: 'checkbox', name: 'notify_only_broken_pipelines', help: 'Do not send notifications for successful pipelines.' }.freeze,
{ type: 'select', name: 'branches_to_be_notified', choices: branch_choices }.freeze,
{
type: 'text',
name: 'labels_to_be_notified',
placeholder: '~backend,~frontend',
help: 'Send notifications for issue, merge request, and comment events with the listed labels only. Leave blank to receive notifications for all events.'
}.freeze,
{
type: 'select',
name: 'labels_to_be_notified_behavior',
choices: [
['Match any of the labels', MATCH_ANY_LABEL],
['Match all of the labels', MATCH_ALL_LABELS]
]
}.freeze
].freeze
end
def execute(data)
return unless supported_events.include?(data[:object_kind])
return unless webhook.present?
object_kind = data[:object_kind]
data = custom_data(data)
return unless notify_label?(data)
# WebHook events often have an 'update' event that follows a 'open' or
# 'close' action. Ignore update events for now to prevent duplicate
# messages from arriving.
message = get_message(object_kind, data)
return false unless message
event_type = data[:event_type] || object_kind
channel_names = get_channel_field(event_type).presence || channel.presence
channels = channel_names&.split(',')&.map(&:strip)
opts = {}
opts[:channel] = channels if channels.present?
opts[:username] = username if username
if notify(message, opts)
log_usage(event_type, user_id_from_hook_data(data))
return true
end
false
end
def event_channel_names
supported_events.map { |event| event_channel_name(event) }
end
def event_field(event)
fields.find { |field| field[:name] == event_channel_name(event) }
end
def global_fields
fields.reject { |field| field[:name].end_with?('channel') }
end
def default_channel_placeholder
raise NotImplementedError
end
private
def log_usage(_, _)
# Implement in child class
end
def labels_to_be_notified_list
return [] if labels_to_be_notified.nil?
labels_to_be_notified.delete('~').split(',').map(&:strip)
end
def notify_label?(data)
return true unless SUPPORTED_EVENTS_FOR_LABEL_FILTER.include?(data[:object_kind]) && labels_to_be_notified.present?
labels = data[:labels] || data.dig(:issue, :labels) || data.dig(:merge_request, :labels) || data.dig(:object_attributes, :labels)
return false if labels.blank?
matching_labels = labels_to_be_notified_list & labels.pluck(:title)
if labels_to_be_notified_behavior == MATCH_ALL_LABELS
labels_to_be_notified_list.difference(matching_labels).empty?
else
matching_labels.any?
end
end
def user_id_from_hook_data(data)
data.dig(:user, :id) || data[:user_id]
end
# every notifier must implement this independently
def notify(message, opts)
raise NotImplementedError
end
def custom_data(data)
data.merge(project_url: project_url, project_name: project_name).with_indifferent_access
end
def get_message(object_kind, data)
case object_kind
when "push", "tag_push"
Integrations::ChatMessage::PushMessage.new(data) if notify_for_ref?(data)
when "issue"
Integrations::ChatMessage::IssueMessage.new(data) unless update?(data)
when "merge_request"
Integrations::ChatMessage::MergeMessage.new(data) unless update?(data)
when "note"
Integrations::ChatMessage::NoteMessage.new(data)
when "pipeline"
Integrations::ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
when "wiki_page"
Integrations::ChatMessage::WikiPageMessage.new(data)
when "deployment"
Integrations::ChatMessage::DeploymentMessage.new(data)
end
end
def get_channel_field(event)
field_name = event_channel_name(event)
self.public_send(field_name) # rubocop:disable GitlabSecurity/PublicSend
end
def build_event_channels
supported_events.reduce([]) do |channels, event|
channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel_placeholder }
end
end
def event_channel_name(event)
EVENT_CHANNEL[event]
end
def project_name
project.full_name
end
def project_url
project.web_url
end
def update?(data)
data[:object_attributes][:action] == 'update'
end
def should_pipeline_be_notified?(data)
notify_for_ref?(data) && notify_for_pipeline?(data)
end
def notify_for_ref?(data)
return true if data[:object_kind] == 'tag_push'
return true if data.dig(:object_attributes, :tag)
notify_for_branch?(data)
end
def notify_for_pipeline?(data)
case data[:object_attributes][:status]
when 'success'
!notify_only_broken_pipelines?
when 'failed'
true
else
false
end
end
end
# frozen_string_literal: true
require "discordrb/webhooks"
class DiscordService < ChatNotificationService
include ActionView::Helpers::UrlHelper
ATTACHMENT_REGEX = /: (?<entry>.*?)\n - (?<name>.*)\n*/.freeze
def title
s_("DiscordService|Discord Notifications")
end
def description
s_("DiscordService|Send notifications about project events to a Discord channel.")
end
def self.to_param
"discord"
end
def help
docs_link = link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/discord_notifications'), target: '_blank', rel: 'noopener noreferrer'
s_('Send notifications about project events to a Discord channel. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def event_field(event)
# No-op.
end
def default_channel_placeholder
# No-op.
end
def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page]
end
def default_fields
[
{ type: "text", name: "webhook", placeholder: "https://discordapp.com/api/webhooks/…", help: "URL to the webhook for the Discord channel." },
{ type: "checkbox", name: "notify_only_broken_pipelines" },
{ type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
]
end
private
def notify(message, opts)
client = Discordrb::Webhooks::Client.new(url: webhook)
client.execute do |builder|
builder.add_embed do |embed|
embed.author = Discordrb::Webhooks::EmbedAuthor.new(name: message.user_name, icon_url: message.user_avatar)
embed.description = (message.pretext + "\n" + Array.wrap(message.attachments).join("\n")).gsub(ATTACHMENT_REGEX, " \\k<entry> - \\k<name>\n")
end
end
rescue RestClient::Exception => error
log_error(error.message)
false
end
def custom_data(data)
super(data).merge(markdown: true)
end
end
# frozen_string_literal: true
require 'hangouts_chat'
class HangoutsChatService < ChatNotificationService
include ActionView::Helpers::UrlHelper
def title
'Google Chat'
end
def description
'Send notifications from GitLab to a room in Google Chat.'
end
def self.to_param
'hangouts_chat'
end
def help
docs_link = link_to _('How do I set up a Google Chat webhook?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/hangouts_chat'), target: '_blank', rel: 'noopener noreferrer'
s_('Before enabling this integration, create a webhook for the room in Google Chat where you want to receive notifications from this project. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def event_field(event)
end
def default_channel_placeholder
end
def webhook_placeholder
'https://chat.googleapis.com/v1/spaces…'
end
def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push
pipeline wiki_page]
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}" },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{ type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
]
end
private
def notify(message, opts)
simple_text = parse_simple_text_message(message)
HangoutsChat::Sender.new(webhook).simple(simple_text)
end
def parse_simple_text_message(message)
header = message.pretext
return header if message.attachments.empty?
attachment = message.attachments.first
title = format_attachment_title(attachment)
body = attachment[:text]
[header, title, body].compact.join("\n")
end
def format_attachment_title(attachment)
return attachment[:title] unless attachment[:title_link]
"<#{attachment[:title_link]}|#{attachment[:title]}>"
end
end
# frozen_string_literal: true
class MattermostService < ChatNotificationService
include SlackMattermost::Notifier
include ActionView::Helpers::UrlHelper
def title
s_('Mattermost notifications')
end
def description
s_('Send notifications about project events to Mattermost channels.')
end
def self.to_param
'mattermost'
end
def help
docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/mattermost'), target: '_blank', rel: 'noopener noreferrer'
s_('Send notifications about project events to Mattermost channels. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def default_channel_placeholder
'my-channel'
end
def webhook_placeholder
'http://mattermost.example.com/hooks/'
end
end
......@@ -22,17 +22,17 @@ class MattermostSlashCommandsService < SlashCommandsService
end
def configure(user, params)
token = Mattermost::Command.new(user)
token = ::Mattermost::Command.new(user)
.create(command(params))
update(active: true, token: token) if token
rescue Mattermost::Error => e
rescue ::Mattermost::Error => e
[false, e.message]
end
def list_teams(current_user)
[Mattermost::Team.new(current_user).all, nil]
rescue Mattermost::Error => e
[::Mattermost::Team.new(current_user).all, nil]
rescue ::Mattermost::Error => e
[[], e.message]
end
......
# frozen_string_literal: true
class MicrosoftTeamsService < ChatNotificationService
def title
'Microsoft Teams notifications'
end
def description
'Send notifications about project events to Microsoft Teams.'
end
def self.to_param
'microsoft_teams'
end
def help
'<p>Use this service to send notifications about events in GitLab projects to your Microsoft Teams channels. <a href="https://docs.gitlab.com/ee/user/project/integrations/microsoft_teams.html">How do I configure this integration?</a></p>'
end
def webhook_placeholder
'https://outlook.office.com/webhook/…'
end
def event_field(event)
end
def default_channel_placeholder
end
def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push
pipeline wiki_page]
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}" },
{ type: 'checkbox', name: 'notify_only_broken_pipelines', help: 'If selected, successful pipelines do not trigger a notification event.' },
{ type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
]
end
private
def notify(message, opts)
MicrosoftTeams::Notifier.new(webhook).ping(
title: message.project_name,
summary: message.summary,
activity: message.activity,
attachments: message.attachments
)
end
def custom_data(data)
super(data).merge(markdown: true)
end
end
......@@ -6,7 +6,7 @@ module SlackMattermost
def notify(message, opts)
# See https://gitlab.com/gitlab-org/slack-notifier/#custom-http-client
notifier = Slack::Messenger.new(webhook, opts.merge(http_client: HTTPClient))
notifier = ::Slack::Messenger.new(webhook, opts.merge(http_client: HTTPClient))
notifier.ping(
message.pretext,
attachments: message.attachments,
......
# frozen_string_literal: true
class SlackService < ChatNotificationService
include SlackMattermost::Notifier
extend ::Gitlab::Utils::Override
SUPPORTED_EVENTS_FOR_USAGE_LOG = %w[
push issue confidential_issue merge_request note confidential_note
tag_push wiki_page deployment
].freeze
prop_accessor EVENT_CHANNEL['alert']
def title
'Slack notifications'
end
def description
'Send notifications about project events to Slack.'
end
def self.to_param
'slack'
end
def default_channel_placeholder
_('general, development')
end
def webhook_placeholder
'https://hooks.slack.com/services/…'
end
def supported_events
additional = []
additional << 'alert'
super + additional
end
def get_message(object_kind, data)
return Integrations::ChatMessage::AlertMessage.new(data) if object_kind == 'alert'
super
end
override :log_usage
def log_usage(event, user_id)
return unless user_id
return unless SUPPORTED_EVENTS_FOR_USAGE_LOG.include?(event)
key = "i_ecosystem_slack_service_#{event}_notification"
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(key, values: user_id)
end
end
......@@ -29,6 +29,6 @@ class SlackSlashCommandsService < SlashCommandsService
private
def format(text)
Slack::Messenger::Util::LinkFormatter.format(text) if text
::Slack::Messenger::Util::LinkFormatter.format(text) if text
end
end
# frozen_string_literal: true
class UnifyCircuitService < ChatNotificationService
def title
'Unify Circuit'
end
def description
s_('Integrations|Send notifications about project events to Unify Circuit.')
end
def self.to_param
'unify_circuit'
end
def help
'This service sends notifications about projects events to a Unify Circuit conversation.<br />
To set up this service:
<ol>
<li><a href="https://www.circuit.com/unifyportalfaqdetail?articleId=164448">Set up an incoming webhook for your conversation</a>. All notifications will come to this conversation.</li>
<li>Paste the <strong>Webhook URL</strong> into the field below.</li>
<li>Select events below to enable notifications.</li>
</ol>'
end
def event_field(event)
end
def default_channel_placeholder
end
def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push
pipeline wiki_page]
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: "e.g. https://circuit.com/rest/v2/webhooks/incoming/…", required: true },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{ type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
]
end
private
def notify(message, opts)
response = Gitlab::HTTP.post(webhook, body: {
subject: message.project_name,
text: message.summary,
markdown: true
}.to_json)
response if response.success?
end
def custom_data(data)
super(data).merge(markdown: true)
end
end
# frozen_string_literal: true
class WebexTeamsService < ChatNotificationService
include ActionView::Helpers::UrlHelper
def title
s_("WebexTeamsService|Webex Teams")
end
def description
s_("WebexTeamsService|Send notifications about project events to Webex Teams.")
end
def self.to_param
'webex_teams'
end
def help
docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/webex_teams'), target: '_blank', rel: 'noopener noreferrer'
s_("WebexTeamsService|Send notifications about project events to a Webex Teams conversation. %{docs_link}") % { docs_link: docs_link.html_safe }
end
def event_field(event)
end
def default_channel_placeholder
end
def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push
pipeline wiki_page]
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: "https://api.ciscospark.com/v1/webhooks/incoming/...", required: true },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{ type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
]
end
private
def notify(message, opts)
header = { 'Content-Type' => 'application/json' }
response = Gitlab::HTTP.post(webhook, headers: header, body: { markdown: message.summary }.to_json)
response if response.success?
end
def custom_data(data)
super(data).merge(markdown: true)
end
end
......@@ -28,7 +28,7 @@ module Groups
@group.name ||= @group.path.dup
if create_chat_team?
response = Mattermost::CreateTeamService.new(@group, current_user).execute
response = ::Mattermost::CreateTeamService.new(@group, current_user).execute
return @group if @group.errors.any?
@group.build_chat_team(name: response['name'], team_id: response['id'])
......
......@@ -9,8 +9,8 @@ module Mattermost
def execute
# The user that creates the team will be Team Admin
Mattermost::Team.new(current_user).create(@group.mattermost_team_params)
rescue Mattermost::ClientError => e
::Mattermost::Team.new(current_user).create(@group.mattermost_team_params)
rescue ::Mattermost::ClientError => e
@group.errors.add(:mattermost_team, e.message)
end
end
......
......@@ -783,29 +783,29 @@ module API
::Integrations::Confluence,
::Integrations::CustomIssueTracker,
::Integrations::Datadog,
::Integrations::Discord,
::Integrations::DroneCi,
::Integrations::EmailsOnPush,
::Integrations::Ewm,
::Integrations::ExternalWiki,
::Integrations::Flowdock,
::Integrations::HangoutsChat,
::Integrations::Irker,
::Integrations::Jenkins,
::Integrations::Jira,
::Integrations::Mattermost,
::Integrations::MicrosoftTeams,
::Integrations::Packagist,
::Integrations::PipelinesEmail,
::Integrations::Pivotaltracker,
::Integrations::Redmine,
::Integrations::Slack,
::Integrations::Teamcity,
::Integrations::Youtrack,
::DiscordService,
::HangoutsChatService,
::MattermostSlashCommandsService,
::SlackSlashCommandsService,
::PrometheusService,
::PushoverService,
::SlackService,
::MattermostService,
::MicrosoftTeamsService
::PushoverService
]
end
......
......@@ -5,8 +5,9 @@ module Gitlab
class StiType < ActiveRecord::Type::String
NAMESPACED_INTEGRATIONS = Set.new(%w(
Asana Assembla Bamboo Bugzilla Buildkite Campfire Confluence CustomIssueTracker Datadog
DroneCi EmailsOnPush Ewm ExternalWiki Flowdock IssueTracker Irker Jenkins Jira MockCi Packagist
PipelinesEmail Pivotaltracker Redmine Teamcity Youtrack
Discord DroneCi EmailsOnPush Ewm ExternalWiki Flowdock HangoutsChat IssueTracker Irker
Jenkins Jira Mattermost MicrosoftTeams MockCi Packagist PipelinesEmail Pivotaltracker
Redmine Slack Teamcity UnifyCircuit Youtrack WebexTeams
)).freeze
def cast(value)
......
......@@ -63,7 +63,7 @@ module Gitlab
# Convert Markdown to slacks format
def format(string)
Slack::Messenger::Util::LinkFormatter.format(string)
::Slack::Messenger::Util::LinkFormatter.format(string)
end
def resource_url
......
# frozen_string_literal: true
module Mattermost
ClientError = Class.new(Mattermost::Error)
ClientError = Class.new(::Mattermost::Error)
class Client
attr_reader :user
......@@ -11,7 +11,7 @@ module Mattermost
end
def with_session(&blk)
Mattermost::Session.new(user).with_session(&blk)
::Mattermost::Session.new(user).with_session(&blk)
end
private
......@@ -52,12 +52,12 @@ module Mattermost
json_response = Gitlab::Json.parse(response.body, legacy_mode: true)
unless response.success?
raise Mattermost::ClientError, json_response['message'] || 'Undefined error'
raise ::Mattermost::ClientError, json_response['message'] || 'Undefined error'
end
json_response
rescue JSON::JSONError
raise Mattermost::ClientError, 'Cannot parse response'
raise ::Mattermost::ClientError, 'Cannot parse response'
end
end
end
# frozen_string_literal: true
module Mattermost
class NoSessionError < Mattermost::Error
class NoSessionError < ::Mattermost::Error
def message
'No session could be set up, is Mattermost configured with Single Sign On?'
end
end
ConnectionError = Class.new(Mattermost::Error)
ConnectionError = Class.new(::Mattermost::Error)
# This class' prime objective is to obtain a session token on a Mattermost
# instance with SSO configured where this GitLab instance is the provider.
......@@ -42,7 +42,7 @@ module Mattermost
yield self
rescue Errno::ECONNREFUSED => e
Gitlab::AppLogger.error(e.message + "\n" + e.backtrace.join("\n"))
raise Mattermost::NoSessionError
raise ::Mattermost::NoSessionError
ensure
destroy
end
......@@ -100,11 +100,11 @@ module Mattermost
end
def create
raise Mattermost::NoSessionError unless oauth_uri
raise Mattermost::NoSessionError unless token_uri
raise ::Mattermost::NoSessionError unless oauth_uri
raise ::Mattermost::NoSessionError unless token_uri
@token = request_token
raise Mattermost::NoSessionError unless @token
raise ::Mattermost::NoSessionError unless @token
@headers = {
Authorization: "Bearer #{@token}"
......@@ -174,9 +174,9 @@ module Mattermost
def handle_exceptions
yield
rescue Gitlab::HTTP::Error => e
raise Mattermost::ConnectionError, e.message
raise ::Mattermost::ConnectionError, e.message
rescue Errno::ECONNREFUSED => e
raise Mattermost::ConnectionError, e.message
raise ::Mattermost::ConnectionError, e.message
end
def parse_cookie(response)
......
......@@ -32,7 +32,7 @@ module MicrosoftTeams
result['title'] = title
result['summary'] = summary
result['sections'] << MicrosoftTeams::Activity.new(**activity).prepare
result['sections'] << ::MicrosoftTeams::Activity.new(**activity).prepare
unless attachments.blank?
result['sections'] << { text: attachments }
......
......@@ -53,7 +53,7 @@ RSpec.describe Projects::MattermostsController do
context 'the request is succesull' do
before do
allow_next_instance_of(Mattermost::Command) do |instance|
allow_next_instance_of(::Mattermost::Command) do |instance|
allow(instance).to receive(:create).and_return('token')
end
end
......
......@@ -47,7 +47,7 @@ RSpec.describe Projects::ServicesController do
let(:service) { project.create_microsoft_teams_service(webhook: 'http://webhook.com') }
it 'returns success' do
allow_any_instance_of(MicrosoftTeams::Notifier).to receive(:ping).and_return(true)
allow_any_instance_of(::MicrosoftTeams::Notifier).to receive(:ping).and_return(true)
put :test, params: project_params
......@@ -145,7 +145,7 @@ RSpec.describe Projects::ServicesController do
end
it 'returns an error response when a network exception is raised' do
expect_next(SlackService).to receive(:test).and_raise(Errno::ECONNREFUSED)
expect_next(Integrations::Slack).to receive(:test).and_raise(Errno::ECONNREFUSED)
put :test, params: project_params
......
......@@ -160,7 +160,7 @@ FactoryBot.define do
password { 'my-secret-password' }
end
factory :slack_service do
factory :slack_service, class: 'Integrations::Slack' do
project
active { true }
webhook { 'https://slack.service.url' }
......
......@@ -20,7 +20,7 @@ RSpec.describe 'User activates Slack notifications', :js do
end
context 'when service is already configured' do
let(:service) { SlackService.new }
let(:service) { Integrations::Slack.new }
let(:project) { create(:project, slack_service: service) }
before do
......
......@@ -14,13 +14,13 @@ RSpec.describe Mattermost::Client do
it 'yields an error on malformed JSON' do
bad_json = Struct::Request.new("I'm not json", true)
expect { subject.send(:json_response, bad_json) }.to raise_error(Mattermost::ClientError)
expect { subject.send(:json_response, bad_json) }.to raise_error(::Mattermost::ClientError)
end
it 'shows a client error if the request was unsuccessful' do
bad_request = Struct::Request.new("true", false)
expect { subject.send(:json_response, bad_request) }.to raise_error(Mattermost::ClientError)
expect { subject.send(:json_response, bad_request) }.to raise_error(::Mattermost::ClientError)
end
end
end
......@@ -6,10 +6,10 @@ RSpec.describe Mattermost::Command do
let(:params) { { 'token' => 'token', team_id: 'abc' } }
before do
session = Mattermost::Session.new(nil)
session = ::Mattermost::Session.new(nil)
session.base_uri = 'http://mattermost.example.com'
allow_any_instance_of(Mattermost::Client).to receive(:with_session)
allow_any_instance_of(::Mattermost::Client).to receive(:with_session)
.and_yield(session)
end
......@@ -57,7 +57,7 @@ RSpec.describe Mattermost::Command do
end
it 'raises an error with message' do
expect { subject }.to raise_error(Mattermost::Error, 'This trigger word is already in use. Please choose another word.')
expect { subject }.to raise_error(::Mattermost::Error, 'This trigger word is already in use. Please choose another word.')
end
end
end
......
......@@ -33,7 +33,7 @@ RSpec.describe Mattermost::Session, type: :request do
context 'without oauth uri' do
it 'makes a request to the oauth uri' do
expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
expect { subject.with_session }.to raise_error(::Mattermost::NoSessionError)
end
end
......@@ -49,7 +49,7 @@ RSpec.describe Mattermost::Session, type: :request do
it 'can not create a session' do
expect do
subject.with_session
end.to raise_error(Mattermost::NoSessionError)
end.to raise_error(::Mattermost::NoSessionError)
end
end
......@@ -113,13 +113,13 @@ RSpec.describe Mattermost::Session, type: :request do
expect_to_cancel_exclusive_lease(lease_key, 'uuid')
# Cannot set up a session, but we should still cancel the lease
expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
expect { subject.with_session }.to raise_error(::Mattermost::NoSessionError)
end
it 'returns a NoSessionError error without lease' do
stub_exclusive_lease_taken(lease_key)
expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
expect { subject.with_session }.to raise_error(::Mattermost::NoSessionError)
end
end
end
......
......@@ -7,7 +7,7 @@ RSpec.describe Mattermost::Team do
session = Mattermost::Session.new(nil)
session.base_uri = 'http://mattermost.example.com'
allow_any_instance_of(Mattermost::Client).to receive(:with_session)
allow_any_instance_of(::Mattermost::Client).to receive(:with_session)
.and_yield(session)
end
......@@ -65,7 +65,7 @@ RSpec.describe Mattermost::Team do
end
it 'raises an error with message' do
expect { subject }.to raise_error(Mattermost::Error, 'Cannot list teams.')
expect { subject }.to raise_error(::Mattermost::Error, 'Cannot list teams.')
end
end
end
......@@ -123,7 +123,7 @@ RSpec.describe Mattermost::Team do
end
it 'raises an error with message' do
expect { subject }.to raise_error(Mattermost::Error, 'A team with that name already exists')
expect { subject }.to raise_error(::Mattermost::Error, 'A team with that name already exists')
end
end
end
......@@ -169,7 +169,7 @@ RSpec.describe Mattermost::Team do
end
it 'raises an error with message' do
expect { subject }.to raise_error(Mattermost::Error, "We couldn't find the existing team")
expect { subject }.to raise_error(::Mattermost::Error, "We couldn't find the existing team")
end
end
end
......
......@@ -672,7 +672,7 @@ RSpec.describe Integration do
expect(described_class.service_name_to_model('asana')).to eq(Integrations::Asana)
# TODO We can remove this test when all models have been namespaced:
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60968#note_570994955
expect(described_class.service_name_to_model('webex_teams')).to eq(WebexTeamsService)
expect(described_class.service_name_to_model('pushover')).to eq(PushoverService)
end
it 'raises an error if service name is invalid' do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe ChatNotificationService do
RSpec.describe Integrations::BaseChatNotification do
describe 'Associations' do
before do
allow(subject).to receive(:activated?).and_return(true)
......
......@@ -2,8 +2,8 @@
require "spec_helper"
RSpec.describe DiscordService do
it_behaves_like "chat service", "Discord notifications" do
RSpec.describe Integrations::Discord do
it_behaves_like "chat integration", "Discord notifications" do
let(:client) { Discordrb::Webhooks::Client }
let(:client_arguments) { { url: webhook_url } }
let(:payload) do
......
......@@ -2,8 +2,8 @@
require "spec_helper"
RSpec.describe HangoutsChatService do
it_behaves_like "chat service", "Hangouts Chat" do
RSpec.describe Integrations::HangoutsChat do
it_behaves_like "chat integration", "Hangouts Chat" do
let(:client) { HangoutsChat::Sender }
let(:client_arguments) { webhook_url }
let(:payload) do
......
......@@ -2,6 +2,6 @@
require 'spec_helper'
RSpec.describe MattermostService do
RSpec.describe Integrations::Mattermost do
it_behaves_like "slack or mattermost notifications", "Mattermost"
end
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe MicrosoftTeamsService do
RSpec.describe Integrations::MicrosoftTeams do
let(:chat_service) { described_class.new }
let(:webhook_url) { 'https://example.gitlab.com/' }
......@@ -64,7 +64,7 @@ RSpec.describe MicrosoftTeamsService do
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)
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
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe SlackService do
RSpec.describe Integrations::Slack do
it_behaves_like "slack or mattermost notifications", 'Slack'
describe '#execute' do
......
......@@ -2,8 +2,8 @@
require "spec_helper"
RSpec.describe UnifyCircuitService do
it_behaves_like "chat service", "Unify Circuit" do
RSpec.describe Integrations::UnifyCircuit do
it_behaves_like "chat integration", "Unify Circuit" do
let(:client_arguments) { webhook_url }
let(:payload) do
{
......
......@@ -2,8 +2,8 @@
require "spec_helper"
RSpec.describe WebexTeamsService do
it_behaves_like "chat service", "Webex Teams" do
RSpec.describe Integrations::WebexTeams do
it_behaves_like "chat integration", "Webex Teams" do
let(:client_arguments) { webhook_url }
let(:payload) do
{
......
......@@ -11,10 +11,10 @@ RSpec.describe MattermostSlashCommandsService do
let(:user) { create(:user) }
before do
session = Mattermost::Session.new(nil)
session = ::Mattermost::Session.new(nil)
session.base_uri = 'http://mattermost.example.com'
allow_any_instance_of(Mattermost::Client).to receive(:with_session)
allow_any_instance_of(::Mattermost::Client).to receive(:with_session)
.and_yield(session)
end
......
......@@ -5250,7 +5250,7 @@ RSpec.describe Project, factory_default: :keep do
it 'executes services with the specified scope' do
data = 'any data'
expect_next_found_instance_of(SlackService) do |instance|
expect_next_found_instance_of(Integrations::Slack) do |instance|
expect(instance).to receive(:async_execute).with(data).once
end
......@@ -5258,7 +5258,7 @@ RSpec.describe Project, factory_default: :keep do
end
it 'does not execute services that don\'t match the specified scope' do
expect(SlackService).not_to receive(:allocate).and_wrap_original do |method|
expect(Integrations::Slack).not_to receive(:allocate).and_wrap_original do |method|
method.call.tap do |instance|
expect(instance).not_to receive(:async_execute)
end
......
......@@ -140,7 +140,7 @@ RSpec.describe Groups::CreateService, '#execute' do
end
it 'create the chat team with the group' do
allow_any_instance_of(Mattermost::Team).to receive(:create)
allow_any_instance_of(::Mattermost::Team).to receive(:create)
.and_return({ 'name' => 'tanuki', 'id' => 'lskdjfwlekfjsdifjj' })
expect { subject }.to change { ChatTeam.count }.from(0).to(1)
......
......@@ -41,7 +41,7 @@ RSpec.describe Groups::DestroyService do
let!(:chat_team) { create(:chat_team, namespace: group) }
it 'destroys the team too' do
expect_next_instance_of(Mattermost::Team) do |instance|
expect_next_instance_of(::Mattermost::Team) do |instance|
expect(instance).to receive(:destroy)
end
......
# frozen_string_literal: true
RSpec.shared_examples "chat service" do |service_name|
RSpec.shared_examples "chat integration" do |integration_name|
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
context "when integration is active" do
before do
subject.active = true
end
......@@ -16,7 +16,7 @@ RSpec.shared_examples "chat service" do |service_name|
it_behaves_like "issue tracker service URL attribute", :webhook
end
context "when service is inactive" do
context "when integration is inactive" do
before do
subject.active = false
end
......@@ -47,12 +47,13 @@ RSpec.shared_examples "chat service" do |service_name|
WebMock.stub_request(:post, webhook_url)
end
shared_examples "triggered #{service_name} service" do |branches_to_be_notified: nil|
shared_examples "triggered #{integration_name} integration" do |branches_to_be_notified: nil|
before do
subject.branches_to_be_notified = branches_to_be_notified if branches_to_be_notified
end
it "calls #{service_name} API" do
it "calls #{integration_name} API" do
# binding.pry
result = subject.execute(sample_data)
expect(result).to be(true)
......@@ -63,12 +64,12 @@ RSpec.shared_examples "chat service" do |service_name|
end
end
shared_examples "untriggered #{service_name} service" do |branches_to_be_notified: nil|
shared_examples "untriggered #{integration_name} integration" do |branches_to_be_notified: nil|
before do
subject.branches_to_be_notified = branches_to_be_notified if branches_to_be_notified
end
it "does not call #{service_name} API" do
it "does not call #{integration_name} API" do
result = subject.execute(sample_data)
expect(result).to be(false)
......@@ -81,7 +82,7 @@ RSpec.shared_examples "chat service" do |service_name|
Gitlab::DataBuilder::Push.build_sample(project, user)
end
it_behaves_like "triggered #{service_name} service"
it_behaves_like "triggered #{integration_name} integration"
it "specifies the webhook when it is configured", if: defined?(client) do
expect(client).to receive(:new).with(client_arguments).and_return(double(:chat_service).as_null_object)
......@@ -95,19 +96,19 @@ RSpec.shared_examples "chat service" do |service_name|
end
context "when only default branch are to be notified" do
it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "default"
it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "default"
end
context "when only protected branches are to be notified" do
it_behaves_like "untriggered #{service_name} service", branches_to_be_notified: "protected"
it_behaves_like "untriggered #{integration_name} integration", branches_to_be_notified: "protected"
end
context "when default and protected branches are to be notified" do
it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "default_and_protected"
it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "default_and_protected"
end
context "when all branches are to be notified" do
it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "all"
it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "all"
end
end
......@@ -121,19 +122,19 @@ RSpec.shared_examples "chat service" do |service_name|
end
context "when only default branch are to be notified" do
it_behaves_like "untriggered #{service_name} service", branches_to_be_notified: "default"
it_behaves_like "untriggered #{integration_name} integration", branches_to_be_notified: "default"
end
context "when only protected branches are to be notified" do
it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "protected"
it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "protected"
end
context "when default and protected branches are to be notified" do
it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "default_and_protected"
it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "default_and_protected"
end
context "when all branches are to be notified" do
it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "all"
it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "all"
end
end
......@@ -143,19 +144,19 @@ RSpec.shared_examples "chat service" do |service_name|
end
context "when only default branch are to be notified" do
it_behaves_like "untriggered #{service_name} service", branches_to_be_notified: "default"
it_behaves_like "untriggered #{integration_name} integration", branches_to_be_notified: "default"
end
context "when only protected branches are to be notified" do
it_behaves_like "untriggered #{service_name} service", branches_to_be_notified: "protected"
it_behaves_like "untriggered #{integration_name} integration", branches_to_be_notified: "protected"
end
context "when default and protected branches are to be notified" do
it_behaves_like "untriggered #{service_name} service", branches_to_be_notified: "default_and_protected"
it_behaves_like "untriggered #{integration_name} integration", branches_to_be_notified: "default_and_protected"
end
context "when all branches are to be notified" do
it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "all"
it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "all"
end
end
end
......@@ -168,7 +169,7 @@ RSpec.shared_examples "chat service" do |service_name|
service.hook_data(issue, "open")
end
it_behaves_like "triggered #{service_name} service"
it_behaves_like "triggered #{integration_name} integration"
end
context "with merge events" do
......@@ -191,7 +192,7 @@ RSpec.shared_examples "chat service" do |service_name|
project.add_developer(user)
end
it_behaves_like "triggered #{service_name} service"
it_behaves_like "triggered #{integration_name} integration"
end
context "with wiki page events" do
......@@ -207,7 +208,7 @@ RSpec.shared_examples "chat service" do |service_name|
let(:wiki_page) { create(:wiki_page, wiki: project.wiki, **opts) }
let(:sample_data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, "create") }
it_behaves_like "triggered #{service_name} service"
it_behaves_like "triggered #{integration_name} integration"
end
context "with note events" do
......@@ -222,7 +223,7 @@ RSpec.shared_examples "chat service" do |service_name|
note: "a comment on a commit")
end
it_behaves_like "triggered #{service_name} service"
it_behaves_like "triggered #{integration_name} integration"
end
context "with merge request comment" do
......@@ -230,7 +231,7 @@ RSpec.shared_examples "chat service" do |service_name|
create(:note_on_merge_request, project: project, note: "merge request note")
end
it_behaves_like "triggered #{service_name} service"
it_behaves_like "triggered #{integration_name} integration"
end
context "with issue comment" do
......@@ -238,7 +239,7 @@ RSpec.shared_examples "chat service" do |service_name|
create(:note_on_issue, project: project, note: "issue note")
end
it_behaves_like "triggered #{service_name} service"
it_behaves_like "triggered #{integration_name} integration"
end
context "with snippet comment" do
......@@ -246,7 +247,7 @@ RSpec.shared_examples "chat service" do |service_name|
create(:note_on_project_snippet, project: project, note: "snippet note")
end
it_behaves_like "triggered #{service_name} service"
it_behaves_like "triggered #{integration_name} integration"
end
end
......@@ -262,14 +263,14 @@ RSpec.shared_examples "chat service" do |service_name|
context "with failed pipeline" do
let(:status) { "failed" }
it_behaves_like "triggered #{service_name} service"
it_behaves_like "triggered #{integration_name} integration"
end
context "with succeeded pipeline" do
let(:status) { "success" }
context "with default notify_only_broken_pipelines" do
it "does not call #{service_name} API" do
it "does not call #{integration_name} API" do
result = subject.execute(sample_data)
expect(result).to be_falsy
......@@ -281,7 +282,7 @@ RSpec.shared_examples "chat service" do |service_name|
subject.notify_only_broken_pipelines = false
end
it_behaves_like "triggered #{service_name} service"
it_behaves_like "triggered #{integration_name} integration"
end
end
......@@ -291,19 +292,19 @@ RSpec.shared_examples "chat service" do |service_name|
end
context "when only default branch are to be notified" do
it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "default"
it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "default"
end
context "when only protected branches are to be notified" do
it_behaves_like "untriggered #{service_name} service", branches_to_be_notified: "protected"
it_behaves_like "untriggered #{integration_name} integration", branches_to_be_notified: "protected"
end
context "when default and protected branches are to be notified" do
it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "default_and_protected"
it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "default_and_protected"
end
context "when all branches are to be notified" do
it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "all"
it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "all"
end
end
......@@ -317,19 +318,19 @@ RSpec.shared_examples "chat service" do |service_name|
end
context "when only default branch are to be notified" do
it_behaves_like "untriggered #{service_name} service", branches_to_be_notified: "default"
it_behaves_like "untriggered #{integration_name} integration", branches_to_be_notified: "default"
end
context "when only protected branches are to be notified" do
it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "protected"
it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "protected"
end
context "when default and protected branches are to be notified" do
it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "default_and_protected"
it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "default_and_protected"
end
context "when all branches are to be notified" do
it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "all"
it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "all"
end
end
......@@ -339,19 +340,19 @@ RSpec.shared_examples "chat service" do |service_name|
end
context "when only default branch are to be notified" do
it_behaves_like "untriggered #{service_name} service", branches_to_be_notified: "default"
it_behaves_like "untriggered #{integration_name} integration", branches_to_be_notified: "default"
end
context "when only protected branches are to be notified" do
it_behaves_like "untriggered #{service_name} service", branches_to_be_notified: "protected"
it_behaves_like "untriggered #{integration_name} integration", branches_to_be_notified: "protected"
end
context "when default and protected branches are to be notified" do
it_behaves_like "untriggered #{service_name} service", branches_to_be_notified: "default_and_protected"
it_behaves_like "untriggered #{integration_name} integration", branches_to_be_notified: "default_and_protected"
end
context "when all branches are to be notified" do
it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "all"
it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "all"
end
end
end
......
......@@ -81,7 +81,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
shared_examples 'calls the service API with the event message' do |event_message|
specify do
expect_next_instance_of(Slack::Messenger) do |messenger|
expect_next_instance_of(::Slack::Messenger) do |messenger|
expect(messenger).to receive(:ping).with(event_message, anything).and_call_original
end
......@@ -95,7 +95,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
let(:chat_service_params) { { username: 'slack_username' } }
it 'uses the username as an option' do
expect(Slack::Messenger).to execute_with_options(username: 'slack_username')
expect(::Slack::Messenger).to execute_with_options(username: 'slack_username')
execute_service
end
......@@ -110,7 +110,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
let(:chat_service_params) { { push_channel: 'random' } }
it 'uses the right channel for push event' do
expect(Slack::Messenger).to execute_with_options(channel: ['random'])
expect(::Slack::Messenger).to execute_with_options(channel: ['random'])
execute_service
end
......@@ -136,7 +136,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
let(:chat_service_params) { { issue_channel: 'random' } }
it 'uses the right channel for issue event' do
expect(Slack::Messenger).to execute_with_options(channel: ['random'])
expect(::Slack::Messenger).to execute_with_options(channel: ['random'])
execute_service
end
......@@ -147,7 +147,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
end
it 'falls back to issue channel' do
expect(Slack::Messenger).to execute_with_options(channel: ['random'])
expect(::Slack::Messenger).to execute_with_options(channel: ['random'])
execute_service
end
......@@ -156,7 +156,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
let(:chat_service_params) { { issue_channel: 'random', confidential_issue_channel: 'confidential' } }
it 'uses the confidential issue channel when it is defined' do
expect(Slack::Messenger).to execute_with_options(channel: ['confidential'])
expect(::Slack::Messenger).to execute_with_options(channel: ['confidential'])
execute_service
end
......@@ -175,7 +175,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
let(:chat_service_params) { { merge_request_channel: 'random' } }
it 'uses the right channel for merge request event' do
expect(Slack::Messenger).to execute_with_options(channel: ['random'])
expect(::Slack::Messenger).to execute_with_options(channel: ['random'])
execute_service
end
......@@ -192,7 +192,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
let(:chat_service_params) { { wiki_page_channel: 'random' } }
it 'uses the right channel for wiki event' do
expect(Slack::Messenger).to execute_with_options(channel: ['random'])
expect(::Slack::Messenger).to execute_with_options(channel: ['random'])
execute_service
end
......@@ -216,7 +216,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
let(:chat_service_params) { { note_channel: 'random' } }
it 'uses the right channel' do
expect(Slack::Messenger).to execute_with_options(channel: ['random'])
expect(::Slack::Messenger).to execute_with_options(channel: ['random'])
execute_service
end
......@@ -227,7 +227,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
end
it 'falls back to note channel' do
expect(Slack::Messenger).to execute_with_options(channel: ['random'])
expect(::Slack::Messenger).to execute_with_options(channel: ['random'])
execute_service
end
......@@ -236,7 +236,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
let(:chat_service_params) { { note_channel: 'random', confidential_note_channel: 'confidential' } }
it 'uses confidential channel' do
expect(Slack::Messenger).to execute_with_options(channel: ['confidential'])
expect(::Slack::Messenger).to execute_with_options(channel: ['confidential'])
execute_service
end
......
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