Merge branch 'zj-slash-commands-mattermost' into 'master'
Slash command for mattermost Closes #22540 ## Does this MR meet the acceptance criteria? - [x] [Changelog entry](https://docs.gitlab.com/ce/development/changelog.html) added - [ ] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md) - Tests - [x] Added for this feature/bug - [x] All builds are passing - [x] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html) - [x] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides) - [x] Branch has no merge conflicts with `master` (if it does - rebase it please) See merge request !7438
This commit is contained in:
commit
ffc5fc6a38
|
|
@ -28,6 +28,8 @@ class Projects::ServicesController < Projects::ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def test
|
def test
|
||||||
|
return render_404 unless @service.can_test?
|
||||||
|
|
||||||
data = @service.test_data(project, current_user)
|
data = @service.test_data(project, current_user)
|
||||||
outcome = @service.test(data)
|
outcome = @service.test(data)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,8 @@ module TriggersHelper
|
||||||
"#{Settings.gitlab.url}/api/v3/projects/#{project_id}/ref/#{ref}/trigger/builds"
|
"#{Settings.gitlab.url}/api/v3/projects/#{project_id}/ref/#{ref}/trigger/builds"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def service_trigger_url(service)
|
||||||
|
"#{Settings.gitlab.url}/api/v3/projects/#{service.project_id}/services/#{service.to_param}/trigger"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,9 @@ class Project < ActiveRecord::Base
|
||||||
|
|
||||||
cache_markdown_field :description, pipeline: :description
|
cache_markdown_field :description, pipeline: :description
|
||||||
|
|
||||||
delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true
|
delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
|
||||||
|
:merge_requests_enabled?, :issues_enabled?, to: :project_feature,
|
||||||
|
allow_nil: true
|
||||||
|
|
||||||
default_value_for :archived, false
|
default_value_for :archived, false
|
||||||
default_value_for :visibility_level, gitlab_config_features.visibility_level
|
default_value_for :visibility_level, gitlab_config_features.visibility_level
|
||||||
|
|
@ -75,6 +77,7 @@ 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
|
||||||
|
|
@ -89,6 +92,7 @@ class Project < ActiveRecord::Base
|
||||||
has_one :assembla_service, dependent: :destroy
|
has_one :assembla_service, dependent: :destroy
|
||||||
has_one :asana_service, dependent: :destroy
|
has_one :asana_service, dependent: :destroy
|
||||||
has_one :gemnasium_service, dependent: :destroy
|
has_one :gemnasium_service, dependent: :destroy
|
||||||
|
has_one :mattermost_slash_commands_service, dependent: :destroy
|
||||||
has_one :slack_service, dependent: :destroy
|
has_one :slack_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
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,10 @@ class ProjectFeature < ActiveRecord::Base
|
||||||
merge_requests_access_level > DISABLED
|
merge_requests_access_level > DISABLED
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def issues_enabled?
|
||||||
|
issues_access_level > DISABLED
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Validates builds and merge requests access level
|
# Validates builds and merge requests access level
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Base class for Chat services
|
||||||
|
# This class is not meant to be used directly, but only to inherrit from.
|
||||||
|
class ChatService < Service
|
||||||
|
default_value_for :category, 'chat'
|
||||||
|
|
||||||
|
has_many :chat_names, foreign_key: :service_id
|
||||||
|
|
||||||
|
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 trigger(params)
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
class MattermostSlashCommandsService < ChatService
|
||||||
|
include TriggersHelper
|
||||||
|
|
||||||
|
prop_accessor :token
|
||||||
|
|
||||||
|
def can_test?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def title
|
||||||
|
'Mattermost Command'
|
||||||
|
end
|
||||||
|
|
||||||
|
def description
|
||||||
|
"Perform common operations on GitLab in Mattermost"
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_param
|
||||||
|
'mattermost_slash_commands'
|
||||||
|
end
|
||||||
|
|
||||||
|
def help
|
||||||
|
"This service allows you to use slash commands with your Mattermost installation.<br/>
|
||||||
|
To setup this Service you need to create a new <b>Slash commands</b> in your Mattermost integration panel.<br/>
|
||||||
|
<br/>
|
||||||
|
Create integration with URL #{service_trigger_url(self)} and enter the token below."
|
||||||
|
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
|
||||||
|
|
@ -202,7 +202,6 @@ class Service < ActiveRecord::Base
|
||||||
bamboo
|
bamboo
|
||||||
buildkite
|
buildkite
|
||||||
builds_email
|
builds_email
|
||||||
pipelines_email
|
|
||||||
bugzilla
|
bugzilla
|
||||||
campfire
|
campfire
|
||||||
custom_issue_tracker
|
custom_issue_tracker
|
||||||
|
|
@ -214,6 +213,8 @@ class Service < ActiveRecord::Base
|
||||||
hipchat
|
hipchat
|
||||||
irker
|
irker
|
||||||
jira
|
jira
|
||||||
|
mattermost_slash_commands
|
||||||
|
pipelines_email
|
||||||
pivotaltracker
|
pivotaltracker
|
||||||
pushover
|
pushover
|
||||||
redmine
|
redmine
|
||||||
|
|
|
||||||
|
|
@ -10,26 +10,27 @@
|
||||||
.col-sm-10
|
.col-sm-10
|
||||||
= form.check_box :active
|
= form.check_box :active
|
||||||
|
|
||||||
.form-group
|
- if @service.supported_events.present?
|
||||||
= form.label :url, "Trigger", class: 'control-label'
|
.form-group
|
||||||
|
= form.label :url, "Trigger", class: 'control-label'
|
||||||
|
|
||||||
.col-sm-10
|
.col-sm-10
|
||||||
- @service.supported_events.each do |event|
|
- @service.supported_events.each do |event|
|
||||||
%div
|
%div
|
||||||
= form.check_box service_event_field_name(event), class: 'pull-left'
|
= form.check_box service_event_field_name(event), class: 'pull-left'
|
||||||
.prepend-left-20
|
.prepend-left-20
|
||||||
= form.label service_event_field_name(event), class: 'list-label' do
|
= form.label service_event_field_name(event), class: 'list-label' do
|
||||||
%strong
|
%strong
|
||||||
= event.humanize
|
= event.humanize
|
||||||
|
|
||||||
- field = @service.event_field(event)
|
- field = @service.event_field(event)
|
||||||
|
|
||||||
- if field
|
- if field
|
||||||
%p
|
%p
|
||||||
= form.text_field field[:name], class: "form-control", placeholder: field[:placeholder]
|
= form.text_field field[:name], class: "form-control", placeholder: field[:placeholder]
|
||||||
|
|
||||||
%p.light
|
%p.light
|
||||||
= service_event_description(event)
|
= service_event_description(event)
|
||||||
|
|
||||||
- @service.global_fields.each do |field|
|
- @service.global_fields.each do |field|
|
||||||
- type = field[:type]
|
- type = field[:type]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Added Mattermost slash command
|
||||||
|
merge_request: 7438
|
||||||
|
author:
|
||||||
|
|
@ -85,8 +85,8 @@ module API
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def project_service
|
def project_service(project = user_project)
|
||||||
@project_service ||= user_project.find_or_initialize_service(params[:service_slug].underscore)
|
@project_service ||= project.find_or_initialize_service(params[:service_slug].underscore)
|
||||||
@project_service || not_found!("Service")
|
@project_service || not_found!("Service")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
module API
|
module API
|
||||||
# Projects API
|
# Projects API
|
||||||
class Services < Grape::API
|
class Services < Grape::API
|
||||||
before { authenticate! }
|
|
||||||
before { authorize_admin_project }
|
|
||||||
|
|
||||||
resource :projects do
|
resource :projects do
|
||||||
|
before { authenticate! }
|
||||||
|
before { authorize_admin_project }
|
||||||
|
|
||||||
# Set <service_slug> service for project
|
# Set <service_slug> service for project
|
||||||
#
|
#
|
||||||
# Example Request:
|
# Example Request:
|
||||||
|
|
@ -59,5 +59,28 @@ module API
|
||||||
present project_service, with: Entities::ProjectService, include_passwords: current_user.is_admin?
|
present project_service, with: Entities::ProjectService, include_passwords: current_user.is_admin?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
resource :projects do
|
||||||
|
desc 'Trigger a slash command' do
|
||||||
|
detail 'Added in GitLab 8.13'
|
||||||
|
end
|
||||||
|
post ':id/services/:service_slug/trigger' do
|
||||||
|
project = Project.find_with_namespace(params[:id]) || Project.find_by(id: params[:id])
|
||||||
|
|
||||||
|
# This is not accurate, but done to prevent leakage of the project names
|
||||||
|
not_found!('Service') unless project
|
||||||
|
|
||||||
|
service = project_service(project)
|
||||||
|
|
||||||
|
result = service.try(:active?) && service.try(:trigger, params)
|
||||||
|
|
||||||
|
if result
|
||||||
|
status result[:status] || 200
|
||||||
|
present result
|
||||||
|
else
|
||||||
|
not_found!('Service')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
module Gitlab
|
||||||
|
module ChatCommands
|
||||||
|
class BaseCommand
|
||||||
|
QUERY_LIMIT = 5
|
||||||
|
|
||||||
|
def self.match(_text)
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.help_message
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.available?(_project)
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.allowed?(_user, _ability)
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.can?(object, action, subject)
|
||||||
|
Ability.allowed?(object, action, subject)
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute(_)
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
def collection
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_accessor :project, :current_user, :params
|
||||||
|
|
||||||
|
def initialize(project, user, params = {})
|
||||||
|
@project, @current_user, @params = project, user, params.dup
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def find_by_iid(iid)
|
||||||
|
resource = collection.find_by(iid: iid)
|
||||||
|
|
||||||
|
readable?(resource) ? resource : nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
module Gitlab
|
||||||
|
module ChatCommands
|
||||||
|
class Command < BaseCommand
|
||||||
|
COMMANDS = [
|
||||||
|
Gitlab::ChatCommands::IssueShow,
|
||||||
|
Gitlab::ChatCommands::IssueCreate,
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
def execute
|
||||||
|
command, match = match_command
|
||||||
|
|
||||||
|
if command
|
||||||
|
if command.allowed?(project, current_user)
|
||||||
|
present command.new(project, current_user, params).execute(match)
|
||||||
|
else
|
||||||
|
access_denied
|
||||||
|
end
|
||||||
|
else
|
||||||
|
help(help_messages)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def match_command
|
||||||
|
match = nil
|
||||||
|
service = available_commands.find do |klass|
|
||||||
|
match = klass.match(command)
|
||||||
|
end
|
||||||
|
|
||||||
|
[service, match]
|
||||||
|
end
|
||||||
|
|
||||||
|
def help_messages
|
||||||
|
available_commands.map(&:help_message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def available_commands
|
||||||
|
COMMANDS.select do |klass|
|
||||||
|
klass.available?(project)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def command
|
||||||
|
params[:text]
|
||||||
|
end
|
||||||
|
|
||||||
|
def help(messages)
|
||||||
|
Mattermost::Presenter.help(messages, params[:command])
|
||||||
|
end
|
||||||
|
|
||||||
|
def access_denied
|
||||||
|
Mattermost::Presenter.access_denied
|
||||||
|
end
|
||||||
|
|
||||||
|
def present(resource)
|
||||||
|
Mattermost::Presenter.present(resource)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
module Gitlab
|
||||||
|
module ChatCommands
|
||||||
|
class IssueCommand < BaseCommand
|
||||||
|
def self.available?(project)
|
||||||
|
project.issues_enabled? && project.default_issues_tracker?
|
||||||
|
end
|
||||||
|
|
||||||
|
def collection
|
||||||
|
project.issues
|
||||||
|
end
|
||||||
|
|
||||||
|
def readable?(issue)
|
||||||
|
self.class.can?(current_user, :read_issue, issue)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
module Gitlab
|
||||||
|
module ChatCommands
|
||||||
|
class IssueCreate < IssueCommand
|
||||||
|
def self.match(text)
|
||||||
|
/\Aissue\s+create\s+(?<title>[^\n]*)\n*(?<description>.*)\z/.match(text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.help_message
|
||||||
|
'issue create <title>\n<description>'
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.allowed?(project, user)
|
||||||
|
can?(user, :create_issue, project)
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute(match)
|
||||||
|
title = match[:title]
|
||||||
|
description = match[:description]
|
||||||
|
|
||||||
|
Issues::CreateService.new(project, current_user, title: title, description: description).execute
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
module Gitlab
|
||||||
|
module ChatCommands
|
||||||
|
class IssueShow < IssueCommand
|
||||||
|
def self.match(text)
|
||||||
|
/\Aissue\s+show\s+(?<iid>\d+)/.match(text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.help_message
|
||||||
|
"issue show <id>"
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute(match)
|
||||||
|
find_by_iid(match[:iid])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
module Mattermost
|
||||||
|
class Presenter
|
||||||
|
class << self
|
||||||
|
include Gitlab::Routing.url_helpers
|
||||||
|
|
||||||
|
def authorize_chat_name(url)
|
||||||
|
message = if url
|
||||||
|
":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{url})."
|
||||||
|
else
|
||||||
|
":sweat_smile: Couldn't identify you, nor can I autorize you!"
|
||||||
|
end
|
||||||
|
|
||||||
|
ephemeral_response(message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def help(commands, trigger)
|
||||||
|
if commands.none?
|
||||||
|
ephemeral_response("No commands configured")
|
||||||
|
else
|
||||||
|
commands.map! { |command| "#{trigger} #{command}" }
|
||||||
|
message = header_with_list("Available commands", commands)
|
||||||
|
|
||||||
|
ephemeral_response(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def present(resource)
|
||||||
|
return not_found unless resource
|
||||||
|
|
||||||
|
if resource.respond_to?(:count)
|
||||||
|
if resource.count > 1
|
||||||
|
return multiple_resources(resource)
|
||||||
|
elsif resource.count == 0
|
||||||
|
return not_found
|
||||||
|
else
|
||||||
|
resource = resource.first
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
single_resource(resource)
|
||||||
|
end
|
||||||
|
|
||||||
|
def access_denied
|
||||||
|
ephemeral_response("Whoops! That action is not allowed. This incident will be [reported](https://xkcd.com/838/).")
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def not_found
|
||||||
|
ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:")
|
||||||
|
end
|
||||||
|
|
||||||
|
def single_resource(resource)
|
||||||
|
return error(resource) if resource.errors.any? || !resource.persisted?
|
||||||
|
|
||||||
|
message = "### #{title(resource)}"
|
||||||
|
message << "\n\n#{resource.description}" if resource.description
|
||||||
|
|
||||||
|
in_channel_response(message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def multiple_resources(resources)
|
||||||
|
resources.map! { |resource| title(resource) }
|
||||||
|
|
||||||
|
message = header_with_list("Multiple results were found:", resources)
|
||||||
|
|
||||||
|
ephemeral_response(message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def error(resource)
|
||||||
|
message = header_with_list("The action was not successful, because:", resource.errors.messages)
|
||||||
|
|
||||||
|
ephemeral_response(message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def title(resource)
|
||||||
|
"[#{resource.to_reference} #{resource.title}](#{url(resource)})"
|
||||||
|
end
|
||||||
|
|
||||||
|
def header_with_list(header, items)
|
||||||
|
message = [header]
|
||||||
|
|
||||||
|
items.each do |item|
|
||||||
|
message << "- #{item}"
|
||||||
|
end
|
||||||
|
|
||||||
|
message.join("\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
def url(resource)
|
||||||
|
url_for(
|
||||||
|
[
|
||||||
|
resource.project.namespace.becomes(Namespace),
|
||||||
|
resource.project,
|
||||||
|
resource
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def ephemeral_response(message)
|
||||||
|
{
|
||||||
|
response_type: :ephemeral,
|
||||||
|
text: message,
|
||||||
|
status: 200
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def in_channel_response(message)
|
||||||
|
{
|
||||||
|
response_type: :in_channel,
|
||||||
|
text: message,
|
||||||
|
status: 200
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Gitlab::ChatCommands::Command, service: true do
|
||||||
|
let(:project) { create(:empty_project) }
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
|
subject { described_class.new(project, user, params).execute }
|
||||||
|
|
||||||
|
describe '#execute' do
|
||||||
|
context 'when no command is available' do
|
||||||
|
let(:params) { { text: 'issue show 1' } }
|
||||||
|
let(:project) { create(:project, has_external_issue_tracker: true) }
|
||||||
|
|
||||||
|
it 'displays 404 messages' do
|
||||||
|
expect(subject[:response_type]).to be(:ephemeral)
|
||||||
|
expect(subject[:text]).to start_with('404 not found')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when an unknown command is triggered' do
|
||||||
|
let(:params) { { command: '/gitlab', text: "unknown command 123" } }
|
||||||
|
|
||||||
|
it 'displays the help message' do
|
||||||
|
expect(subject[:response_type]).to be(:ephemeral)
|
||||||
|
expect(subject[:text]).to start_with('Available commands')
|
||||||
|
expect(subject[:text]).to match('/gitlab issue show')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'the user can not create an issue' do
|
||||||
|
let(:params) { { text: "issue create my new issue" } }
|
||||||
|
|
||||||
|
it 'rejects the actions' do
|
||||||
|
expect(subject[:response_type]).to be(:ephemeral)
|
||||||
|
expect(subject[:text]).to start_with('Whoops! That action is not allowed')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'issue is successfully created' do
|
||||||
|
let(:params) { { text: "issue create my new issue" } }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.team << [user, :master]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'presents the issue' do
|
||||||
|
expect(subject[:text]).to match("my new issue")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows a link to the new issue' do
|
||||||
|
expect(subject[:text]).to match(/\/issues\/\d+/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Gitlab::ChatCommands::IssueCreate, service: true do
|
||||||
|
describe '#execute' do
|
||||||
|
let(:project) { create(:empty_project) }
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
let(:regex_match) { described_class.match("issue create bird is the word") }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.team << [user, :master]
|
||||||
|
end
|
||||||
|
|
||||||
|
subject do
|
||||||
|
described_class.new(project, user).execute(regex_match)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without description' do
|
||||||
|
it 'creates the issue' do
|
||||||
|
expect { subject }.to change { project.issues.count }.by(1)
|
||||||
|
|
||||||
|
expect(subject.title).to eq('bird is the word')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with description' do
|
||||||
|
let(:description) { "Surfin bird" }
|
||||||
|
let(:regex_match) { described_class.match("issue create bird is the word\n#{description}") }
|
||||||
|
|
||||||
|
it 'creates the issue with description' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(Issue.last.description).to eq(description)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.match' do
|
||||||
|
it 'matches the title without description' do
|
||||||
|
match = described_class.match("issue create my title")
|
||||||
|
|
||||||
|
expect(match[:title]).to eq('my title')
|
||||||
|
expect(match[:description]).to eq("")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'matches the title with description' do
|
||||||
|
match = described_class.match("issue create my title\n\ndescription")
|
||||||
|
|
||||||
|
expect(match[:title]).to eq('my title')
|
||||||
|
expect(match[:description]).to eq('description')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Gitlab::ChatCommands::IssueShow, service: true do
|
||||||
|
describe '#execute' do
|
||||||
|
let(:issue) { create(:issue) }
|
||||||
|
let(:project) { issue.project }
|
||||||
|
let(:user) { issue.author }
|
||||||
|
let(:regex_match) { described_class.match("issue show #{issue.iid}") }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.team << [user, :master]
|
||||||
|
end
|
||||||
|
|
||||||
|
subject do
|
||||||
|
described_class.new(project, user).execute(regex_match)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'the issue exists' do
|
||||||
|
it 'returns the issue' do
|
||||||
|
expect(subject.iid).to be issue.iid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'the issue does not exist' do
|
||||||
|
let(:regex_match) { described_class.match("issue show 2343242") }
|
||||||
|
|
||||||
|
it "returns nil" do
|
||||||
|
expect(subject).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'self.match' do
|
||||||
|
it 'matches the iid' do
|
||||||
|
match = described_class.match("issue show 123")
|
||||||
|
|
||||||
|
expect(match[:iid]).to eq("123")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -116,6 +116,7 @@ project:
|
||||||
- base_tags
|
- base_tags
|
||||||
- tag_taggings
|
- tag_taggings
|
||||||
- tags
|
- tags
|
||||||
|
- chat_services
|
||||||
- creator
|
- creator
|
||||||
- group
|
- group
|
||||||
- namespace
|
- namespace
|
||||||
|
|
@ -127,6 +128,7 @@ project:
|
||||||
- emails_on_push_service
|
- emails_on_push_service
|
||||||
- builds_email_service
|
- builds_email_service
|
||||||
- pipelines_email_service
|
- pipelines_email_service
|
||||||
|
- mattermost_slash_commands_service
|
||||||
- irker_service
|
- irker_service
|
||||||
- pivotaltracker_service
|
- pivotaltracker_service
|
||||||
- hipchat_service
|
- hipchat_service
|
||||||
|
|
@ -188,4 +190,4 @@ award_emoji:
|
||||||
- awardable
|
- awardable
|
||||||
- user
|
- user
|
||||||
priorities:
|
priorities:
|
||||||
- label
|
- label
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
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
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe MattermostSlashCommandsService, models: true do
|
||||||
|
describe "Associations" do
|
||||||
|
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
|
||||||
|
|
@ -20,6 +20,7 @@ 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_service).dependent(:destroy) }
|
it { is_expected.to have_one(:slack_service).dependent(:destroy) }
|
||||||
it { is_expected.to have_one(:pushover_service).dependent(:destroy) }
|
it { is_expected.to have_one(:pushover_service).dependent(:destroy) }
|
||||||
|
|
@ -35,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(: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) }
|
||||||
it { is_expected.to have_one(:bamboo_service).dependent(:destroy) }
|
it { is_expected.to have_one(:bamboo_service).dependent(:destroy) }
|
||||||
|
|
|
||||||
|
|
@ -88,4 +88,61 @@ describe API::API, api: true do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'POST /projects/:id/services/:slug/trigger' do
|
||||||
|
let!(:project) { create(:empty_project) }
|
||||||
|
let(:service_name) { 'mattermost_slash_commands' }
|
||||||
|
|
||||||
|
context 'no service is available' do
|
||||||
|
it 'returns a not found message' do
|
||||||
|
post api("/projects/#{project.id}/services/idonotexist/trigger")
|
||||||
|
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
expect(json_response["message"]).to eq("404 Service Not Found")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'the service exists' do
|
||||||
|
let(:params) { { token: 'token' } }
|
||||||
|
|
||||||
|
context 'the service is not active' do
|
||||||
|
let!(:inactive_service) do
|
||||||
|
project.create_mattermost_slash_commands_service(
|
||||||
|
active: false,
|
||||||
|
properties: { token: 'token' }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'when the service is inactive' do
|
||||||
|
post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger")
|
||||||
|
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'the service is active' do
|
||||||
|
let!(:active_service) do
|
||||||
|
project.create_mattermost_slash_commands_service(
|
||||||
|
active: true,
|
||||||
|
properties: { token: 'token' }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'retusn status 200' do
|
||||||
|
post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger"), params
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the project can not be found' do
|
||||||
|
it 'returns a generic 404' do
|
||||||
|
post api("/projects/404/services/mattermost_slash_commands/trigger"), params
|
||||||
|
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
expect(json_response["message"]).to eq("404 Service Not Found")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ describe ChatNames::FindUserService, services: true do
|
||||||
context 'when existing user is requested' do
|
context 'when existing user is requested' do
|
||||||
let(:params) { { team_id: chat_name.team_id, user_id: chat_name.chat_id } }
|
let(:params) { { team_id: chat_name.team_id, user_id: chat_name.chat_id } }
|
||||||
|
|
||||||
it 'returns existing user' do
|
it 'returns the existing user' do
|
||||||
is_expected.to eq(user)
|
is_expected.to eq(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue