From 141faaacf9119ce5d765efe73c6509030ba078cd Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 25 Nov 2016 17:36:37 -0200 Subject: Mattermost Notifications Service --- app/models/project.rb | 1 + .../project_services/chat_message/base_message.rb | 34 +++ .../project_services/chat_message/build_message.rb | 82 ++++++ .../project_services/chat_message/issue_message.rb | 69 +++++ .../project_services/chat_message/merge_message.rb | 60 ++++ .../project_services/chat_message/note_message.rb | 83 ++++++ .../chat_message/pipeline_message.rb | 78 +++++ .../project_services/chat_message/push_message.rb | 110 +++++++ .../chat_message/wiki_page_message.rb | 53 ++++ app/models/project_services/chat_service.rb | 141 ++++++++- app/models/project_services/mattermost_service.rb | 41 +++ .../mattermost_slash_commands_service.rb | 12 +- app/models/project_services/slack_service.rb | 174 ++--------- .../project_services/slack_service/base_message.rb | 34 --- .../slack_service/build_message.rb | 82 ------ .../slack_service/issue_message.rb | 69 ----- .../slack_service/merge_message.rb | 60 ---- .../project_services/slack_service/note_message.rb | 83 ------ .../slack_service/pipeline_message.rb | 78 ----- .../project_services/slack_service/push_message.rb | 110 ------- .../slack_service/wiki_page_message.rb | 53 ---- app/models/service.rb | 1 + changelogs/unreleased/issue_22269.yml | 4 + doc/api/services.md | 38 ++- .../img/mattermost_configuration.png | Bin 0 -> 73502 bytes doc/project_services/mattermost.md | 45 +++ doc/project_services/project_services.md | 3 +- doc/project_services/slack.md | 4 +- spec/lib/gitlab/import_export/all_models.yml | 1 + .../chat_message/build_message_spec.rb | 57 ++++ .../chat_message/issue_message_spec.rb | 67 +++++ .../chat_message/merge_message_spec.rb | 51 ++++ .../chat_message/note_message_spec.rb | 130 ++++++++ .../chat_message/pipeline_message_spec.rb | 67 +++++ .../chat_message/push_message_spec.rb | 88 ++++++ .../chat_message/wiki_page_message_spec.rb | 73 +++++ spec/models/project_services/chat_service_spec.rb | 11 +- .../project_services/mattermost_service_spec.rb | 5 + .../slack_service/build_message_spec.rb | 57 ---- .../slack_service/issue_message_spec.rb | 67 ----- .../slack_service/merge_message_spec.rb | 51 ---- .../slack_service/note_message_spec.rb | 130 -------- .../slack_service/pipeline_message_spec.rb | 67 ----- .../slack_service/push_message_spec.rb | 88 ------ .../slack_service/wiki_page_message_spec.rb | 73 ----- spec/models/project_services/slack_service_spec.rb | 324 +------------------- spec/models/project_spec.rb | 1 + spec/support/slack_mattermost_shared_examples.rb | 328 +++++++++++++++++++++ 48 files changed, 1736 insertions(+), 1602 deletions(-) create mode 100644 app/models/project_services/chat_message/base_message.rb create mode 100644 app/models/project_services/chat_message/build_message.rb create mode 100644 app/models/project_services/chat_message/issue_message.rb create mode 100644 app/models/project_services/chat_message/merge_message.rb create mode 100644 app/models/project_services/chat_message/note_message.rb create mode 100644 app/models/project_services/chat_message/pipeline_message.rb create mode 100644 app/models/project_services/chat_message/push_message.rb create mode 100644 app/models/project_services/chat_message/wiki_page_message.rb create mode 100644 app/models/project_services/mattermost_service.rb delete mode 100644 app/models/project_services/slack_service/base_message.rb delete mode 100644 app/models/project_services/slack_service/build_message.rb delete mode 100644 app/models/project_services/slack_service/issue_message.rb delete mode 100644 app/models/project_services/slack_service/merge_message.rb delete mode 100644 app/models/project_services/slack_service/note_message.rb delete mode 100644 app/models/project_services/slack_service/pipeline_message.rb delete mode 100644 app/models/project_services/slack_service/push_message.rb delete mode 100644 app/models/project_services/slack_service/wiki_page_message.rb create mode 100644 changelogs/unreleased/issue_22269.yml create mode 100644 doc/project_services/img/mattermost_configuration.png create mode 100644 doc/project_services/mattermost.md create mode 100644 spec/models/project_services/chat_message/build_message_spec.rb create mode 100644 spec/models/project_services/chat_message/issue_message_spec.rb create mode 100644 spec/models/project_services/chat_message/merge_message_spec.rb create mode 100644 spec/models/project_services/chat_message/note_message_spec.rb create mode 100644 spec/models/project_services/chat_message/pipeline_message_spec.rb create mode 100644 spec/models/project_services/chat_message/push_message_spec.rb create mode 100644 spec/models/project_services/chat_message/wiki_page_message_spec.rb create mode 100644 spec/models/project_services/mattermost_service_spec.rb delete mode 100644 spec/models/project_services/slack_service/build_message_spec.rb delete mode 100644 spec/models/project_services/slack_service/issue_message_spec.rb delete mode 100644 spec/models/project_services/slack_service/merge_message_spec.rb delete mode 100644 spec/models/project_services/slack_service/note_message_spec.rb delete mode 100644 spec/models/project_services/slack_service/pipeline_message_spec.rb delete mode 100644 spec/models/project_services/slack_service/push_message_spec.rb delete mode 100644 spec/models/project_services/slack_service/wiki_page_message_spec.rb create mode 100644 spec/support/slack_mattermost_shared_examples.rb diff --git a/app/models/project.rb b/app/models/project.rb index 2c726cfc5df..19c2d24212d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -95,6 +95,7 @@ class Project < ActiveRecord::Base has_one :asana_service, dependent: :destroy has_one :gemnasium_service, dependent: :destroy has_one :mattermost_slash_commands_service, dependent: :destroy + has_one :mattermost_service, dependent: :destroy has_one :slack_service, dependent: :destroy has_one :buildkite_service, dependent: :destroy has_one :bamboo_service, dependent: :destroy diff --git a/app/models/project_services/chat_message/base_message.rb b/app/models/project_services/chat_message/base_message.rb new file mode 100644 index 00000000000..a03605d01fb --- /dev/null +++ b/app/models/project_services/chat_message/base_message.rb @@ -0,0 +1,34 @@ +require 'slack-notifier' + +module ChatMessage + class BaseMessage + def initialize(params) + raise NotImplementedError + end + + def pretext + format(message) + end + + def fallback + end + + def attachments + raise NotImplementedError + end + + private + + def message + raise NotImplementedError + end + + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end + + def attachment_color + '#345' + end + end +end diff --git a/app/models/project_services/chat_message/build_message.rb b/app/models/project_services/chat_message/build_message.rb new file mode 100644 index 00000000000..53e35cb21bf --- /dev/null +++ b/app/models/project_services/chat_message/build_message.rb @@ -0,0 +1,82 @@ +module ChatMessage + class BuildMessage < BaseMessage + attr_reader :sha + attr_reader :ref_type + attr_reader :ref + attr_reader :status + attr_reader :project_name + attr_reader :project_url + attr_reader :user_name + attr_reader :duration + + def initialize(params) + @sha = params[:sha] + @ref_type = params[:tag] ? 'tag' : 'branch' + @ref = params[:ref] + @project_name = params[:project_name] + @project_url = params[:project_url] + @status = params[:commit][:status] + @user_name = params[:commit][:author_name] + @duration = params[:commit][:duration] + end + + def pretext + '' + end + + def fallback + format(message) + end + + def attachments + [{ text: format(message), color: attachment_color }] + end + + private + + def message + "#{project_link}: Commit #{commit_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) + end + + def humanized_status + case status + when 'success' + 'passed' + else + status + end + end + + def attachment_color + if status == 'success' + 'good' + else + 'danger' + end + end + + def branch_url + "#{project_url}/commits/#{ref}" + end + + def branch_link + "[#{ref}](#{branch_url})" + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def commit_url + "#{project_url}/commit/#{sha}/builds" + end + + def commit_link + "[#{Commit.truncate_sha(sha)}](#{commit_url})" + end + end +end diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb new file mode 100644 index 00000000000..14fd64e5332 --- /dev/null +++ b/app/models/project_services/chat_message/issue_message.rb @@ -0,0 +1,69 @@ +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 + attr_reader :state + attr_reader :description + + def initialize(params) + @user_name = params[:user][:username] + @project_name = params[:project_name] + @project_url = params[:project_url] + + obj_attr = params[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + @title = obj_attr[:title] + @issue_iid = obj_attr[:iid] + @issue_url = obj_attr[:url] + @action = obj_attr[:action] + @state = obj_attr[:state] + @description = obj_attr[:description] || '' + end + + def attachments + return [] unless opened_issue? + + description_message + end + + private + + def message + case state + when "opened" + "[#{project_link}] Issue #{state} by #{user_name}" + else + "[#{project_link}] Issue #{issue_link} #{state} by #{user_name}" + end + end + + def opened_issue? + action == "open" + end + + def description_message + [{ + title: issue_title, + title_link: issue_url, + text: format(description), + color: "#C95823" }] + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def issue_link + "[#{issue_title}](#{issue_url})" + end + + def issue_title + "##{issue_iid} #{title}" + end + end +end diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb new file mode 100644 index 00000000000..ab5e8b24167 --- /dev/null +++ b/app/models/project_services/chat_message/merge_message.rb @@ -0,0 +1,60 @@ +module ChatMessage + class MergeMessage < BaseMessage + attr_reader :user_name + attr_reader :project_name + attr_reader :project_url + attr_reader :merge_request_id + 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] + + obj_attr = params[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + @merge_request_id = 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 + + private + + def format_title(title) + '*' + title.lines.first.chomp + '*' + end + + def message + merge_request_message + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def merge_request_message + "#{user_name} #{state} #{merge_request_link} in #{project_link}: #{title}" + end + + def merge_request_link + "[merge request !#{merge_request_id}](#{merge_request_url})" + end + + def merge_request_url + "#{project_url}/merge_requests/#{merge_request_id}" + end + end +end diff --git a/app/models/project_services/chat_message/note_message.rb b/app/models/project_services/chat_message/note_message.rb new file mode 100644 index 00000000000..ca1d7207034 --- /dev/null +++ b/app/models/project_services/chat_message/note_message.rb @@ -0,0 +1,83 @@ +module ChatMessage + class NoteMessage < BaseMessage + attr_reader :message + attr_reader :user_name + attr_reader :project_name + attr_reader :project_link + attr_reader :note + attr_reader :note_url + attr_reader :title + + def initialize(params) + params = HashWithIndifferentAccess.new(params) + @user_name = params[:user][:username] + @project_name = params[:project_name] + @project_url = params[:project_url] + + 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 + when "Commit" + create_commit_note(HashWithIndifferentAccess.new(params[:commit])) + when "Issue" + create_issue_note(HashWithIndifferentAccess.new(params[:issue])) + when "MergeRequest" + create_merge_note(HashWithIndifferentAccess.new(params[:merge_request])) + when "Snippet" + create_snippet_note(HashWithIndifferentAccess.new(params[:snippet])) + end + end + + def attachments + description_message + end + + private + + 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])) + end + + def create_issue_note(issue) + commented_on_message( + "issue ##{issue[:iid]}", + format_title(issue[:title])) + end + + def create_merge_note(merge_request) + commented_on_message( + "merge request !#{merge_request[:iid]}", + format_title(merge_request[:title])) + end + + def create_snippet_note(snippet) + commented_on_message( + "snippet ##{snippet[:id]}", + format_title(snippet[:title])) + end + + def description_message + [{ text: format(@note), color: attachment_color }] + end + + def project_link + "[#{@project_name}](#{@project_url})" + end + + def commented_on_message(target, title) + @message = "#{@user_name} [commented on #{target}](#{@note_url}) in #{project_link}: *#{title}*" + end + end +end diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb new file mode 100644 index 00000000000..210027565a8 --- /dev/null +++ b/app/models/project_services/chat_message/pipeline_message.rb @@ -0,0 +1,78 @@ +module ChatMessage + class PipelineMessage < BaseMessage + attr_reader :ref_type, :ref, :status, :project_name, :project_url, + :user_name, :duration, :pipeline_id + + def initialize(data) + 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 + '' + end + + def fallback + format(message) + end + + def attachments + [{ text: format(message), color: attachment_color }] + 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) + end + + def humanized_status + case status + when 'success' + 'passed' + else + status + end + end + + def attachment_color + if status == 'success' + 'good' + else + 'danger' + end + end + + def branch_url + "#{project_url}/commits/#{ref}" + end + + def branch_link + "[#{ref}](#{branch_url})" + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def pipeline_url + "#{project_url}/pipelines/#{pipeline_id}" + end + + def pipeline_link + "[##{pipeline_id}](#{pipeline_url})" + end + end +end diff --git a/app/models/project_services/chat_message/push_message.rb b/app/models/project_services/chat_message/push_message.rb new file mode 100644 index 00000000000..2d73b71ec37 --- /dev/null +++ b/app/models/project_services/chat_message/push_message.rb @@ -0,0 +1,110 @@ +module ChatMessage + class PushMessage < BaseMessage + 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) + @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? + + commit_message_attachments + end + + private + + def message + if new_branch? + new_branch_message + elsif removed_branch? + removed_branch_message + else + push_message + end + end + + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end + + def new_branch_message + "#{user_name} pushed new #{ref_type} #{branch_link} to #{project_link}" + end + + def removed_branch_message + "#{user_name} removed #{ref_type} #{ref} from #{project_link}" + end + + def push_message + "#{user_name} pushed to #{ref_type} #{branch_link} of #{project_link} (#{compare_link})" + end + + def commit_messages + commits.map { |commit| compose_commit_message(commit) }.join("\n") + end + + def commit_message_attachments + [{ text: format(commit_messages), color: attachment_color }] + end + + def compose_commit_message(commit) + author = commit[:author][:name] + id = Commit.truncate_sha(commit[:id]) + message = commit[:message] + url = commit[:url] + + "[#{id}](#{url}): #{message} - #{author}" + end + + def new_branch? + Gitlab::Git.blank_ref?(before) + end + + def removed_branch? + Gitlab::Git.blank_ref?(after) + end + + def branch_url + "#{project_url}/commits/#{ref}" + end + + def compare_url + "#{project_url}/compare/#{before}...#{after}" + end + + def branch_link + "[#{ref}](#{branch_url})" + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def compare_link + "[Compare changes](#{compare_url})" + end + + def attachment_color + '#345' + end + end +end diff --git a/app/models/project_services/chat_message/wiki_page_message.rb b/app/models/project_services/chat_message/wiki_page_message.rb new file mode 100644 index 00000000000..134083e4504 --- /dev/null +++ b/app/models/project_services/chat_message/wiki_page_message.rb @@ -0,0 +1,53 @@ +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] + + obj_attr = params[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + @title = obj_attr[:title] + @wiki_page_url = obj_attr[:url] + @description = obj_attr[:content] + + @action = + case obj_attr[:action] + when "create" + "created" + when "update" + "edited" + end + end + + def attachments + description_message + end + + private + + def message + "#{user_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*" + end + + def description_message + [{ text: format(@description), color: attachment_color }] + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def wiki_page_link + "[wiki page](#{wiki_page_url})" + end + end +end diff --git a/app/models/project_services/chat_service.rb b/app/models/project_services/chat_service.rb index d36beff5fa6..8ac049ba939 100644 --- a/app/models/project_services/chat_service.rb +++ b/app/models/project_services/chat_service.rb @@ -1,21 +1,148 @@ # Base class for Chat services # This class is not meant to be used directly, but only to inherrit from. class ChatService < Service + include ChatMessage + default_value_for :category, 'chat' - has_many :chat_names, foreign_key: :service_id + prop_accessor :webhook, :username, :channel + boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines + + validates :webhook, presence: true, url: true, if: :activated? + + def initialize_properties + # Custom serialized properties initialization + self.supported_events.each { |event| self.class.prop_accessor(event_channel_name(event)) } - def valid_token?(token) - self.respond_to?(:token) && - self.token.present? && - ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) + if properties.nil? + self.properties = {} + self.notify_only_broken_builds = true + self.notify_only_broken_pipelines = true + end + end + + def can_test? + valid? end def supported_events - [] + %w[push issue confidential_issue merge_request note tag_push + build pipeline wiki_page] end - def trigger(params) + def execute(data) + return unless supported_events.include?(data[:object_kind]) + return unless webhook.present? + + object_kind = data[:object_kind] + + data = data.merge( + project_url: project_url, + project_name: project_name + ) + + # 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 + + opt = {} + + opt[:channel] = get_channel_field(object_kind).presence || channel || default_channel + opt[:username] = username if username + + notifier = Slack::Notifier.new(webhook, opt) + notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback) + + true + 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 raise NotImplementedError end + + private + + def get_message(object_kind, data) + case object_kind + when "push", "tag_push" + PushMessage.new(data) + when "issue" + IssueMessage.new(data) unless is_update?(data) + when "merge_request" + MergeMessage.new(data) unless is_update?(data) + when "note" + NoteMessage.new(data) + when "build" + BuildMessage.new(data) if should_build_be_notified?(data) + when "pipeline" + PipelineMessage.new(data) if should_pipeline_be_notified?(data) + when "wiki_page" + WikiPageMessage.new(data) + end + end + + def get_channel_field(event) + field_name = event_channel_name(event) + self.public_send(field_name) + end + + def build_event_channels + supported_events.reduce([]) do |channels, event| + channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel } + end + end + + def event_channel_name(event) + "#{event}_channel" + end + + def project_name + project.name_with_namespace.gsub(/\s/, '') + end + + def project_url + project.web_url + end + + def is_update?(data) + data[:object_attributes][:action] == 'update' + end + + def should_build_be_notified?(data) + case data[:commit][:status] + when 'success' + !notify_only_broken_builds? + when 'failed' + true + else + false + end + end + + def should_pipeline_be_notified?(data) + case data[:object_attributes][:status] + when 'success' + !notify_only_broken_pipelines? + when 'failed' + true + else + false + end + end end diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb new file mode 100644 index 00000000000..9d61c251a32 --- /dev/null +++ b/app/models/project_services/mattermost_service.rb @@ -0,0 +1,41 @@ +class MattermostService < ChatService + def title + 'Mattermost notifications' + end + + def description + 'Receive event notifications in Mattermost' + end + + def to_param + 'mattermost' + end + + def help + 'This service sends notifications about projects events to Mattermost channels.
+ To set up this service: +
    +
  1. Enable incoming webhooks in your Mattermost installation.
  2. +
  3. Add an incoming webhook in your Mattermost team. The default channel can be overridden for each event.
  4. +
  5. Paste the webhook URL into the field bellow.
  6. +
  7. Select events below to enable notifications. The channel and username are optional.
  8. +
' + end + + def fields + default_fields + build_event_channels + end + + def default_fields + [ + { type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' }, + { type: 'text', name: 'username', placeholder: 'username' }, + { type: 'checkbox', name: 'notify_only_broken_builds' }, + { type: 'checkbox', name: 'notify_only_broken_pipelines' }, + ] + end + + def default_channel + "#town-square" + end +end diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 33431f41dc2..3993dfbda17 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -1,8 +1,18 @@ -class MattermostSlashCommandsService < ChatService +class MattermostSlashCommandsService < Service include TriggersHelper prop_accessor :token + def valid_token?(token) + self.respond_to?(:token) && + self.token.present? && + ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) + end + + def supported_events + [] + end + def can_test? false end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index e1b937817f4..0df1743c4ba 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -1,25 +1,10 @@ -class SlackService < Service - prop_accessor :webhook, :username, :channel - boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines - validates :webhook, presence: true, url: true, if: :activated? - - def initialize_properties - # Custom serialized properties initialization - self.supported_events.each { |event| self.class.prop_accessor(event_channel_name(event)) } - - if properties.nil? - self.properties = {} - self.notify_only_broken_builds = true - self.notify_only_broken_pipelines = true - end - end - +class SlackService < ChatService def title - 'Slack' + 'Slack notifications' end def description - 'A team communication tool for the 21st century' + 'Receive event notifications in Slack' end def to_param @@ -27,150 +12,29 @@ class SlackService < Service end def help - 'This service sends notifications to your Slack channel.
- To setup this Service you need to create a new "Incoming webhook" in your Slack integration panel, - and enter the Webhook URL below.' + 'This service sends notifications about projects events to Slack channels.
+ To setup this service: +
    +
  1. Add an incoming webhook in your Slack team. The default channel can be overridden for each event.
  2. +
  3. Paste the Webhook URL into the field below.
  4. +
  5. Select events below to enable notifications. The channel and username are optional.
  6. +
' end def fields - default_fields = - [ - { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' }, - { type: 'text', name: 'username', placeholder: 'username' }, - { type: 'text', name: 'channel', placeholder: "#general" }, - { type: 'checkbox', name: 'notify_only_broken_builds' }, - { type: 'checkbox', name: 'notify_only_broken_pipelines' }, - ] - default_fields + build_event_channels end - def supported_events - %w[push issue confidential_issue merge_request note tag_push - build pipeline wiki_page] - end - - def execute(data) - return unless supported_events.include?(data[:object_kind]) - return unless webhook.present? - - object_kind = data[:object_kind] - - data = data.merge( - project_url: project_url, - project_name: project_name - ) - - # 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) - - if message - opt = {} - - event_channel = get_channel_field(object_kind) || channel - - opt[:channel] = event_channel if event_channel - opt[:username] = username if username - - notifier = Slack::Notifier.new(webhook, opt) - notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback) - - true - else - false - end - 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) } + def default_fields + [ + { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' }, + { type: 'text', name: 'username', placeholder: 'username' }, + { type: 'checkbox', name: 'notify_only_broken_builds' }, + { type: 'checkbox', name: 'notify_only_broken_pipelines' }, + ] end - def global_fields - fields.reject { |field| field[:name].end_with?('channel') } - end - - private - - def get_message(object_kind, data) - case object_kind - when "push", "tag_push" - PushMessage.new(data) - when "issue" - IssueMessage.new(data) unless is_update?(data) - when "merge_request" - MergeMessage.new(data) unless is_update?(data) - when "note" - NoteMessage.new(data) - when "build" - BuildMessage.new(data) if should_build_be_notified?(data) - when "pipeline" - PipelineMessage.new(data) if should_pipeline_be_notified?(data) - when "wiki_page" - WikiPageMessage.new(data) - end - end - - def get_channel_field(event) - field_name = event_channel_name(event) - self.public_send(field_name) - end - - def build_event_channels - supported_events.reduce([]) do |channels, event| - channels << { type: 'text', name: event_channel_name(event), placeholder: "#general" } - end - end - - def event_channel_name(event) - "#{event}_channel" - end - - def project_name - project.name_with_namespace.gsub(/\s/, '') - end - - def project_url - project.web_url - end - - def is_update?(data) - data[:object_attributes][:action] == 'update' - end - - def should_build_be_notified?(data) - case data[:commit][:status] - when 'success' - !notify_only_broken_builds? - when 'failed' - true - else - false - end - end - - def should_pipeline_be_notified?(data) - case data[:object_attributes][:status] - when 'success' - !notify_only_broken_pipelines? - when 'failed' - true - else - false - end + def default_channel + "#general" end end - -require "slack_service/issue_message" -require "slack_service/push_message" -require "slack_service/merge_message" -require "slack_service/note_message" -require "slack_service/build_message" -require "slack_service/pipeline_message" -require "slack_service/wiki_page_message" diff --git a/app/models/project_services/slack_service/base_message.rb b/app/models/project_services/slack_service/base_message.rb deleted file mode 100644 index f1182824687..00000000000 --- a/app/models/project_services/slack_service/base_message.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'slack-notifier' - -class SlackService - class BaseMessage - def initialize(params) - raise NotImplementedError - end - - def pretext - format(message) - end - - def fallback - end - - def attachments - raise NotImplementedError - end - - private - - def message - raise NotImplementedError - end - - def format(string) - Slack::Notifier::LinkFormatter.format(string) - end - - def attachment_color - '#345' - end - end -end diff --git a/app/models/project_services/slack_service/build_message.rb b/app/models/project_services/slack_service/build_message.rb deleted file mode 100644 index 0fca4267bad..00000000000 --- a/app/models/project_services/slack_service/build_message.rb +++ /dev/null @@ -1,82 +0,0 @@ -class SlackService - class BuildMessage < BaseMessage - attr_reader :sha - attr_reader :ref_type - attr_reader :ref - attr_reader :status - attr_reader :project_name - attr_reader :project_url - attr_reader :user_name - attr_reader :duration - - def initialize(params) - @sha = params[:sha] - @ref_type = params[:tag] ? 'tag' : 'branch' - @ref = params[:ref] - @project_name = params[:project_name] - @project_url = params[:project_url] - @status = params[:commit][:status] - @user_name = params[:commit][:author_name] - @duration = params[:commit][:duration] - end - - def pretext - '' - end - - def fallback - format(message) - end - - def attachments - [{ text: format(message), color: attachment_color }] - end - - private - - def message - "#{project_link}: Commit #{commit_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) - end - - def humanized_status - case status - when 'success' - 'passed' - else - status - end - end - - def attachment_color - if status == 'success' - 'good' - else - 'danger' - end - end - - def branch_url - "#{project_url}/commits/#{ref}" - end - - def branch_link - "[#{ref}](#{branch_url})" - end - - def project_link - "[#{project_name}](#{project_url})" - end - - def commit_url - "#{project_url}/commit/#{sha}/builds" - end - - def commit_link - "[#{Commit.truncate_sha(sha)}](#{commit_url})" - end - end -end diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/slack_service/issue_message.rb deleted file mode 100644 index cd87a79d0c6..00000000000 --- a/app/models/project_services/slack_service/issue_message.rb +++ /dev/null @@ -1,69 +0,0 @@ -class SlackService - 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 - attr_reader :state - attr_reader :description - - def initialize(params) - @user_name = params[:user][:username] - @project_name = params[:project_name] - @project_url = params[:project_url] - - obj_attr = params[:object_attributes] - obj_attr = HashWithIndifferentAccess.new(obj_attr) - @title = obj_attr[:title] - @issue_iid = obj_attr[:iid] - @issue_url = obj_attr[:url] - @action = obj_attr[:action] - @state = obj_attr[:state] - @description = obj_attr[:description] || '' - end - - def attachments - return [] unless opened_issue? - - description_message - end - - private - - def message - case state - when "opened" - "[#{project_link}] Issue #{state} by #{user_name}" - else - "[#{project_link}] Issue #{issue_link} #{state} by #{user_name}" - end - end - - def opened_issue? - action == "open" - end - - def description_message - [{ - title: issue_title, - title_link: issue_url, - text: format(description), - color: "#C95823" }] - end - - def project_link - "[#{project_name}](#{project_url})" - end - - def issue_link - "[#{issue_title}](#{issue_url})" - end - - def issue_title - "##{issue_iid} #{title}" - end - end -end diff --git a/app/models/project_services/slack_service/merge_message.rb b/app/models/project_services/slack_service/merge_message.rb deleted file mode 100644 index b7615c96068..00000000000 --- a/app/models/project_services/slack_service/merge_message.rb +++ /dev/null @@ -1,60 +0,0 @@ -class SlackService - class MergeMessage < BaseMessage - attr_reader :user_name - attr_reader :project_name - attr_reader :project_url - attr_reader :merge_request_id - 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] - - obj_attr = params[:object_attributes] - obj_attr = HashWithIndifferentAccess.new(obj_attr) - @merge_request_id = 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 - - private - - def format_title(title) - '*' + title.lines.first.chomp + '*' - end - - def message - merge_request_message - end - - def project_link - "[#{project_name}](#{project_url})" - end - - def merge_request_message - "#{user_name} #{state} #{merge_request_link} in #{project_link}: #{title}" - end - - def merge_request_link - "[merge request !#{merge_request_id}](#{merge_request_url})" - end - - def merge_request_url - "#{project_url}/merge_requests/#{merge_request_id}" - end - end -end diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/slack_service/note_message.rb deleted file mode 100644 index 797c5937f09..00000000000 --- a/app/models/project_services/slack_service/note_message.rb +++ /dev/null @@ -1,83 +0,0 @@ -class SlackService - class NoteMessage < BaseMessage - attr_reader :message - attr_reader :user_name - attr_reader :project_name - attr_reader :project_link - attr_reader :note - attr_reader :note_url - attr_reader :title - - def initialize(params) - params = HashWithIndifferentAccess.new(params) - @user_name = params[:user][:username] - @project_name = params[:project_name] - @project_url = params[:project_url] - - 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 - when "Commit" - create_commit_note(HashWithIndifferentAccess.new(params[:commit])) - when "Issue" - create_issue_note(HashWithIndifferentAccess.new(params[:issue])) - when "MergeRequest" - create_merge_note(HashWithIndifferentAccess.new(params[:merge_request])) - when "Snippet" - create_snippet_note(HashWithIndifferentAccess.new(params[:snippet])) - end - end - - def attachments - description_message - end - - private - - 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])) - end - - def create_issue_note(issue) - commented_on_message( - "issue ##{issue[:iid]}", - format_title(issue[:title])) - end - - def create_merge_note(merge_request) - commented_on_message( - "merge request !#{merge_request[:iid]}", - format_title(merge_request[:title])) - end - - def create_snippet_note(snippet) - commented_on_message( - "snippet ##{snippet[:id]}", - format_title(snippet[:title])) - end - - def description_message - [{ text: format(@note), color: attachment_color }] - end - - def project_link - "[#{@project_name}](#{@project_url})" - end - - def commented_on_message(target, title) - @message = "#{@user_name} [commented on #{target}](#{@note_url}) in #{project_link}: *#{title}*" - end - end -end diff --git a/app/models/project_services/slack_service/pipeline_message.rb b/app/models/project_services/slack_service/pipeline_message.rb deleted file mode 100644 index b6355fc4171..00000000000 --- a/app/models/project_services/slack_service/pipeline_message.rb +++ /dev/null @@ -1,78 +0,0 @@ -class SlackService - class PipelineMessage < BaseMessage - attr_reader :ref_type, :ref, :status, :project_name, :project_url, - :user_name, :duration, :pipeline_id - - def initialize(data) - 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 - '' - end - - def fallback - format(message) - end - - def attachments - [{ text: format(message), color: attachment_color }] - 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) - end - - def humanized_status - case status - when 'success' - 'passed' - else - status - end - end - - def attachment_color - if status == 'success' - 'good' - else - 'danger' - end - end - - def branch_url - "#{project_url}/commits/#{ref}" - end - - def branch_link - "[#{ref}](#{branch_url})" - end - - def project_link - "[#{project_name}](#{project_url})" - end - - def pipeline_url - "#{project_url}/pipelines/#{pipeline_id}" - end - - def pipeline_link - "[##{pipeline_id}](#{pipeline_url})" - end - end -end diff --git a/app/models/project_services/slack_service/push_message.rb b/app/models/project_services/slack_service/push_message.rb deleted file mode 100644 index b26f3e9ddce..00000000000 --- a/app/models/project_services/slack_service/push_message.rb +++ /dev/null @@ -1,110 +0,0 @@ -class SlackService - class PushMessage < BaseMessage - 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) - @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? - - commit_message_attachments - end - - private - - def message - if new_branch? - new_branch_message - elsif removed_branch? - removed_branch_message - else - push_message - end - end - - def format(string) - Slack::Notifier::LinkFormatter.format(string) - end - - def new_branch_message - "#{user_name} pushed new #{ref_type} #{branch_link} to #{project_link}" - end - - def removed_branch_message - "#{user_name} removed #{ref_type} #{ref} from #{project_link}" - end - - def push_message - "#{user_name} pushed to #{ref_type} #{branch_link} of #{project_link} (#{compare_link})" - end - - def commit_messages - commits.map { |commit| compose_commit_message(commit) }.join("\n") - end - - def commit_message_attachments - [{ text: format(commit_messages), color: attachment_color }] - end - - def compose_commit_message(commit) - author = commit[:author][:name] - id = Commit.truncate_sha(commit[:id]) - message = commit[:message] - url = commit[:url] - - "[#{id}](#{url}): #{message} - #{author}" - end - - def new_branch? - Gitlab::Git.blank_ref?(before) - end - - def removed_branch? - Gitlab::Git.blank_ref?(after) - end - - def branch_url - "#{project_url}/commits/#{ref}" - end - - def compare_url - "#{project_url}/compare/#{before}...#{after}" - end - - def branch_link - "[#{ref}](#{branch_url})" - end - - def project_link - "[#{project_name}](#{project_url})" - end - - def compare_link - "[Compare changes](#{compare_url})" - end - - def attachment_color - '#345' - end - end -end diff --git a/app/models/project_services/slack_service/wiki_page_message.rb b/app/models/project_services/slack_service/wiki_page_message.rb deleted file mode 100644 index 160ca3ac115..00000000000 --- a/app/models/project_services/slack_service/wiki_page_message.rb +++ /dev/null @@ -1,53 +0,0 @@ -class SlackService - 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] - - obj_attr = params[:object_attributes] - obj_attr = HashWithIndifferentAccess.new(obj_attr) - @title = obj_attr[:title] - @wiki_page_url = obj_attr[:url] - @description = obj_attr[:content] - - @action = - case obj_attr[:action] - when "create" - "created" - when "update" - "edited" - end - end - - def attachments - description_message - end - - private - - def message - "#{user_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*" - end - - def description_message - [{ text: format(@description), color: attachment_color }] - end - - def project_link - "[#{project_name}](#{project_url})" - end - - def wiki_page_link - "[wiki page](#{wiki_page_url})" - end - end -end diff --git a/app/models/service.rb b/app/models/service.rb index e49a8fa2904..8e58f2a1925 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -220,6 +220,7 @@ class Service < ActiveRecord::Base pivotaltracker pushover redmine + mattermost slack teamcity ] diff --git a/changelogs/unreleased/issue_22269.yml b/changelogs/unreleased/issue_22269.yml new file mode 100644 index 00000000000..6b7164aff77 --- /dev/null +++ b/changelogs/unreleased/issue_22269.yml @@ -0,0 +1,4 @@ +--- +title: Create mattermost service +merge_request: +author: diff --git a/doc/api/services.md b/doc/api/services.md index 3dad953cd1e..1466b8189b0 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -703,9 +703,9 @@ Get Redmine service settings for a project. GET /projects/:id/services/redmine ``` -## Slack +## Slack notifications -A team communication tool for the 21st century +Receive event notifications in Slack ### Create/Edit Slack service @@ -737,6 +737,40 @@ Get Slack service settings for a project. GET /projects/:id/services/slack ``` +## Mattermost notifications + +Receive event notifications in Mattermost + +### Create/Edit Mattermost notifications service + +Set Mattermost service for a project. + +``` +PUT /projects/:id/services/mattermost +``` + +Parameters: + +- `webhook` (**required**) - https://mattermost.example/hooks/1298aff... +- `username` (optional) - username +- `channel` (optional) - #channel + +### Delete Mattermost notifications service + +Delete Mattermost Notifications service for a project. + +``` +DELETE /projects/:id/services/mattermost +``` + +### Get Mattermost notifications service settings + +Get Mattermost notifications service settings for a project. + +``` +GET /projects/:id/services/mattermost +``` + ## JetBrains TeamCity CI A continuous integration and build server diff --git a/doc/project_services/img/mattermost_configuration.png b/doc/project_services/img/mattermost_configuration.png new file mode 100644 index 00000000000..3c5ff5ee317 Binary files /dev/null and b/doc/project_services/img/mattermost_configuration.png differ diff --git a/doc/project_services/mattermost.md b/doc/project_services/mattermost.md new file mode 100644 index 00000000000..fbc7dfeee6d --- /dev/null +++ b/doc/project_services/mattermost.md @@ -0,0 +1,45 @@ +# Mattermost Notifications Service + +## On Mattermost + +To enable Mattermost integration you must create an incoming webhook integration: + +1. Sign in to your Mattermost instance +1. Visit incoming webhooks, that will be something like: https://mattermost.example/your_team_name/integrations/incoming_webhooks/add +1. Choose a display name, description and channel, those can be overridden on GitLab +1. Save it, copy the **Webhook URL**, we'll need this later for GitLab. + +There might be some cases that Incoming Webhooks are blocked by admin, ask your mattermost admin to enable +it on https://mattermost.example/admin_console/integrations/custom. + +Display name override is not enabled by default, you need to ask your admin to enable it on that same section. + +## On GitLab + +After you set up Mattermost, it's time to set up GitLab. + +Go to your project's **Settings > Services > Mattermost Notifications** and you will see a +checkbox with the following events that can be triggered: + +- Push +- Issue +- Merge request +- Note +- Tag push +- Build +- Wiki page + +Bellow each of these event checkboxes, you will have an input field to insert +which Mattermost channel you want to send that event message, with `#town-square` +being the default. The hash sign is optional. + +At the end, fill in your Mattermost details: + +| Field | Description | +| ----- | ----------- | +| **Webhook** | The incoming webhooks which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo... | +| **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. | +| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. | + + +![Mattermost configuration](img/mattermost_configuration.png) diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index a7bcd186a8c..0f398874b8f 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -44,10 +44,11 @@ further configuration instructions and details. Contributions are welcome. | JetBrains TeamCity CI | A continuous integration and build server | | [Kubernetes](kubernetes.md) | A containerized deployment service | | [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands | +| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost | +| [Slack Notifications](slack.md) | Receive event notifications in Slack | | PivotalTracker | Project Management Software (Source Commits Endpoint) | | Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop | | [Redmine](redmine.md) | Redmine issue tracker | -| [Slack](slack.md) | A team communication tool for the 21st century | ## Services Templates diff --git a/doc/project_services/slack.md b/doc/project_services/slack.md index 3cfe77c9f85..0b682b43810 100644 --- a/doc/project_services/slack.md +++ b/doc/project_services/slack.md @@ -1,4 +1,4 @@ -# Slack Service +# Slack Notifications Service ## On Slack @@ -15,7 +15,7 @@ Slack: After you set up Slack, it's time to set up GitLab. -Go to your project's **Settings > Services > Slack** and you will see a +Go to your project's **Settings > Services > Slack Notifications** and you will see a checkbox with the following events that can be triggered: - Push diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index c4ee838b7c9..068137f6255 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -137,6 +137,7 @@ project: - asana_service - gemnasium_service - slack_service +- mattermost_service - buildkite_service - bamboo_service - teamcity_service diff --git a/spec/models/project_services/chat_message/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb new file mode 100644 index 00000000000..b71d153f814 --- /dev/null +++ b/spec/models/project_services/chat_message/build_message_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe ChatMessage::BuildMessage do + subject { described_class.new(args) } + + let(:args) do + { + sha: '97de212e80737a608d939f648d959671fb0a0142', + ref: 'develop', + tag: false, + + project_name: 'project_name', + project_url: 'example.gitlab.com', + + commit: { + status: status, + author_name: 'hacker', + duration: duration, + }, + } + end + + let(:message) { build_message } + + context 'build succeeded' do + let(:status) { 'success' } + let(:color) { 'good' } + let(:duration) { 10 } + let(:message) { build_message('passed') } + + it 'returns a message with information about succeeded build' do + expect(subject.pretext).to be_empty + expect(subject.fallback).to eq(message) + expect(subject.attachments).to eq([text: message, color: color]) + end + end + + context 'build failed' do + let(:status) { 'failed' } + let(:color) { 'danger' } + let(:duration) { 10 } + + it 'returns a message with information about failed build' do + expect(subject.pretext).to be_empty + expect(subject.fallback).to eq(message) + expect(subject.attachments).to eq([text: message, color: color]) + end + end + + def build_message(status_text = status) + ":" \ + " Commit " \ + " of branch" \ + " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}" + end +end diff --git a/spec/models/project_services/chat_message/issue_message_spec.rb b/spec/models/project_services/chat_message/issue_message_spec.rb new file mode 100644 index 00000000000..ebe0ead4408 --- /dev/null +++ b/spec/models/project_services/chat_message/issue_message_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe ChatMessage::IssueMessage, models: true do + subject { described_class.new(args) } + + let(:args) do + { + user: { + name: 'Test User', + username: 'test.user' + }, + project_name: 'project_name', + project_url: 'somewhere.com', + + object_attributes: { + title: 'Issue title', + id: 10, + iid: 100, + assignee_id: 1, + url: 'url', + action: 'open', + state: 'opened', + description: 'issue description' + } + } + end + + let(:color) { '#C95823' } + + context '#initialize' do + before do + args[:object_attributes][:description] = nil + end + + it 'returns a non-null description' do + expect(subject.description).to eq('') + end + end + + context 'open' do + it 'returns a message regarding opening of issues' do + expect(subject.pretext).to eq( + '] Issue opened by test.user') + expect(subject.attachments).to eq([ + { + title: "#100 Issue title", + title_link: "url", + text: "issue description", + color: color, + } + ]) + 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( + '] Issue closed by test.user') + expect(subject.attachments).to be_empty + end + end +end diff --git a/spec/models/project_services/chat_message/merge_message_spec.rb b/spec/models/project_services/chat_message/merge_message_spec.rb new file mode 100644 index 00000000000..07c414c6ca4 --- /dev/null +++ b/spec/models/project_services/chat_message/merge_message_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe ChatMessage::MergeMessage, models: true do + subject { described_class.new(args) } + + let(:args) do + { + user: { + name: 'Test User', + username: 'test.user' + }, + project_name: 'project_name', + project_url: 'somewhere.com', + + object_attributes: { + title: "Issue title\nSecond line", + id: 10, + iid: 100, + assignee_id: 1, + url: 'url', + state: 'opened', + description: 'issue description', + source_branch: 'source_branch', + target_branch: 'target_branch', + } + } + end + + let(:color) { '#345' } + + context 'open' do + it 'returns a message regarding opening of merge requests' do + expect(subject.pretext).to eq( + 'test.user opened '\ + 'in : *Issue title*') + expect(subject.attachments).to be_empty + 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 '\ + 'in : *Issue title*') + expect(subject.attachments).to be_empty + end + end +end diff --git a/spec/models/project_services/chat_message/note_message_spec.rb b/spec/models/project_services/chat_message/note_message_spec.rb new file mode 100644 index 00000000000..31936da40a2 --- /dev/null +++ b/spec/models/project_services/chat_message/note_message_spec.rb @@ -0,0 +1,130 @@ +require 'spec_helper' + +describe ChatMessage::NoteMessage, models: true do + let(:color) { '#345' } + + before do + @args = { + user: { + name: 'Test User', + username: 'test.user', + avatar_url: 'http://fakeavatar' + }, + project_name: 'project_name', + project_url: 'somewhere.com', + repository: { + name: 'project_name', + url: 'somewhere.com', + }, + object_attributes: { + id: 10, + note: 'comment on a commit', + url: 'url', + noteable_type: 'Commit' + } + } + end + + context 'commit notes' do + before do + @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 + + it 'returns a message regarding notes on commits' do + message = described_class.new(@args) + expect(message.pretext).to eq("test.user in : " \ + "*Added a commit message*") + expected_attachments = [ + { + text: "comment on a commit", + color: color, + } + ] + expect(message.attachments).to eq(expected_attachments) + 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] = { + id: 1, + iid: 30, + title: "merge request title\ndetails\n" + } + end + + it 'returns a message regarding notes on a merge request' do + message = described_class.new(@args) + expect(message.pretext).to eq("test.user in : " \ + "*merge request title*") + expected_attachments = [ + { + text: "comment on a merge request", + color: color, + } + ] + expect(message.attachments).to eq(expected_attachments) + end + end + + context 'issue notes' do + before do + @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 + + it 'returns a message regarding notes on an issue' do + message = described_class.new(@args) + expect(message.pretext).to eq( + "test.user in : " \ + "*issue title*") + expected_attachments = [ + { + text: "comment on an issue", + color: color, + } + ] + expect(message.attachments).to eq(expected_attachments) + 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] = { + id: 5, + title: "snippet title\ndetails\n" + } + end + + it 'returns a message regarding notes on a project snippet' do + message = described_class.new(@args) + expect(message.pretext).to eq("test.user in : " \ + "*snippet title*") + expected_attachments = [ + { + text: "comment on a snippet", + color: color, + } + ] + expect(message.attachments).to eq(expected_attachments) + end + end +end diff --git a/spec/models/project_services/chat_message/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb new file mode 100644 index 00000000000..eca71db07b6 --- /dev/null +++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe ChatMessage::PipelineMessage do + subject { described_class.new(args) } + let(:user) { { name: 'hacker' } } + + let(:args) do + { + object_attributes: { + id: 123, + sha: '97de212e80737a608d939f648d959671fb0a0142', + tag: false, + ref: 'develop', + status: status, + duration: duration + }, + project: { path_with_namespace: 'project_name', + web_url: 'example.gitlab.com' }, + user: user + } + end + + let(:message) { build_message } + + context 'pipeline succeeded' do + let(:status) { 'success' } + let(:color) { 'good' } + let(:duration) { 10 } + let(:message) { build_message('passed') } + + it 'returns a message with information about succeeded build' do + verify_message + end + end + + context 'pipeline failed' do + let(:status) { 'failed' } + let(:color) { 'danger' } + let(:duration) { 10 } + + it 'returns a message with information about failed build' do + verify_message + end + + context 'when triggered by API therefore lacking user' do + let(:user) { nil } + 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 + + def build_message(status_text = status, name = user[:name]) + ":" \ + " Pipeline " \ + " of branch" \ + " by #{name} #{status_text} in #{duration} #{'second'.pluralize(duration)}" + end +end diff --git a/spec/models/project_services/chat_message/push_message_spec.rb b/spec/models/project_services/chat_message/push_message_spec.rb new file mode 100644 index 00000000000..b781c4505db --- /dev/null +++ b/spec/models/project_services/chat_message/push_message_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe ChatMessage::PushMessage, models: true do + subject { described_class.new(args) } + + let(:args) do + { + after: 'after', + before: 'before', + project_name: 'project_name', + ref: 'refs/heads/master', + user_name: 'test.user', + project_url: 'url' + } + end + + let(:color) { '#345' } + + context 'push' do + before do + args[:commits] = [ + { message: 'message1', url: 'url1', id: 'abcdefghijkl', author: { name: 'author1' } }, + { message: 'message2', url: 'url2', id: '123456789012', author: { name: 'author2' } }, + ] + end + + it 'returns a message regarding pushes' do + expect(subject.pretext).to eq( + 'test.user pushed to branch of '\ + ' ()' + ) + expect(subject.attachments).to eq([ + { + text: ": message1 - author1\n"\ + ": message2 - author2", + color: color, + } + ]) + end + end + + context 'tag push' do + let(:args) do + { + after: 'after', + before: Gitlab::Git::BLANK_SHA, + project_name: 'project_name', + ref: 'refs/tags/new_tag', + user_name: 'test.user', + project_url: 'url' + } + end + + it 'returns a message regarding pushes' do + expect(subject.pretext).to eq('test.user pushed new tag ' \ + ' to ' \ + '') + expect(subject.attachments).to be_empty + end + end + + context 'new branch' do + before do + args[:before] = Gitlab::Git::BLANK_SHA + end + + it 'returns a message regarding a new branch' do + expect(subject.pretext).to eq( + 'test.user pushed new branch to '\ + '' + ) + expect(subject.attachments).to be_empty + end + end + + context 'removed branch' do + before do + args[:after] = Gitlab::Git::BLANK_SHA + end + + it 'returns a message regarding a removed branch' do + expect(subject.pretext).to eq( + 'test.user removed branch master from ' + ) + expect(subject.attachments).to be_empty + end + end +end diff --git a/spec/models/project_services/chat_message/wiki_page_message_spec.rb b/spec/models/project_services/chat_message/wiki_page_message_spec.rb new file mode 100644 index 00000000000..94c04dc0865 --- /dev/null +++ b/spec/models/project_services/chat_message/wiki_page_message_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' + +describe ChatMessage::WikiPageMessage, models: true do + subject { described_class.new(args) } + + let(:args) do + { + user: { + name: 'Test User', + username: 'test.user' + }, + project_name: 'project_name', + project_url: 'somewhere.com', + object_attributes: { + title: 'Wiki page title', + url: 'url', + content: 'Wiki page description' + } + } + 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 in : '\ + '*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 in : '\ + '*Wiki page title*') + end + end + end + + describe '#attachments' do + let(:color) { '#345' } + + 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([ + { + text: "Wiki page description", + color: color, + } + ]) + 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([ + { + text: "Wiki page description", + color: color, + } + ]) + end + end + end +end diff --git a/spec/models/project_services/chat_service_spec.rb b/spec/models/project_services/chat_service_spec.rb index c6a45a3e1be..e6314a43501 100644 --- a/spec/models/project_services/chat_service_spec.rb +++ b/spec/models/project_services/chat_service_spec.rb @@ -2,14 +2,7 @@ require 'spec_helper' describe ChatService, models: true do describe "Associations" do - it { is_expected.to have_many :chat_names } - end - - describe '#valid_token?' do - subject { described_class.new } - - it 'is false as it has no token' do - expect(subject.valid_token?('wer')).to be_falsey - end + before { allow(subject).to receive(:activated?).and_return(true) } + it { is_expected.to validate_presence_of :webhook } end end diff --git a/spec/models/project_services/mattermost_service_spec.rb b/spec/models/project_services/mattermost_service_spec.rb new file mode 100644 index 00000000000..1e5b4c715c3 --- /dev/null +++ b/spec/models/project_services/mattermost_service_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe MattermostService, models: true do + it_behaves_like "slack or mattermost" +end diff --git a/spec/models/project_services/slack_service/build_message_spec.rb b/spec/models/project_services/slack_service/build_message_spec.rb deleted file mode 100644 index 452f4e2782c..00000000000 --- a/spec/models/project_services/slack_service/build_message_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -require 'spec_helper' - -describe SlackService::BuildMessage do - subject { SlackService::BuildMessage.new(args) } - - let(:args) do - { - sha: '97de212e80737a608d939f648d959671fb0a0142', - ref: 'develop', - tag: false, - - project_name: 'project_name', - project_url: 'example.gitlab.com', - - commit: { - status: status, - author_name: 'hacker', - duration: duration, - }, - } - end - - let(:message) { build_message } - - context 'build succeeded' do - let(:status) { 'success' } - let(:color) { 'good' } - let(:duration) { 10 } - let(:message) { build_message('passed') } - - it 'returns a message with information about succeeded build' do - expect(subject.pretext).to be_empty - expect(subject.fallback).to eq(message) - expect(subject.attachments).to eq([text: message, color: color]) - end - end - - context 'build failed' do - let(:status) { 'failed' } - let(:color) { 'danger' } - let(:duration) { 10 } - - it 'returns a message with information about failed build' do - expect(subject.pretext).to be_empty - expect(subject.fallback).to eq(message) - expect(subject.attachments).to eq([text: message, color: color]) - end - end - - def build_message(status_text = status) - ":" \ - " Commit " \ - " of branch" \ - " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}" - end -end diff --git a/spec/models/project_services/slack_service/issue_message_spec.rb b/spec/models/project_services/slack_service/issue_message_spec.rb deleted file mode 100644 index 98c36ec088d..00000000000 --- a/spec/models/project_services/slack_service/issue_message_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'spec_helper' - -describe SlackService::IssueMessage, models: true do - subject { SlackService::IssueMessage.new(args) } - - let(:args) do - { - user: { - name: 'Test User', - username: 'test.user' - }, - project_name: 'project_name', - project_url: 'somewhere.com', - - object_attributes: { - title: 'Issue title', - id: 10, - iid: 100, - assignee_id: 1, - url: 'url', - action: 'open', - state: 'opened', - description: 'issue description' - } - } - end - - let(:color) { '#C95823' } - - context '#initialize' do - before do - args[:object_attributes][:description] = nil - end - - it 'returns a non-null description' do - expect(subject.description).to eq('') - end - end - - context 'open' do - it 'returns a message regarding opening of issues' do - expect(subject.pretext).to eq( - '] Issue opened by test.user') - expect(subject.attachments).to eq([ - { - title: "#100 Issue title", - title_link: "url", - text: "issue description", - color: color, - } - ]) - 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( - '] Issue closed by test.user') - expect(subject.attachments).to be_empty - end - end -end diff --git a/spec/models/project_services/slack_service/merge_message_spec.rb b/spec/models/project_services/slack_service/merge_message_spec.rb deleted file mode 100644 index c5c052d9af1..00000000000 --- a/spec/models/project_services/slack_service/merge_message_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'spec_helper' - -describe SlackService::MergeMessage, models: true do - subject { SlackService::MergeMessage.new(args) } - - let(:args) do - { - user: { - name: 'Test User', - username: 'test.user' - }, - project_name: 'project_name', - project_url: 'somewhere.com', - - object_attributes: { - title: "Issue title\nSecond line", - id: 10, - iid: 100, - assignee_id: 1, - url: 'url', - state: 'opened', - description: 'issue description', - source_branch: 'source_branch', - target_branch: 'target_branch', - } - } - end - - let(:color) { '#345' } - - context 'open' do - it 'returns a message regarding opening of merge requests' do - expect(subject.pretext).to eq( - 'test.user opened '\ - 'in : *Issue title*') - expect(subject.attachments).to be_empty - 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 '\ - 'in : *Issue title*') - expect(subject.attachments).to be_empty - end - end -end diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb deleted file mode 100644 index 97f818125d3..00000000000 --- a/spec/models/project_services/slack_service/note_message_spec.rb +++ /dev/null @@ -1,130 +0,0 @@ -require 'spec_helper' - -describe SlackService::NoteMessage, models: true do - let(:color) { '#345' } - - before do - @args = { - user: { - name: 'Test User', - username: 'test.user', - avatar_url: 'http://fakeavatar' - }, - project_name: 'project_name', - project_url: 'somewhere.com', - repository: { - name: 'project_name', - url: 'somewhere.com', - }, - object_attributes: { - id: 10, - note: 'comment on a commit', - url: 'url', - noteable_type: 'Commit' - } - } - end - - context 'commit notes' do - before do - @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 - - it 'returns a message regarding notes on commits' do - message = SlackService::NoteMessage.new(@args) - expect(message.pretext).to eq("test.user in : " \ - "*Added a commit message*") - expected_attachments = [ - { - text: "comment on a commit", - color: color, - } - ] - expect(message.attachments).to eq(expected_attachments) - 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] = { - id: 1, - iid: 30, - title: "merge request title\ndetails\n" - } - end - - it 'returns a message regarding notes on a merge request' do - message = SlackService::NoteMessage.new(@args) - expect(message.pretext).to eq("test.user in : " \ - "*merge request title*") - expected_attachments = [ - { - text: "comment on a merge request", - color: color, - } - ] - expect(message.attachments).to eq(expected_attachments) - end - end - - context 'issue notes' do - before do - @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 - - it 'returns a message regarding notes on an issue' do - message = SlackService::NoteMessage.new(@args) - expect(message.pretext).to eq( - "test.user in : " \ - "*issue title*") - expected_attachments = [ - { - text: "comment on an issue", - color: color, - } - ] - expect(message.attachments).to eq(expected_attachments) - 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] = { - id: 5, - title: "snippet title\ndetails\n" - } - end - - it 'returns a message regarding notes on a project snippet' do - message = SlackService::NoteMessage.new(@args) - expect(message.pretext).to eq("test.user in : " \ - "*snippet title*") - expected_attachments = [ - { - text: "comment on a snippet", - color: color, - } - ] - expect(message.attachments).to eq(expected_attachments) - end - end -end diff --git a/spec/models/project_services/slack_service/pipeline_message_spec.rb b/spec/models/project_services/slack_service/pipeline_message_spec.rb deleted file mode 100644 index 4098500122f..00000000000 --- a/spec/models/project_services/slack_service/pipeline_message_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'spec_helper' - -describe SlackService::PipelineMessage do - subject { SlackService::PipelineMessage.new(args) } - let(:user) { { name: 'hacker' } } - - let(:args) do - { - object_attributes: { - id: 123, - sha: '97de212e80737a608d939f648d959671fb0a0142', - tag: false, - ref: 'develop', - status: status, - duration: duration - }, - project: { path_with_namespace: 'project_name', - web_url: 'example.gitlab.com' }, - user: user - } - end - - let(:message) { build_message } - - context 'pipeline succeeded' do - let(:status) { 'success' } - let(:color) { 'good' } - let(:duration) { 10 } - let(:message) { build_message('passed') } - - it 'returns a message with information about succeeded build' do - verify_message - end - end - - context 'pipeline failed' do - let(:status) { 'failed' } - let(:color) { 'danger' } - let(:duration) { 10 } - - it 'returns a message with information about failed build' do - verify_message - end - - context 'when triggered by API therefore lacking user' do - let(:user) { nil } - 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 - - def build_message(status_text = status, name = user[:name]) - ":" \ - " Pipeline " \ - " of branch" \ - " by #{name} #{status_text} in #{duration} #{'second'.pluralize(duration)}" - end -end diff --git a/spec/models/project_services/slack_service/push_message_spec.rb b/spec/models/project_services/slack_service/push_message_spec.rb deleted file mode 100644 index 17cd05e24f1..00000000000 --- a/spec/models/project_services/slack_service/push_message_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -require 'spec_helper' - -describe SlackService::PushMessage, models: true do - subject { SlackService::PushMessage.new(args) } - - let(:args) do - { - after: 'after', - before: 'before', - project_name: 'project_name', - ref: 'refs/heads/master', - user_name: 'test.user', - project_url: 'url' - } - end - - let(:color) { '#345' } - - context 'push' do - before do - args[:commits] = [ - { message: 'message1', url: 'url1', id: 'abcdefghijkl', author: { name: 'author1' } }, - { message: 'message2', url: 'url2', id: '123456789012', author: { name: 'author2' } }, - ] - end - - it 'returns a message regarding pushes' do - expect(subject.pretext).to eq( - 'test.user pushed to branch of '\ - ' ()' - ) - expect(subject.attachments).to eq([ - { - text: ": message1 - author1\n"\ - ": message2 - author2", - color: color, - } - ]) - end - end - - context 'tag push' do - let(:args) do - { - after: 'after', - before: Gitlab::Git::BLANK_SHA, - project_name: 'project_name', - ref: 'refs/tags/new_tag', - user_name: 'test.user', - project_url: 'url' - } - end - - it 'returns a message regarding pushes' do - expect(subject.pretext).to eq('test.user pushed new tag ' \ - ' to ' \ - '') - expect(subject.attachments).to be_empty - end - end - - context 'new branch' do - before do - args[:before] = Gitlab::Git::BLANK_SHA - end - - it 'returns a message regarding a new branch' do - expect(subject.pretext).to eq( - 'test.user pushed new branch to '\ - '' - ) - expect(subject.attachments).to be_empty - end - end - - context 'removed branch' do - before do - args[:after] = Gitlab::Git::BLANK_SHA - end - - it 'returns a message regarding a removed branch' do - expect(subject.pretext).to eq( - 'test.user removed branch master from ' - ) - expect(subject.attachments).to be_empty - end - end -end diff --git a/spec/models/project_services/slack_service/wiki_page_message_spec.rb b/spec/models/project_services/slack_service/wiki_page_message_spec.rb deleted file mode 100644 index 093911598b0..00000000000 --- a/spec/models/project_services/slack_service/wiki_page_message_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -require 'spec_helper' - -describe SlackService::WikiPageMessage, models: true do - subject { described_class.new(args) } - - let(:args) do - { - user: { - name: 'Test User', - username: 'test.user' - }, - project_name: 'project_name', - project_url: 'somewhere.com', - object_attributes: { - title: 'Wiki page title', - url: 'url', - content: 'Wiki page description' - } - } - 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 in : '\ - '*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 in : '\ - '*Wiki page title*') - end - end - end - - describe '#attachments' do - let(:color) { '#345' } - - 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([ - { - text: "Wiki page description", - color: color, - } - ]) - 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([ - { - text: "Wiki page description", - color: color, - } - ]) - end - end - end -end diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb index c07a70a8069..4928391fd7e 100644 --- a/spec/models/project_services/slack_service_spec.rb +++ b/spec/models/project_services/slack_service_spec.rb @@ -1,327 +1,5 @@ require 'spec_helper' describe SlackService, models: true do - let(:slack) { SlackService.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) } - let(:username) { 'slack_username' } - let(:channel) { 'slack_channel' } - - let(:push_sample_data) do - Gitlab::DataBuilder::Push.build_sample(project, user) - end - - before do - allow(slack).to receive_messages( - project: project, - project_id: project.id, - service_hook: true, - webhook: webhook_url - ) - - WebMock.stub_request(:post, webhook_url) - - opts = { - title: 'Awesome issue', - description: 'please fix' - } - - issue_service = Issues::CreateService.new(project, user, opts) - @issue = issue_service.execute - @issues_sample_data = issue_service.hook_data(@issue, 'open') - - opts = { - title: 'Awesome merge_request', - description: 'please fix', - source_branch: 'feature', - target_branch: 'master' - } - merge_service = MergeRequests::CreateService.new(project, - user, opts) - @merge_request = merge_service.execute - @merge_sample_data = merge_service.hook_data(@merge_request, - 'open') - - opts = { - title: "Awesome wiki_page", - content: "Some text describing some thing or another", - format: "md", - message: "user created page: Awesome wiki_page" - } - - wiki_page_service = WikiPages::CreateService.new(project, user, opts) - @wiki_page = wiki_page_service.execute - @wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create') - end - - it "calls Slack API for push events" do - slack.execute(push_sample_data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - - it "calls Slack API for issue events" do - slack.execute(@issues_sample_data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - - it "calls Slack API for merge requests events" do - slack.execute(@merge_sample_data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - - it "calls Slack API for wiki page events" do - slack.execute(@wiki_page_sample_data) - - expect(WebMock).to have_requested(:post, webhook_url).once - end - - it 'uses the username as an option for slack when configured' do - allow(slack).to receive(:username).and_return(username) - expect(Slack::Notifier).to receive(:new). - with(webhook_url, username: username). - and_return( - double(:slack_service).as_null_object - ) - - slack.execute(push_sample_data) - end - - it 'uses the channel as an option when it is configured' do - allow(slack).to receive(:channel).and_return(channel) - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: channel). - and_return( - double(:slack_service).as_null_object - ) - slack.execute(push_sample_data) - end - - context "event channels" do - it "uses the right channel for push event" do - slack.update_attributes(push_channel: "random") - - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( - double(:slack_service).as_null_object - ) - - slack.execute(push_sample_data) - end - - it "uses the right channel for merge request event" do - slack.update_attributes(merge_request_channel: "random") - - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( - double(:slack_service).as_null_object - ) - - slack.execute(@merge_sample_data) - end - - it "uses the right channel for issue event" do - slack.update_attributes(issue_channel: "random") - - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( - double(:slack_service).as_null_object - ) - - slack.execute(@issues_sample_data) - end - - it "uses the right channel for wiki event" do - slack.update_attributes(wiki_page_channel: "random") - - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( - double(:slack_service).as_null_object - ) - - slack.execute(@wiki_page_sample_data) - end - - context "note event" do - let(:issue_note) do - create(:note_on_issue, project: project, note: "issue note") - end - - it "uses the right channel" do - slack.update_attributes(note_channel: "random") - - note_data = Gitlab::DataBuilder::Note.build(issue_note, user) - - expect(Slack::Notifier).to receive(:new). - with(webhook_url, channel: "random"). - and_return( - double(:slack_service).as_null_object - ) - - slack.execute(note_data) - end - end - end - end - - describe "Note events" do - let(:user) { create(:user) } - let(:project) { create(:project, creator_id: user.id) } - - before do - allow(slack).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 Slack API for commit comment events" do - data = Gitlab::DataBuilder::Note.build(commit_note, user) - slack.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 Slack API for merge request comment events" do - data = Gitlab::DataBuilder::Note.build(merge_request_note, user) - slack.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 Slack API for issue comment events" do - data = Gitlab::DataBuilder::Note.build(issue_note, user) - slack.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 Slack API for snippet comment events" do - data = Gitlab::DataBuilder::Note.build(snippet_note, user) - slack.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) } - - let(:pipeline) do - create(:ci_pipeline, - project: project, status: status, - sha: project.commit.sha, ref: project.default_branch) - end - - before do - allow(slack).to receive_messages( - project: project, - service_hook: true, - webhook: webhook_url - ) - end - - shared_examples 'call Slack API' do - before do - WebMock.stub_request(:post, webhook_url) - end - - it 'calls Slack API for pipeline events' do - data = Gitlab::DataBuilder::Pipeline.build(pipeline) - slack.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 Slack API' - end - - context 'with succeeded pipeline' do - let(:status) { 'success' } - - context 'with default to notify_only_broken_pipelines' do - it 'does not call Slack API for pipeline events' do - data = Gitlab::DataBuilder::Pipeline.build(pipeline) - result = slack.execute(data) - - expect(result).to be_falsy - end - end - - context 'with setting notify_only_broken_pipelines to false' do - before do - slack.notify_only_broken_pipelines = false - end - - it_behaves_like 'call Slack API' - end - end - end + it_behaves_like "slack or mattermost" end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 21ff238841e..1d8e42202ea 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -23,6 +23,7 @@ describe Project, models: true do it { is_expected.to have_many(:chat_services) } 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(:mattermost_service).dependent(:destroy) } it { is_expected.to have_one(:pushover_service).dependent(:destroy) } it { is_expected.to have_one(:asana_service).dependent(:destroy) } it { is_expected.to have_many(:boards).dependent(:destroy) } diff --git a/spec/support/slack_mattermost_shared_examples.rb b/spec/support/slack_mattermost_shared_examples.rb new file mode 100644 index 00000000000..56d4965f74d --- /dev/null +++ b/spec/support/slack_mattermost_shared_examples.rb @@ -0,0 +1,328 @@ +Dir[Rails.root.join("app/models/project_services/chat_message/*.rb")].each { |f| require f } + +RSpec.shared_examples 'slack or mattermost' 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) } + let(:username) { 'slack_username' } + let(:channel) { 'slack_channel' } + + let(:push_sample_data) do + Gitlab::DataBuilder::Push.build_sample(project, user) + end + + 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) + + opts = { + title: 'Awesome issue', + description: 'please fix' + } + + issue_service = Issues::CreateService.new(project, user, opts) + @issue = issue_service.execute + @issues_sample_data = issue_service.hook_data(@issue, 'open') + + opts = { + title: 'Awesome merge_request', + description: 'please fix', + source_branch: 'feature', + target_branch: 'master' + } + merge_service = MergeRequests::CreateService.new(project, + user, opts) + @merge_request = merge_service.execute + @merge_sample_data = merge_service.hook_data(@merge_request, + 'open') + + opts = { + title: "Awesome wiki_page", + content: "Some text describing some thing or another", + format: "md", + message: "user created page: Awesome wiki_page" + } + + wiki_page_service = WikiPages::CreateService.new(project, user, opts) + @wiki_page = wiki_page_service.execute + @wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create') + end + + it "calls Slack/Mattermost API for push events" do + chat_service.execute(push_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it "calls Slack/Mattermost API for issue events" do + chat_service.execute(@issues_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it "calls Slack/Mattermost API for merge requests events" do + chat_service.execute(@merge_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it "calls Slack/Mattermost API for wiki page events" do + chat_service.execute(@wiki_page_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it 'uses the username as an option for slack when configured' do + allow(chat_service).to receive(:username).and_return(username) + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, username: username, channel: chat_service.default_channel). + and_return( + double(:slack_service).as_null_object + ) + + chat_service.execute(push_sample_data) + end + + it 'uses the channel as an option when it is configured' do + allow(chat_service).to receive(:channel).and_return(channel) + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: channel). + and_return( + double(:slack_service).as_null_object + ) + chat_service.execute(push_sample_data) + end + + context "event channels" do + it "uses the right channel for push event" do + chat_service.update_attributes(push_channel: "random") + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + chat_service.execute(push_sample_data) + end + + it "uses the right channel for merge request event" do + chat_service.update_attributes(merge_request_channel: "random") + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + chat_service.execute(@merge_sample_data) + end + + it "uses the right channel for issue event" do + chat_service.update_attributes(issue_channel: "random") + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + chat_service.execute(@issues_sample_data) + end + + it "uses the right channel for wiki event" do + chat_service.update_attributes(wiki_page_channel: "random") + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + chat_service.execute(@wiki_page_sample_data) + end + + context "note event" do + let(:issue_note) do + create(:note_on_issue, project: project, note: "issue note") + end + + it "uses the right channel" do + chat_service.update_attributes(note_channel: "random") + + note_data = Gitlab::DataBuilder::Note.build(issue_note, user) + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + chat_service.execute(note_data) + end + end + end + end + + describe "Note events" do + let(:user) { create(:user) } + let(:project) { create(:project, creator_id: user.id) } + + 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 Slack/Mattermost 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 Slack 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 Slack 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 Slack 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) } + + 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 Slack/Mattermost API' do + before do + WebMock.stub_request(:post, webhook_url) + end + + it 'calls Slack/Mattermost 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 Slack/Mattermost API' + end + + context 'with succeeded pipeline' do + let(:status) { 'success' } + + context 'with default to notify_only_broken_pipelines' do + it 'does not call Slack/Mattermost 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 Slack/Mattermost API' + end + end + end +end -- cgit v1.2.1