Commit 52278412 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'zj-kamil-slack-slash-commands' into 'master'

Slack slash commands

## What does this MR do?

Implement Slack Slash Commands by utilizing generalized Mattermost presenter to fulfill Slack requirements.

## Why was this MR needed?

We want to expose Slack Slash Commands as a first-class service.

## What are the relevant issue numbers?

Supersedes !8007  
Closes #22182

See merge request !8126
parents 7572a314 e06f88ef
...@@ -169,7 +169,7 @@ gem 'gitlab-flowdock-git-hook', '~> 1.0.1' ...@@ -169,7 +169,7 @@ gem 'gitlab-flowdock-git-hook', '~> 1.0.1'
gem 'gemnasium-gitlab-service', '~> 0.2' gem 'gemnasium-gitlab-service', '~> 0.2'
# Slack integration # Slack integration
gem 'slack-notifier', '~> 1.2.0' gem 'slack-notifier', '~> 1.5.1'
# Asana integration # Asana integration
gem 'asana', '~> 0.4.0' gem 'asana', '~> 0.4.0'
......
...@@ -683,7 +683,7 @@ GEM ...@@ -683,7 +683,7 @@ GEM
json (>= 1.8, < 3) json (>= 1.8, < 3)
simplecov-html (~> 0.10.0) simplecov-html (~> 0.10.0)
simplecov-html (0.10.0) simplecov-html (0.10.0)
slack-notifier (1.2.1) slack-notifier (1.5.1)
slop (3.6.0) slop (3.6.0)
spinach (0.8.10) spinach (0.8.10)
colorize colorize
...@@ -952,7 +952,7 @@ DEPENDENCIES ...@@ -952,7 +952,7 @@ DEPENDENCIES
sidekiq-cron (~> 0.4.4) sidekiq-cron (~> 0.4.4)
sidekiq-limit_fetch (~> 3.4) sidekiq-limit_fetch (~> 3.4)
simplecov (= 0.12.0) simplecov (= 0.12.0)
slack-notifier (~> 1.2.0) slack-notifier (~> 1.5.1)
spinach-rails (~> 0.2.1) spinach-rails (~> 0.2.1)
spinach-rerun-reporter (~> 0.0.2) spinach-rerun-reporter (~> 0.0.2)
spring (~> 1.7.0) spring (~> 1.7.0)
......
...@@ -96,6 +96,10 @@ label { ...@@ -96,6 +96,10 @@ label {
code { code {
line-height: 1.8; line-height: 1.8;
} }
img {
margin-right: $gl-padding;
}
} }
@media(max-width: $screen-xs-max) { @media(max-width: $screen-xs-max) {
......
...@@ -79,7 +79,6 @@ class Project < ActiveRecord::Base ...@@ -79,7 +79,6 @@ class Project < ActiveRecord::Base
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event' has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
has_many :boards, before_add: :validate_board_limit, dependent: :destroy has_many :boards, before_add: :validate_board_limit, dependent: :destroy
has_many :chat_services
# Project services # Project services
has_one :campfire_service, dependent: :destroy has_one :campfire_service, dependent: :destroy
...@@ -96,6 +95,7 @@ class Project < ActiveRecord::Base ...@@ -96,6 +95,7 @@ class Project < ActiveRecord::Base
has_one :gemnasium_service, dependent: :destroy has_one :gemnasium_service, dependent: :destroy
has_one :mattermost_slash_commands_service, dependent: :destroy has_one :mattermost_slash_commands_service, dependent: :destroy
has_one :mattermost_notification_service, dependent: :destroy has_one :mattermost_notification_service, dependent: :destroy
has_one :slack_slash_commands_service, dependent: :destroy
has_one :slack_notification_service, dependent: :destroy has_one :slack_notification_service, dependent: :destroy
has_one :buildkite_service, dependent: :destroy has_one :buildkite_service, dependent: :destroy
has_one :bamboo_service, dependent: :destroy has_one :bamboo_service, dependent: :destroy
......
# Base class for Chat services # Base class for Chat services
# This class is not meant to be used directly, but only to inherit from. # This class is not meant to be used directly, but only to inherrit from.
class ChatService < Service class ChatSlashCommandsService < Service
default_value_for :category, 'chat' default_value_for :category, 'chat'
has_many :chat_names, foreign_key: :service_id prop_accessor :token
has_many :chat_names, foreign_key: :service_id, dependent: :destroy
def valid_token?(token) def valid_token?(token)
self.respond_to?(:token) && self.respond_to?(:token) &&
...@@ -15,7 +17,40 @@ class ChatService < Service ...@@ -15,7 +17,40 @@ class ChatService < Service
[] []
end end
def can_test?
false
end
def fields
[
{ type: 'text', name: 'token', placeholder: '' }
]
end
def trigger(params) def trigger(params)
raise NotImplementedError return unless valid_token?(params[:token])
user = find_chat_user(params)
unless user
url = authorize_chat_name_url(params)
return presenter.authorize_chat_name(url)
end
Gitlab::ChatCommands::Command.new(project, user,
params).execute
end
private
def find_chat_user(params)
ChatNames::FindUserService.new(self, params).execute
end
def authorize_chat_name_url(params)
ChatNames::AuthorizeUserService.new(self, params).execute
end
def presenter
Gitlab::ChatCommands::Presenter.new
end end
end end
class MattermostSlashCommandsService < ChatService class MattermostSlashCommandsService < ChatSlashCommandsService
include TriggersHelper include TriggersHelper
prop_accessor :token prop_accessor :token
...@@ -18,32 +18,4 @@ class MattermostSlashCommandsService < ChatService ...@@ -18,32 +18,4 @@ class MattermostSlashCommandsService < ChatService
def to_param def to_param
'mattermost_slash_commands' 'mattermost_slash_commands'
end end
def fields
[
{ type: 'text', name: 'token', placeholder: '' }
]
end
def trigger(params)
return nil unless valid_token?(params[:token])
user = find_chat_user(params)
unless user
url = authorize_chat_name_url(params)
return Mattermost::Presenter.authorize_chat_name(url)
end
Gitlab::ChatCommands::Command.new(project, user, params).execute
end
private
def find_chat_user(params)
ChatNames::FindUserService.new(self, params).execute
end
def authorize_chat_name_url(params)
ChatNames::AuthorizeUserService.new(self, params).execute
end
end end
class SlackSlashCommandsService < ChatSlashCommandsService
include TriggersHelper
def title
'Slack Command'
end
def description
"Perform common operations on GitLab in Slack"
end
def to_param
'slack_slash_commands'
end
def trigger(params)
# Format messages to be Slack-compatible
super.tap do |result|
result[:text] = format(result[:text])
end
end
private
def format(text)
Slack::Notifier::LinkFormatter.format(text) if text
end
end
...@@ -216,11 +216,12 @@ class Service < ActiveRecord::Base ...@@ -216,11 +216,12 @@ class Service < ActiveRecord::Base
jira jira
kubernetes kubernetes
mattermost_slash_commands mattermost_slash_commands
mattermost_notification
pipelines_email pipelines_email
pivotaltracker pivotaltracker
pushover pushover
redmine redmine
mattermost_notification slack_slash_commands
slack_notification slack_notification
teamcity teamcity
] ]
......
- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}"
.well
This service allows GitLab users to perform common operations on this
project by entering slash commands in Slack.
%br
See list of available commands in Slack after setting up this service,
by entering
%code /&lt;command&gt; help
%br
%br
To setup this service:
%ul.list-unstyled
%li
1.
= link_to 'Add a slash command', 'https://my.slack.com/services/new/slash-commands'
in your Slack team with these options:
%hr
.help-form
.form-group
= label_tag nil, 'Command', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block
%p Fill in the word that works best for your team.
%p
Suggestions:
%code= 'gitlab'
%code= @project.path # Path contains no spaces, but dashes
%code= @project.path_with_namespace
.form-group
= label_tag :url, 'URL', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#url')
.form-group
= label_tag nil, 'Method', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block POST
.form-group
= label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :customize_name, 'GitLab', class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#customize_name')
.form-group
= label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block
= image_tag(asset_url('gitlab_logo.png'), width: 36, height: 36)
= link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank')
.form-group
= label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block Show this command in the autocomplete list
.form-group
= label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#autocomplete_description')
.form-group
= label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#autocomplete_usage_hint')
.form-group
= label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#descriptive_label')
%hr
%ul.list-unstyled
%li
2. Paste the
%strong Token
into the field below
%li
3. Select the
%strong Active
checkbox, press
%strong Save changes
and start using GitLab inside Slack!
---
title: Refactor presenters ChatCommands
merge_request: 7846
author:
...@@ -37,11 +37,11 @@ Feature: Project Services ...@@ -37,11 +37,11 @@ Feature: Project Services
And I fill Assembla settings And I fill Assembla settings
Then I should see Assembla service settings saved Then I should see Assembla service settings saved
Scenario: Activate Slack service Scenario: Activate Slack notifications service
When I visit project "Shop" services page When I visit project "Shop" services page
And I click Slack service link And I click Slack notifications service link
And I fill Slack settings And I fill Slack notifications settings
Then I should see Slack service settings saved Then I should see Slack Notifications service settings saved
Scenario: Activate Pushover service Scenario: Activate Pushover service
When I visit project "Shop" services page When I visit project "Shop" services page
......
...@@ -137,17 +137,17 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps ...@@ -137,17 +137,17 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
expect(find_field('Colorize messages').value).to eq '1' expect(find_field('Colorize messages').value).to eq '1'
end end
step 'I click Slack service link' do step 'I click Slack notifications service link' do
click_link 'Slack' click_link 'Slack notifications'
end end
step 'I fill Slack settings' do step 'I fill Slack notifications settings' do
check 'Active' check 'Active'
fill_in 'Webhook', with: 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' fill_in 'Webhook', with: 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685'
click_button 'Save' click_button 'Save'
end end
step 'I should see Slack service settings saved' do step 'I should see Slack Notifications service settings saved' do
expect(find_field('Webhook').value).to eq 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' expect(find_field('Webhook').value).to eq 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685'
end end
......
...@@ -378,7 +378,6 @@ module API ...@@ -378,7 +378,6 @@ module API
desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)' desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)'
}, },
], ],
'mattermost-slash-commands' => [ 'mattermost-slash-commands' => [
{ {
required: true, required: true,
...@@ -387,6 +386,14 @@ module API ...@@ -387,6 +386,14 @@ module API
desc: 'The Mattermost token' desc: 'The Mattermost token'
} }
], ],
'slack-slash-commands' => [
{
required: true,
name: :token,
type: String,
desc: 'The Slack token'
}
],
'pipelines-email' => [ 'pipelines-email' => [
{ {
required: true, required: true,
......
...@@ -42,6 +42,10 @@ module Gitlab ...@@ -42,6 +42,10 @@ module Gitlab
def find_by_iid(iid) def find_by_iid(iid)
collection.find_by(iid: iid) collection.find_by(iid: iid)
end end
def presenter
Gitlab::ChatCommands::Presenter.new
end
end end
end end
end end
...@@ -22,8 +22,6 @@ module Gitlab ...@@ -22,8 +22,6 @@ module Gitlab
end end
end end
private
def match_command def match_command
match = nil match = nil
service = available_commands.find do |klass| service = available_commands.find do |klass|
...@@ -33,6 +31,8 @@ module Gitlab ...@@ -33,6 +31,8 @@ module Gitlab
[service, match] [service, match]
end end
private
def help_messages def help_messages
available_commands.map(&:help_message) available_commands.map(&:help_message)
end end
...@@ -48,15 +48,15 @@ module Gitlab ...@@ -48,15 +48,15 @@ module Gitlab
end end
def help(messages) def help(messages)
Mattermost::Presenter.help(messages, params[:command]) presenter.help(messages, params[:command])
end end
def access_denied def access_denied
Mattermost::Presenter.access_denied presenter.access_denied
end end
def present(resource) def present(resource)
Mattermost::Presenter.present(resource) presenter.present(resource)
end end
end end
end end
......
...@@ -4,7 +4,7 @@ module Gitlab ...@@ -4,7 +4,7 @@ module Gitlab
include Gitlab::Routing.url_helpers include Gitlab::Routing.url_helpers
def self.match(text) def self.match(text)
/\Adeploy\s+(?<from>.*)\s+to+\s+(?<to>.*)\z/.match(text) /\Adeploy\s+(?<from>\S+.*)\s+to+\s+(?<to>\S+.*)\z/.match(text)
end end
def self.help_message def self.help_message
......
module Mattermost module Gitlab
module ChatCommands
class Presenter class Presenter
class << self include Gitlab::Routing
include Gitlab::Routing.url_helpers
def authorize_chat_name(url) def authorize_chat_name(url)
message = if url message = if url
...@@ -64,7 +64,7 @@ module Mattermost ...@@ -64,7 +64,7 @@ module Mattermost
def single_resource(resource) def single_resource(resource)
return error(resource) if resource.errors.any? || !resource.persisted? return error(resource) if resource.errors.any? || !resource.persisted?
message = "### #{title(resource)}" message = "#{title(resource)}:"
message << "\n\n#{resource.description}" if resource.try(:description) message << "\n\n#{resource.description}" if resource.try(:description)
in_channel_response(message) in_channel_response(message)
......
...@@ -17,9 +17,9 @@ feature 'Admin updates settings', feature: true do ...@@ -17,9 +17,9 @@ feature 'Admin updates settings', feature: true do
expect(page).to have_content "Application settings saved successfully" expect(page).to have_content "Application settings saved successfully"
end end
scenario 'Change Slack Service template settings' do scenario 'Change Slack Notifications Service template settings' do
click_link 'Service Templates' click_link 'Service Templates'
click_link 'Slack' click_link 'Slack notifications'
fill_in 'Webhook', with: 'http://localhost' fill_in 'Webhook', with: 'http://localhost'
fill_in 'Username', with: 'test_user' fill_in 'Username', with: 'test_user'
fill_in 'service_push_channel', with: '#test_channel' fill_in 'service_push_channel', with: '#test_channel'
...@@ -30,7 +30,7 @@ feature 'Admin updates settings', feature: true do ...@@ -30,7 +30,7 @@ feature 'Admin updates settings', feature: true do
expect(page).to have_content 'Application settings saved successfully' expect(page).to have_content 'Application settings saved successfully'
click_link 'Slack' click_link 'Slack notifications'
page.all('input[type=checkbox]').each do |checkbox| page.all('input[type=checkbox]').each do |checkbox|
expect(checkbox).to be_checked expect(checkbox).to be_checked
......
require 'spec_helper'
feature 'Slack slash commands', feature: true do
include WaitForAjax
given(:user) { create(:user) }
given(:project) { create(:project) }
given(:service) { project.create_slack_slash_commands_service }
background do
project.team << [user, :master]
login_as(user)
end
scenario 'user visits the slack slash command config page and shows a help message', js: true do
visit edit_namespace_project_service_path(project.namespace, project, service)
wait_for_ajax
expect(page).to have_content('This service allows GitLab users to perform common')
end
scenario 'shows the token after saving' do
visit edit_namespace_project_service_path(project.namespace, project, service)
fill_in 'service_token', with: 'token'
click_on 'Save'
value = find_field('service_token').value
expect(value).to eq('token')
end
scenario 'shows the correct trigger url' do
visit edit_namespace_project_service_path(project.namespace, project, service)
value = find_field('url').value
expect(value).to match("api/v3/projects/#{project.id}/services/slack_slash_commands/trigger")
end
end
...@@ -5,7 +5,9 @@ describe Gitlab::ChatCommands::Command, service: true do ...@@ -5,7 +5,9 @@ describe Gitlab::ChatCommands::Command, service: true do
let(:user) { create(:user) } let(:user) { create(:user) }
describe '#execute' do describe '#execute' do
subject { described_class.new(project, user, params).execute } subject do
described_class.new(project, user, params).execute
end
context 'when no command is available' do context 'when no command is available' do
let(:params) { { text: 'issue show 1' } } let(:params) { { text: 'issue show 1' } }
...@@ -74,7 +76,7 @@ describe Gitlab::ChatCommands::Command, service: true do ...@@ -74,7 +76,7 @@ describe Gitlab::ChatCommands::Command, service: true do
end end
it 'returns action' do it 'returns action' do
expect(subject[:text]).to include('Deployment from staging to production started') expect(subject[:text]).to include('Deployment from staging to production started.')
expect(subject[:response_type]).to be(:in_channel) expect(subject[:response_type]).to be(:in_channel)
end end
...@@ -91,4 +93,26 @@ describe Gitlab::ChatCommands::Command, service: true do ...@@ -91,4 +93,26 @@ describe Gitlab::ChatCommands::Command, service: true do
end end
end end
end end
describe '#match_command' do
subject { described_class.new(project, user, params).match_command.first }
context 'IssueShow is triggered' do
let(:params) { { text: 'issue show 123' } }
it { is_expected.to eq(Gitlab::ChatCommands::IssueShow) }
end
context 'IssueCreate is triggered' do
let(:params) { { text: 'issue create my title' } }
it { is_expected.to eq(Gitlab::ChatCommands::IssueCreate) }
end
context 'IssueSearch is triggered' do
let(:params) { { text: 'issue search my query' } }
it { is_expected.to eq(Gitlab::ChatCommands::IssueSearch) }
end
end
end end
...@@ -129,6 +129,7 @@ project: ...@@ -129,6 +129,7 @@ project:
- builds_email_service - builds_email_service
- pipelines_email_service - pipelines_email_service
- mattermost_slash_commands_service - mattermost_slash_commands_service
- slack_slash_commands_service
- irker_service - irker_service
- pivotaltracker_service - pivotaltracker_service
- hipchat_service - hipchat_service
......
...@@ -10,7 +10,7 @@ describe ChatMessage::BuildMessage do ...@@ -10,7 +10,7 @@ describe ChatMessage::BuildMessage do
tag: false, tag: false,
project_name: 'project_name', project_name: 'project_name',
project_url: 'example.gitlab.com', project_url: 'http://example.gitlab.com',
commit: { commit: {
status: status, status: status,
...@@ -48,10 +48,10 @@ describe ChatMessage::BuildMessage do ...@@ -48,10 +48,10 @@ describe ChatMessage::BuildMessage do
end end
def build_message(status_text = status) def build_message(status_text = status)
"<example.gitlab.com|project_name>:" \ "<http://example.gitlab.com|project_name>:" \
" Commit <example.gitlab.com/commit/" \ " Commit <http://example.gitlab.com/commit/" \
"97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \ "97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \
" of <example.gitlab.com/commits/develop|develop> branch" \ " of <http://example.gitlab.com/commits/develop|develop> branch" \
" by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}" " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}"
end end
end end
...@@ -10,14 +10,14 @@ describe ChatMessage::IssueMessage, models: true do ...@@ -10,14 +10,14 @@ describe ChatMessage::IssueMessage, models: true do
username: 'test.user' username: 'test.user'
}, },
project_name: 'project_name', project_name: 'project_name',
project_url: 'somewhere.com', project_url: 'http://somewhere.com',
object_attributes: { object_attributes: {
title: 'Issue title', title: 'Issue title',
id: 10, id: 10,
iid: 100, iid: 100,
assignee_id: 1, assignee_id: 1,
url: 'url', url: 'http://url.com',
action: 'open', action: 'open',
state: 'opened', state: 'opened',
description: 'issue description' description: 'issue description'
...@@ -40,11 +40,11 @@ describe ChatMessage::IssueMessage, models: true do ...@@ -40,11 +40,11 @@ describe ChatMessage::IssueMessage, models: true do
context 'open' do context 'open' do
it 'returns a message regarding opening of issues' do it 'returns a message regarding opening of issues' do
expect(subject.pretext).to eq( expect(subject.pretext).to eq(
'<somewhere.com|[project_name>] Issue opened by test.user') '[<http://somewhere.com|project_name>] Issue opened by test.user')
expect(subject.attachments).to eq([ expect(subject.attachments).to eq([
{ {
title: "#100 Issue title", title: "#100 Issue title",
title_link: "url", title_link: "http://url.com",
text: "issue description", text: "issue description",
color: color, color: color,
} }
...@@ -60,7 +60,7 @@ describe ChatMessage::IssueMessage, models: true do ...@@ -60,7 +60,7 @@ describe ChatMessage::IssueMessage, models: true do
it 'returns a message regarding closing of issues' do it 'returns a message regarding closing of issues' do
expect(subject.pretext). to eq( expect(subject.pretext). to eq(
'<somewhere.com|[project_name>] Issue <url|#100 Issue title> closed by test.user') '[<http://somewhere.com|project_name>] Issue <http://url.com|#100 Issue title> closed by test.user')
expect(subject.attachments).to be_empty expect(subject.attachments).to be_empty
end end
end end
......
...@@ -10,14 +10,14 @@ describe ChatMessage::MergeMessage, models: true do ...@@ -10,14 +10,14 @@ describe ChatMessage::MergeMessage, models: true do
username: 'test.user' username: 'test.user'
}, },
project_name: 'project_name', project_name: 'project_name',
project_url: 'somewhere.com', project_url: 'http://somewhere.com',
object_attributes: { object_attributes: {
title: "Issue title\nSecond line", title: "Issue title\nSecond line",
id: 10, id: 10,
iid: 100, iid: 100,
assignee_id: 1, assignee_id: 1,
url: 'url', url: 'http://url.com',
state: 'opened', state: 'opened',
description: 'issue description', description: 'issue description',
source_branch: 'source_branch', source_branch: 'source_branch',
...@@ -31,8 +31,8 @@ describe ChatMessage::MergeMessage, models: true do ...@@ -31,8 +31,8 @@ describe ChatMessage::MergeMessage, models: true do
context 'open' do context 'open' do
it 'returns a message regarding opening of merge requests' do it 'returns a message regarding opening of merge requests' do
expect(subject.pretext).to eq( expect(subject.pretext).to eq(
'test.user opened <somewhere.com/merge_requests/100|merge request !100> '\ 'test.user opened <http://somewhere.com/merge_requests/100|merge request !100> '\
'in <somewhere.com|project_name>: *Issue title*') 'in <http://somewhere.com|project_name>: *Issue title*')
expect(subject.attachments).to be_empty expect(subject.attachments).to be_empty
end end
end end
...@@ -43,8 +43,8 @@ describe ChatMessage::MergeMessage, models: true do ...@@ -43,8 +43,8 @@ describe ChatMessage::MergeMessage, models: true do
end end
it 'returns a message regarding closing of merge requests' do it 'returns a message regarding closing of merge requests' do
expect(subject.pretext).to eq( expect(subject.pretext).to eq(
'test.user closed <somewhere.com/merge_requests/100|merge request !100> '\ 'test.user closed <http://somewhere.com/merge_requests/100|merge request !100> '\
'in <somewhere.com|project_name>: *Issue title*') 'in <http://somewhere.com|project_name>: *Issue title*')
expect(subject.attachments).to be_empty expect(subject.attachments).to be_empty
end end
end end
......
...@@ -11,15 +11,15 @@ describe ChatMessage::NoteMessage, models: true do ...@@ -11,15 +11,15 @@ describe ChatMessage::NoteMessage, models: true do
avatar_url: 'http://fakeavatar' avatar_url: 'http://fakeavatar'
}, },
project_name: 'project_name', project_name: 'project_name',
project_url: 'somewhere.com', project_url: 'http://somewhere.com',
repository: { repository: {
name: 'project_name', name: 'project_name',
url: 'somewhere.com', url: 'http://somewhere.com',
}, },
object_attributes: { object_attributes: {
id: 10, id: 10,
note: 'comment on a commit', note: 'comment on a commit',
url: 'url', url: 'http://url.com',
noteable_type: 'Commit' noteable_type: 'Commit'
} }
} }
...@@ -37,8 +37,8 @@ describe ChatMessage::NoteMessage, models: true do ...@@ -37,8 +37,8 @@ describe ChatMessage::NoteMessage, models: true do
it 'returns a message regarding notes on commits' do it 'returns a message regarding notes on commits' do
message = described_class.new(@args) message = described_class.new(@args)
expect(message.pretext).to eq("test.user <url|commented on " \ expect(message.pretext).to eq("test.user <http://url.com|commented on " \
"commit 5f163b2b> in <somewhere.com|project_name>: " \ "commit 5f163b2b> in <http://somewhere.com|project_name>: " \
"*Added a commit message*") "*Added a commit message*")
expected_attachments = [ expected_attachments = [
{ {
...@@ -63,8 +63,8 @@ describe ChatMessage::NoteMessage, models: true do ...@@ -63,8 +63,8 @@ describe ChatMessage::NoteMessage, models: true do
it 'returns a message regarding notes on a merge request' do it 'returns a message regarding notes on a merge request' do
message = described_class.new(@args) message = described_class.new(@args)
expect(message.pretext).to eq("test.user <url|commented on " \ expect(message.pretext).to eq("test.user <http://url.com|commented on " \
"merge request !30> in <somewhere.com|project_name>: " \ "merge request !30> in <http://somewhere.com|project_name>: " \
"*merge request title*") "*merge request title*")
expected_attachments = [ expected_attachments = [
{ {
...@@ -90,8 +90,8 @@ describe ChatMessage::NoteMessage, models: true do ...@@ -90,8 +90,8 @@ describe ChatMessage::NoteMessage, models: true do
it 'returns a message regarding notes on an issue' do it 'returns a message regarding notes on an issue' do
message = described_class.new(@args) message = described_class.new(@args)
expect(message.pretext).to eq( expect(message.pretext).to eq(
"test.user <url|commented on " \ "test.user <http://url.com|commented on " \
"issue #20> in <somewhere.com|project_name>: " \ "issue #20> in <http://somewhere.com|project_name>: " \
"*issue title*") "*issue title*")
expected_attachments = [ expected_attachments = [
{ {
...@@ -115,8 +115,8 @@ describe ChatMessage::NoteMessage, models: true do ...@@ -115,8 +115,8 @@ describe ChatMessage::NoteMessage, models: true do
it 'returns a message regarding notes on a project snippet' do it 'returns a message regarding notes on a project snippet' do
message = described_class.new(@args) message = described_class.new(@args)
expect(message.pretext).to eq("test.user <url|commented on " \ expect(message.pretext).to eq("test.user <http://url.com|commented on " \
"snippet #5> in <somewhere.com|project_name>: " \ "snippet #5> in <http://somewhere.com|project_name>: " \
"*snippet title*") "*snippet title*")
expected_attachments = [ expected_attachments = [
{ {
......
...@@ -15,7 +15,7 @@ describe ChatMessage::PipelineMessage do ...@@ -15,7 +15,7 @@ describe ChatMessage::PipelineMessage do
duration: duration duration: duration
}, },
project: { path_with_namespace: 'project_name', project: { path_with_namespace: 'project_name',
web_url: 'example.gitlab.com' }, web_url: 'http://example.gitlab.com' },
user: user user: user
} }
end end
...@@ -59,9 +59,9 @@ describe ChatMessage::PipelineMessage do ...@@ -59,9 +59,9 @@ describe ChatMessage::PipelineMessage do
end end
def build_message(status_text = status, name = user[:name]) def build_message(status_text = status, name = user[:name])
"<example.gitlab.com|project_name>:" \ "<http://example.gitlab.com|project_name>:" \
" Pipeline <example.gitlab.com/pipelines/123|#123>" \ " Pipeline <http://example.gitlab.com/pipelines/123|#123>" \
" of <example.gitlab.com/commits/develop|develop> branch" \ " of <http://example.gitlab.com/commits/develop|develop> branch" \
" by #{name} #{status_text} in #{duration} #{'second'.pluralize(duration)}" " by #{name} #{status_text} in #{duration} #{'second'.pluralize(duration)}"
end end
end end
...@@ -10,7 +10,7 @@ describe ChatMessage::PushMessage, models: true do ...@@ -10,7 +10,7 @@ describe ChatMessage::PushMessage, models: true do
project_name: 'project_name', project_name: 'project_name',
ref: 'refs/heads/master', ref: 'refs/heads/master',
user_name: 'test.user', user_name: 'test.user',
project_url: 'url' project_url: 'http://url.com'
} }
end end
...@@ -19,20 +19,20 @@ describe ChatMessage::PushMessage, models: true do ...@@ -19,20 +19,20 @@ describe ChatMessage::PushMessage, models: true do
context 'push' do context 'push' do
before do before do
args[:commits] = [ args[:commits] = [
{ message: 'message1', url: 'url1', id: 'abcdefghijkl', author: { name: 'author1' } }, { message: 'message1', url: 'http://url1.com', id: 'abcdefghijkl', author: { name: 'author1' } },
{ message: 'message2', url: 'url2', id: '123456789012', author: { name: 'author2' } }, { message: 'message2', url: 'http://url2.com', id: '123456789012', author: { name: 'author2' } },
] ]
end end
it 'returns a message regarding pushes' do it 'returns a message regarding pushes' do
expect(subject.pretext).to eq( expect(subject.pretext).to eq(
'test.user pushed to branch <url/commits/master|master> of '\ 'test.user pushed to branch <http://url.com/commits/master|master> of '\
'<url|project_name> (<url/compare/before...after|Compare changes>)' '<http://url.com|project_name> (<http://url.com/compare/before...after|Compare changes>)'
) )
expect(subject.attachments).to eq([ expect(subject.attachments).to eq([
{ {
text: "<url1|abcdefgh>: message1 - author1\n"\ text: "<http://url1.com|abcdefgh>: message1 - author1\n"\
"<url2|12345678>: message2 - author2", "<http://url2.com|12345678>: message2 - author2",
color: color, color: color,
} }
]) ])
...@@ -47,14 +47,14 @@ describe ChatMessage::PushMessage, models: true do ...@@ -47,14 +47,14 @@ describe ChatMessage::PushMessage, models: true do
project_name: 'project_name', project_name: 'project_name',
ref: 'refs/tags/new_tag', ref: 'refs/tags/new_tag',
user_name: 'test.user', user_name: 'test.user',
project_url: 'url' project_url: 'http://url.com'
} }
end end
it 'returns a message regarding pushes' do it 'returns a message regarding pushes' do
expect(subject.pretext).to eq('test.user pushed new tag ' \ expect(subject.pretext).to eq('test.user pushed new tag ' \
'<url/commits/new_tag|new_tag> to ' \ '<http://url.com/commits/new_tag|new_tag> to ' \
'<url|project_name>') '<http://url.com|project_name>')
expect(subject.attachments).to be_empty expect(subject.attachments).to be_empty
end end
end end
...@@ -66,8 +66,8 @@ describe ChatMessage::PushMessage, models: true do ...@@ -66,8 +66,8 @@ describe ChatMessage::PushMessage, models: true do
it 'returns a message regarding a new branch' do it 'returns a message regarding a new branch' do
expect(subject.pretext).to eq( expect(subject.pretext).to eq(
'test.user pushed new branch <url/commits/master|master> to '\ 'test.user pushed new branch <http://url.com/commits/master|master> to '\
'<url|project_name>' '<http://url.com|project_name>'
) )
expect(subject.attachments).to be_empty expect(subject.attachments).to be_empty
end end
...@@ -80,7 +80,7 @@ describe ChatMessage::PushMessage, models: true do ...@@ -80,7 +80,7 @@ describe ChatMessage::PushMessage, models: true do
it 'returns a message regarding a removed branch' do it 'returns a message regarding a removed branch' do
expect(subject.pretext).to eq( expect(subject.pretext).to eq(
'test.user removed branch master from <url|project_name>' 'test.user removed branch master from <http://url.com|project_name>'
) )
expect(subject.attachments).to be_empty expect(subject.attachments).to be_empty
end end
......
...@@ -10,10 +10,10 @@ describe ChatMessage::WikiPageMessage, models: true do ...@@ -10,10 +10,10 @@ describe ChatMessage::WikiPageMessage, models: true do
username: 'test.user' username: 'test.user'
}, },
project_name: 'project_name', project_name: 'project_name',
project_url: 'somewhere.com', project_url: 'http://somewhere.com',
object_attributes: { object_attributes: {
title: 'Wiki page title', title: 'Wiki page title',
url: 'url', url: 'http://url.com',
content: 'Wiki page description' content: 'Wiki page description'
} }
} }
...@@ -25,7 +25,7 @@ describe ChatMessage::WikiPageMessage, models: true do ...@@ -25,7 +25,7 @@ describe ChatMessage::WikiPageMessage, models: true do
it 'returns a message that a new wiki page was created' do it 'returns a message that a new wiki page was created' do
expect(subject.pretext).to eq( expect(subject.pretext).to eq(
'test.user created <url|wiki page> in <somewhere.com|project_name>: '\ 'test.user created <http://url.com|wiki page> in <http://somewhere.com|project_name>: '\
'*Wiki page title*') '*Wiki page title*')
end end
end end
...@@ -35,7 +35,7 @@ describe ChatMessage::WikiPageMessage, models: true do ...@@ -35,7 +35,7 @@ describe ChatMessage::WikiPageMessage, models: true do
it 'returns a message that a wiki page was updated' do it 'returns a message that a wiki page was updated' do
expect(subject.pretext).to eq( expect(subject.pretext).to eq(
'test.user edited <url|wiki page> in <somewhere.com|project_name>: '\ 'test.user edited <http://url.com|wiki page> in <http://somewhere.com|project_name>: '\
'*Wiki page title*') '*Wiki page title*')
end end
end end
......
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
end
end
require 'spec_helper' require 'spec_helper'
describe MattermostNotificationService, models: true do describe MattermostNotificationService, models: true do
it_behaves_like "slack or mattermost" it_behaves_like "slack or mattermost notifications"
end end
require 'spec_helper' require 'spec_helper'
describe MattermostSlashCommandsService, models: true do describe MattermostSlashCommandsService, :models do
describe "Associations" do it_behaves_like "chat slash commands service"
it { is_expected.to respond_to :token }
end
describe '#valid_token?' do
subject { described_class.new }
context 'when the token is empty' do
it 'is false' do
expect(subject.valid_token?('wer')).to be_falsey
end
end
context 'when there is a token' do
before do
subject.token = '123'
end
it 'accepts equal tokens' do
expect(subject.valid_token?('123')).to be_truthy
end
end
end
describe '#trigger' do
subject { described_class.new }
context 'no token is passed' do
let(:params) { Hash.new }
it 'returns nil' do
expect(subject.trigger(params)).to be_nil
end
end
context 'with a token passed' do
let(:project) { create(:empty_project) }
let(:params) { { token: 'token' } }
before do
allow(subject).to receive(:token).and_return('token')
end
context 'no user can be found' do
context 'when no url can be generated' do
it 'responds with the authorize url' do
response = subject.trigger(params)
expect(response[:response_type]).to eq :ephemeral
expect(response[:text]).to start_with ":sweat_smile: Couldn't identify you"
end
end
context 'when an auth url can be generated' do
let(:params) do
{
team_domain: 'http://domain.tld',
team_id: 'T3423423',
user_id: 'U234234',
user_name: 'mepmep',
token: 'token'
}
end
let(:service) do
project.create_mattermost_slash_commands_service(
properties: { token: 'token' }
)
end
it 'generates the url' do
response = service.trigger(params)
expect(response[:text]).to start_with(':wave: Hi there!')
end
end
end
context 'when the user is authenticated' do
let!(:chat_name) { create(:chat_name, service: service) }
let(:service) do
project.create_mattermost_slash_commands_service(
properties: { token: 'token' }
)
end
let(:params) { { token: 'token', team_id: chat_name.team_id, user_id: chat_name.chat_id } }
it 'triggers the command' do
expect_any_instance_of(Gitlab::ChatCommands::Command).to receive(:execute)
service.trigger(params)
end
end
end
end
end end
require 'spec_helper' require 'spec_helper'
describe SlackNotificationService, models: true do describe SlackNotificationService, models: true do
it_behaves_like "slack or mattermost" it_behaves_like "slack or mattermost notifications"
end end
require 'spec_helper'
describe SlackSlashCommandsService, :models do
it_behaves_like "chat slash commands service"
describe '#trigger' do
context 'when an auth url is generated' do
let(:project) { create(:empty_project) }
let(:params) do
{
team_domain: 'http://domain.tld',
team_id: 'T3423423',
user_id: 'U234234',
user_name: 'mepmep',
token: 'token'
}
end
let(:service) do
project.create_slack_slash_commands_service(
properties: { token: 'token' }
)
end
let(:authorize_url) do
'http://authorize.example.com/'
end
before do
allow(service).to receive(:authorize_chat_name_url).and_return(authorize_url)
end
it 'uses slack compatible links' do
response = service.trigger(params)
expect(response[:text]).to include("<#{authorize_url}|connect your GitLab account>")
end
end
end
end
...@@ -20,7 +20,6 @@ describe Project, models: true do ...@@ -20,7 +20,6 @@ describe Project, models: true do
it { is_expected.to have_many(:deploy_keys) } it { is_expected.to have_many(:deploy_keys) }
it { is_expected.to have_many(:hooks).dependent(:destroy) } it { is_expected.to have_many(:hooks).dependent(:destroy) }
it { is_expected.to have_many(:protected_branches).dependent(:destroy) } it { is_expected.to have_many(:protected_branches).dependent(:destroy) }
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(:forked_project_link).dependent(:destroy) }
it { is_expected.to have_one(:slack_notification_service).dependent(:destroy) } it { is_expected.to have_one(:slack_notification_service).dependent(:destroy) }
it { is_expected.to have_one(:mattermost_notification_service).dependent(:destroy) } it { is_expected.to have_one(:mattermost_notification_service).dependent(:destroy) }
...@@ -37,6 +36,7 @@ describe Project, models: true do ...@@ -37,6 +36,7 @@ describe Project, models: true do
it { is_expected.to have_one(:hipchat_service).dependent(:destroy) } it { is_expected.to have_one(:hipchat_service).dependent(:destroy) }
it { is_expected.to have_one(:flowdock_service).dependent(:destroy) } it { is_expected.to have_one(:flowdock_service).dependent(:destroy) }
it { is_expected.to have_one(:assembla_service).dependent(:destroy) } it { is_expected.to have_one(:assembla_service).dependent(:destroy) }
it { is_expected.to have_one(:slack_slash_commands_service).dependent(:destroy) }
it { is_expected.to have_one(:mattermost_slash_commands_service).dependent(:destroy) } it { is_expected.to have_one(:mattermost_slash_commands_service).dependent(:destroy) }
it { is_expected.to have_one(:gemnasium_service).dependent(:destroy) } it { is_expected.to have_one(:gemnasium_service).dependent(:destroy) }
it { is_expected.to have_one(:buildkite_service).dependent(:destroy) } it { is_expected.to have_one(:buildkite_service).dependent(:destroy) }
......
RSpec.shared_examples 'chat slash commands service' do
describe "Associations" do
it { is_expected.to respond_to :token }
it { is_expected.to have_many :chat_names }
end
describe '#valid_token?' do
subject { described_class.new }
context 'when the token is empty' do
it 'is false' do
expect(subject.valid_token?('wer')).to be_falsey
end
end
context 'when there is a token' do
before do
subject.token = '123'
end
it 'accepts equal tokens' do
expect(subject.valid_token?('123')).to be_truthy
end
end
end
describe '#trigger' do
subject { described_class.new }
context 'no token is passed' do
let(:params) { Hash.new }
it 'returns nil' do
expect(subject.trigger(params)).to be_nil
end
end
context 'with a token passed' do
let(:project) { create(:empty_project) }
let(:params) { { token: 'token' } }
before do
allow(subject).to receive(:token).and_return('token')
end
context 'no user can be found' do
context 'when no url can be generated' do
it 'responds with the authorize url' do
response = subject.trigger(params)
expect(response[:response_type]).to eq :ephemeral
expect(response[:text]).to start_with ":sweat_smile: Couldn't identify you"
end
end
context 'when an auth url can be generated' do
let(:params) do
{
team_domain: 'http://domain.tld',
team_id: 'T3423423',
user_id: 'U234234',
user_name: 'mepmep',
token: 'token'
}
end
let(:service) do
project.create_mattermost_slash_commands_service(
properties: { token: 'token' }
)
end
it 'generates the url' do
response = service.trigger(params)
expect(response[:text]).to start_with(':wave: Hi there!')
end
end
end
context 'when the user is authenticated' do
let!(:chat_name) { create(:chat_name, service: subject) }
let(:params) { { token: 'token', team_id: chat_name.team_id, user_id: chat_name.chat_id } }
subject do
described_class.create(project: project, properties: { token: 'token' })
end
it 'triggers the command' do
expect_any_instance_of(Gitlab::ChatCommands::Command).to receive(:execute)
subject.trigger(params)
end
end
end
end
end
Dir[Rails.root.join("app/models/project_services/chat_message/*.rb")].each { |f| require f } Dir[Rails.root.join("app/models/project_services/chat_message/*.rb")].each { |f| require f }
RSpec.shared_examples 'slack or mattermost' do RSpec.shared_examples 'slack or mattermost notifications' do
let(:chat_service) { described_class.new } let(:chat_service) { described_class.new }
let(:webhook_url) { 'https://example.gitlab.com/' } let(:webhook_url) { 'https://example.gitlab.com/' }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment