Commit dc6921bd authored by Z.J. van de Weg's avatar Z.J. van de Weg

Chat Commands have presenters

This improves the styling and readability of the code. This is supported
by both Mattermost and Slack.
parent b525aff6
...@@ -28,20 +28,24 @@ class ChatSlashCommandsService < Service ...@@ -28,20 +28,24 @@ class ChatSlashCommandsService < Service
end end
def trigger(params) def trigger(params)
return unless valid_token?(params[:token]) return access_presenter unless valid_token?(params[:token])
user = find_chat_user(params) user = find_chat_user(params)
unless user
if user
Gitlab::ChatCommands::Command.new(project, user, params).execute
else
url = authorize_chat_name_url(params) url = authorize_chat_name_url(params)
return presenter.authorize_chat_name(url) access_presenter(url).authorize
end end
Gitlab::ChatCommands::Command.new(project, user,
params).execute
end end
private private
def access_presenter(url = nil)
Gitlab::ChatCommands::Presenters::Access.new(url)
end
def find_chat_user(params) def find_chat_user(params)
ChatNames::FindUserService.new(self, params).execute ChatNames::FindUserService.new(self, params).execute
end end
...@@ -49,8 +53,4 @@ class ChatSlashCommandsService < Service ...@@ -49,8 +53,4 @@ class ChatSlashCommandsService < Service
def authorize_chat_name_url(params) def authorize_chat_name_url(params)
ChatNames::AuthorizeUserService.new(self, params).execute ChatNames::AuthorizeUserService.new(self, params).execute
end end
def presenter
Gitlab::ChatCommands::Presenter.new
end
end end
...@@ -42,10 +42,6 @@ module Gitlab ...@@ -42,10 +42,6 @@ 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
...@@ -13,9 +13,9 @@ module Gitlab ...@@ -13,9 +13,9 @@ module Gitlab
if command if command
if command.allowed?(project, current_user) if command.allowed?(project, current_user)
present command.new(project, current_user, params).execute(match) command.new(project, current_user, params).execute(match)
else else
access_denied Gitlab::ChatCommands::Presenters::Access.new.access_denied
end end
else else
help(help_messages) help(help_messages)
...@@ -25,7 +25,7 @@ module Gitlab ...@@ -25,7 +25,7 @@ module Gitlab
def match_command def match_command
match = nil match = nil
service = available_commands.find do |klass| service = available_commands.find do |klass|
match = klass.match(command) match = klass.match(params[:text])
end end
[service, match] [service, match]
...@@ -42,22 +42,6 @@ module Gitlab ...@@ -42,22 +42,6 @@ module Gitlab
klass.available?(project) klass.available?(project)
end end
end end
def command
params[:text]
end
def help(messages)
presenter.help(messages, params[:command])
end
def access_denied
presenter.access_denied
end
def present(resource)
presenter.present(resource)
end
end end
end end
end end
module Gitlab module Gitlab
module ChatCommands module ChatCommands
class Deploy < BaseCommand class Deploy < BaseCommand
include Gitlab::Routing.url_helpers
def self.match(text) def self.match(text)
/\Adeploy\s+(?<from>\S+.*)\s+to+\s+(?<to>\S+.*)\z/.match(text) /\Adeploy\s+(?<from>\S+.*)\s+to+\s+(?<to>\S+.*)\z/.match(text)
end end
...@@ -24,35 +22,29 @@ module Gitlab ...@@ -24,35 +22,29 @@ module Gitlab
to = match[:to] to = match[:to]
actions = find_actions(from, to) actions = find_actions(from, to)
return unless actions.present?
if actions.one? if actions.none?
play!(from, to, actions.first) Gitlab::ChatCommands::Presenters::Deploy.new(nil).no_actions
elsif actions.one?
action = play!(from, to, actions.first)
Gitlab::ChatCommands::Presenters::Deploy.new(action).present(from, to)
else else
Result.new(:error, 'Too many actions defined') Gitlab::ChatCommands::Presenters::Deploy.new(actions).too_many_actions
end end
end end
private private
def play!(from, to, action) def play!(from, to, action)
new_action = action.play(current_user) action.play(current_user)
Result.new(:success, "Deployment from #{from} to #{to} started. Follow the progress: #{url(new_action)}.")
end end
def find_actions(from, to) def find_actions(from, to)
environment = project.environments.find_by(name: from) environment = project.environments.find_by(name: from)
return unless environment return [] unless environment
environment.actions_for(to).select(&:starts_environment?) environment.actions_for(to).select(&:starts_environment?)
end end
def url(subject)
polymorphic_url(
[subject.project.namespace.becomes(Namespace), subject.project, subject]
)
end
end end
end end
end end
...@@ -2,7 +2,7 @@ module Gitlab ...@@ -2,7 +2,7 @@ module Gitlab
module ChatCommands module ChatCommands
class IssueCreate < IssueCommand class IssueCreate < IssueCommand
def self.match(text) def self.match(text)
# we can not match \n with the dot by passing the m modifier as than # we can not match \n with the dot by passing the m modifier as than
# the title and description are not seperated # the title and description are not seperated
/\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text) /\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text)
end end
...@@ -19,8 +19,24 @@ module Gitlab ...@@ -19,8 +19,24 @@ module Gitlab
title = match[:title] title = match[:title]
description = match[:description].to_s.rstrip description = match[:description].to_s.rstrip
issue = create_issue(title: title, description: description)
if issue.errors.any?
presenter(issue).display_errors
else
presenter(issue).present
end
end
private
def create_issue(title:, description:)
Issues::CreateService.new(project, current_user, title: title, description: description).execute Issues::CreateService.new(project, current_user, title: title, description: description).execute
end end
def presenter(issue)
Gitlab::ChatCommands::Presenters::ShowIssue.new(issue)
end
end end
end end
end end
...@@ -10,7 +10,15 @@ module Gitlab ...@@ -10,7 +10,15 @@ module Gitlab
end end
def execute(match) def execute(match)
collection.search(match[:query]).limit(QUERY_LIMIT) issues = collection.search(match[:query]).limit(QUERY_LIMIT)
if issues.none?
Presenters::Access.new(issues).not_found
elsif issues.one?
Presenters::ShowIssue.new(issues.first).present
else
Presenters::ListIssues.new(issues).present
end
end end
end end
end end
......
...@@ -10,7 +10,13 @@ module Gitlab ...@@ -10,7 +10,13 @@ module Gitlab
end end
def execute(match) def execute(match)
find_by_iid(match[:iid]) issue = find_by_iid(match[:iid])
if issue
Gitlab::ChatCommands::Presenters::ShowIssue.new(issue).present
else
Gitlab::ChatCommands::Presenters::Access.new.not_found
end
end end
end end
end end
......
module Gitlab
module ChatCommands
class Presenter
include Gitlab::Routing
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(subject)
return not_found unless subject
if subject.is_a?(Gitlab::ChatCommands::Result)
show_result(subject)
elsif subject.respond_to?(:count)
if subject.none?
not_found
elsif subject.one?
single_resource(subject.first)
else
multiple_resources(subject)
end
else
single_resource(subject)
end
end
def access_denied
ephemeral_response("Whoops! That action is not allowed. This incident will be [reported](https://xkcd.com/838/).")
end
private
def show_result(result)
case result.type
when :success
in_channel_response(result.message)
else
ephemeral_response(result.message)
end
end
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.try(:description)
in_channel_response(message)
end
def multiple_resources(resources)
titles = resources.map { |resource| title(resource) }
message = header_with_list("Multiple results were found:", titles)
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)
reference = resource.try(:to_reference) || resource.try(:id)
title = resource.try(:title) || resource.try(:name)
"[#{reference} #{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
module Gitlab::ChatCommands::Presenters
class Access < Gitlab::ChatCommands::Presenters::Base
def access_denied
ephemeral_response(text: "Whoops! This action is not allowed. This incident will be [reported](https://xkcd.com/838/).")
end
def not_found
ephemeral_response(text: "404 not found! GitLab couldn't find what you were looking for! :boom:")
end
def authorize
message =
if @resource
":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{@resource})."
else
":sweat_smile: Couldn't identify you, nor can I autorize you!"
end
ephemeral_response(text: message)
end
end
end
module Gitlab::ChatCommands::Presenters
class Base
include Gitlab::Routing.url_helpers
def initialize(resource = nil)
@resource = resource
end
def display_errors
message = header_with_list("The action was not successful, because:", @resource.errors.full_messages)
ephemeral_response(text: message)
end
private
def header_with_list(header, items)
message = [header]
items.each do |item|
message << "- #{item}"
end
message.join("\n")
end
def ephemeral_response(message)
response = {
response_type: :ephemeral,
status: 200
}.merge(message)
format_response(response)
end
def in_channel_response(message)
response = {
response_type: :in_channel,
status: 200
}.merge(message)
format_response(response)
end
def format_response(response)
response[:text] = format(response[:text]) if response.has_key?(:text)
if response.has_key?(:attachments)
response[:attachments].each do |attachment|
attachment[:pretext] = format(attachment[:pretext]) if attachment[:pretext]
attachment[:text] = format(attachment[:text]) if attachment[:text]
end
end
response
end
# Convert Markdown to slacks format
def format(string)
Slack::Notifier::LinkFormatter.format(string)
end
def resource_url
url_for(
[
@resource.project.namespace.becomes(Namespace),
@resource.project,
@resource
]
)
end
end
end
module Gitlab::ChatCommands::Presenters
class Deploy < Gitlab::ChatCommands::Presenters::Base
def present(from, to)
message = "Deployment started from #{from} to #{to}. [Follow its progress](#{resource_url})."
in_channel_response(text: message)
end
def no_actions
ephemeral_response(text: "No action found to be executed")
end
def too_many_actions
ephemeral_response(text: "Too many actions defined")
end
private
def resource_url
polymorphic_url(
[ @resource.project.namespace.becomes(Namespace), @resource.project, @resource]
)
end
end
end
module Gitlab::ChatCommands::Presenters
class Issuable < Gitlab::ChatCommands::Presenters::Base
private
def project
@resource.project
end
def author
@resource.author
end
def fields
[
{
title: "Assignee",
value: @resource.assignee ? @resource.assignee.name : "_None_",
short: true
},
{
title: "Milestone",
value: @resource.milestone ? @resource.milestone.title : "_None_",
short: true
},
{
title: "Labels",
value: @resource.labels.any? ? @resource.label_names : "_None_",
short: true
}
]
end
end
end
module Gitlab::ChatCommands::Presenters
class ListIssues < Gitlab::ChatCommands::Presenters::Base
def present
ephemeral_response(text: "Here are the issues I found:", attachments: attachments)
end
private
def attachments
@resource.map do |issue|
state = issue.open? ? "Open" : "Closed"
{
fallback: "Issue #{issue.to_reference}: #{issue.title}",
color: "#d22852",
text: "[#{issue.to_reference}](#{url_for([namespace, project, issue])}) · #{issue.title} (#{state})",
mrkdwn_in: [
"text"
]
}
end
end
def project
@project ||= @resource.first.project
end
def namespace
@namespace ||= project.namespace.becomes(Namespace)
end
end
end
module Gitlab::ChatCommands::Presenters
class ShowIssue < Gitlab::ChatCommands::Presenters::Issuable
def present
in_channel_response(show_issue)
end
private
def show_issue
{
attachments: [
{
title: @resource.title,
title_link: resource_url,
author_name: author.name,
author_icon: author.avatar_url,
fallback: "#{@resource.to_reference}: #{@resource.title}",
text: text,
fields: fields,
mrkdwn_in: [
:title,
:text
]
}
]
}
end
def text
message = ""
message << ":+1: #{@resource.upvotes} " unless @resource.upvotes.zero?
message << ":-1: #{@resource.downvotes} " unless @resource.downvotes.zero?
message << ":speech_balloon: #{@resource.user_notes_count}" unless @resource.user_notes_count.zero?
message
end
end
end
module Mattermost
class Error < StandardError; end
end
module Mattermost
class NoSessionError < Mattermost::Error
def message
'No session could be set up, is Mattermost configured with Single Sign On?'
end
end
class ConnectionError < Mattermost::Error; end
# This class' prime objective is to obtain a session token on a Mattermost
# instance with SSO configured where this GitLab instance is the provider.
#
# The process depends on OAuth, but skips a step in the authentication cycle.
# For example, usually a user would click the 'login in GitLab' button on
# Mattermost, which would yield a 302 status code and redirects you to GitLab
# to approve the use of your account on Mattermost. Which would trigger a
# callback so Mattermost knows this request is approved and gets the required
# data to create the user account etc.
#
# This class however skips the button click, and also the approval phase to
# speed up the process and keep it without manual action and get a session
# going.
class Session
include Doorkeeper::Helpers::Controller
include HTTParty
LEASE_TIMEOUT = 60
base_uri Settings.mattermost.host
attr_accessor :current_resource_owner, :token
def initialize(current_user)
@current_resource_owner = current_user
end
def with_session
with_lease do
raise Mattermost::NoSessionError unless create
begin
yield self
rescue Errno::ECONNREFUSED
raise Mattermost::NoSessionError
ensure
destroy
end
end
end
# Next methods are needed for Doorkeeper
def pre_auth
@pre_auth ||= Doorkeeper::OAuth::PreAuthorization.new(
Doorkeeper.configuration, server.client_via_uid, params)
end
def authorization
@authorization ||= strategy.request
end
def strategy
@strategy ||= server.authorization_request(pre_auth.response_type)
end
def request
@request ||= OpenStruct.new(parameters: params)
end
def params
Rack::Utils.parse_query(oauth_uri.query).symbolize_keys
end
def get(path, options = {})
handle_exceptions do
self.class.get(path, options.merge(headers: @headers))
end
end
def post(path, options = {})
handle_exceptions do
self.class.post(path, options.merge(headers: @headers))
end
end
private
def create
return unless oauth_uri
return unless token_uri
@token = request_token
@headers = {
Authorization: "Bearer #{@token}"
}
@token
end
def destroy
post('/api/v3/users/logout')
end
def oauth_uri
return @oauth_uri if defined?(@oauth_uri)
@oauth_uri = nil
response = get("/api/v3/oauth/gitlab/login", follow_redirects: false)
return unless 300 <= response.code && response.code < 400
redirect_uri = response.headers['location']
return unless redirect_uri
@oauth_uri = URI.parse(redirect_uri)
end
def token_uri
@token_uri ||=
if oauth_uri
authorization.authorize.redirect_uri if pre_auth.authorizable?
end
end
def request_token
response = get(token_uri, follow_redirects: false)
if 200 <= response.code && response.code < 400
response.headers['token']
end
end
def with_lease
lease_uuid = lease_try_obtain
raise NoSessionError unless lease_uuid
begin
yield
ensure
Gitlab::ExclusiveLease.cancel(lease_key, lease_uuid)
end
end
def lease_key
"mattermost:session"
end
def lease_try_obtain
lease = ::Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
lease.try_obtain
end
def handle_exceptions
yield
rescue HTTParty::Error => e
raise Mattermost::ConnectionError.new(e.message)
rescue Errno::ECONNREFUSED
raise Mattermost::ConnectionError.new(e.message)
end
end
end
...@@ -5,6 +5,7 @@ describe Gitlab::ChatCommands::Command, service: true do ...@@ -5,6 +5,7 @@ describe Gitlab::ChatCommands::Command, service: true do
let(:user) { create(:user) } let(:user) { create(:user) }
describe '#execute' do describe '#execute' do
<<<<<<< HEAD
subject do subject do
described_class.new(project, user, params).execute described_class.new(project, user, params).execute
end end
...@@ -18,6 +19,9 @@ describe Gitlab::ChatCommands::Command, service: true do ...@@ -18,6 +19,9 @@ describe Gitlab::ChatCommands::Command, service: true do
expect(subject[:text]).to start_with('404 not found') expect(subject[:text]).to start_with('404 not found')
end end
end end
=======
subject { described_class.new(project, user, params).execute }
>>>>>>> Chat Commands have presenters
context 'when an unknown command is triggered' do context 'when an unknown command is triggered' do
let(:params) { { command: '/gitlab', text: "unknown command 123" } } let(:params) { { command: '/gitlab', text: "unknown command 123" } }
...@@ -34,47 +38,7 @@ describe Gitlab::ChatCommands::Command, service: true do ...@@ -34,47 +38,7 @@ describe Gitlab::ChatCommands::Command, service: true do
it 'rejects the actions' do it 'rejects the actions' do
expect(subject[:response_type]).to be(:ephemeral) expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to start_with('Whoops! That action is not allowed') expect(subject[:text]).to start_with('Whoops! This 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
context 'searching for an issue' do
let(:params) { { text: 'issue search find me' } }
let!(:issue) { create(:issue, project: project, title: 'find me') }
before do
project.team << [user, :master]
end
context 'a single issue is found' do
it 'presents the issue' do
expect(subject[:text]).to match(issue.title)
end
end
context 'multiple issues found' do
let!(:issue2) { create(:issue, project: project, title: "someone find me") }
it 'shows a link to the new issue' do
expect(subject[:text]).to match(issue.title)
expect(subject[:text]).to match(issue2.title)
end
end end
end end
...@@ -90,7 +54,7 @@ describe Gitlab::ChatCommands::Command, service: true do ...@@ -90,7 +54,7 @@ describe Gitlab::ChatCommands::Command, service: true do
context 'and user can not create deployment' do context 'and user can not create deployment' do
it 'returns action' do it 'returns action' do
expect(subject[:response_type]).to be(:ephemeral) expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to start_with('Whoops! That action is not allowed') expect(subject[:text]).to start_with('Whoops! This action is not allowed')
end end
end end
...@@ -100,7 +64,7 @@ describe Gitlab::ChatCommands::Command, service: true do ...@@ -100,7 +64,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 started from staging to production')
expect(subject[:response_type]).to be(:in_channel) expect(subject[:response_type]).to be(:in_channel)
end end
......
...@@ -15,8 +15,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do ...@@ -15,8 +15,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do
end end
context 'if no environment is defined' do context 'if no environment is defined' do
it 'returns nil' do it 'does not execute an action' do
expect(subject).to be_nil expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to eq("No action found to be executed")
end end
end end
...@@ -26,8 +27,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do ...@@ -26,8 +27,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do
let!(:deployment) { create(:deployment, environment: staging, deployable: build) } let!(:deployment) { create(:deployment, environment: staging, deployable: build) }
context 'without actions' do context 'without actions' do
it 'returns nil' do it 'does not execute an action' do
expect(subject).to be_nil expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to eq("No action found to be executed")
end end
end end
...@@ -37,8 +39,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do ...@@ -37,8 +39,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do
end end
it 'returns success result' do it 'returns success result' do
expect(subject.type).to eq(:success) expect(subject[:response_type]).to be(:in_channel)
expect(subject.message).to include('Deployment from staging to production started') expect(subject[:text]).to start_with('Deployment started from staging to production')
end end
context 'when duplicate action exists' do context 'when duplicate action exists' do
...@@ -47,8 +49,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do ...@@ -47,8 +49,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do
end end
it 'returns error' do it 'returns error' do
expect(subject.type).to eq(:error) expect(subject[:response_type]).to be(:ephemeral)
expect(subject.message).to include('Too many actions defined') expect(subject[:text]).to eq('Too many actions defined')
end end
end end
...@@ -59,9 +61,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do ...@@ -59,9 +61,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do
name: 'teardown', environment: 'production') name: 'teardown', environment: 'production')
end end
it 'returns success result' do it 'returns the success message' do
expect(subject.type).to eq(:success) expect(subject[:response_type]).to be(:in_channel)
expect(subject.message).to include('Deployment from staging to production started') expect(subject[:text]).to start_with('Deployment started from staging to production')
end end
end end
end end
......
...@@ -18,7 +18,7 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do ...@@ -18,7 +18,7 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do
it 'creates the issue' do it 'creates the issue' do
expect { subject }.to change { project.issues.count }.by(1) expect { subject }.to change { project.issues.count }.by(1)
expect(subject.title).to eq('bird is the word') expect(subject[:response_type]).to be(:in_channel)
end end
end end
...@@ -41,6 +41,16 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do ...@@ -41,6 +41,16 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do
expect { subject }.to change { project.issues.count }.by(1) expect { subject }.to change { project.issues.count }.by(1)
end end
end end
context 'issue cannot be created' do
let!(:issue) { create(:issue, project: project, title: 'bird is the word') }
let(:regex_match) { described_class.match("issue create #{'a' * 512}}") }
it 'displays the errors' do
expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to match("- Title is too long")
end
end
end end
describe '.match' do describe '.match' do
......
...@@ -2,9 +2,9 @@ require 'spec_helper' ...@@ -2,9 +2,9 @@ require 'spec_helper'
describe Gitlab::ChatCommands::IssueSearch, service: true do describe Gitlab::ChatCommands::IssueSearch, service: true do
describe '#execute' do describe '#execute' do
let!(:issue) { create(:issue, title: 'find me') } let!(:issue) { create(:issue, project: project, title: 'find me') }
let!(:confidential) { create(:issue, :confidential, project: project, title: 'mepmep find') } let!(:confidential) { create(:issue, :confidential, project: project, title: 'mepmep find') }
let(:project) { issue.project } let(:project) { create(:empty_project) }
let(:user) { issue.author } let(:user) { issue.author }
let(:regex_match) { described_class.match("issue search find") } let(:regex_match) { described_class.match("issue search find") }
...@@ -14,7 +14,8 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do ...@@ -14,7 +14,8 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do
context 'when the user has no access' do context 'when the user has no access' do
it 'only returns the open issues' do it 'only returns the open issues' do
expect(subject).not_to include(confidential) expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to match("not found")
end end
end end
...@@ -24,13 +25,14 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do ...@@ -24,13 +25,14 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do
end end
it 'returns all results' do it 'returns all results' do
expect(subject).to include(confidential, issue) expect(subject).to have_key(:attachments)
expect(subject[:text]).to match("Here are the issues I found:")
end end
end end
context 'without hits on the query' do context 'without hits on the query' do
it 'returns an empty collection' do it 'returns an empty collection' do
expect(subject).to be_empty expect(subject[:text]).to match("not found")
end end
end end
end end
......
...@@ -2,8 +2,8 @@ require 'spec_helper' ...@@ -2,8 +2,8 @@ require 'spec_helper'
describe Gitlab::ChatCommands::IssueShow, service: true do describe Gitlab::ChatCommands::IssueShow, service: true do
describe '#execute' do describe '#execute' do
let(:issue) { create(:issue) } let(:issue) { create(:issue, project: project) }
let(:project) { issue.project } let(:project) { create(:empty_project) }
let(:user) { issue.author } let(:user) { issue.author }
let(:regex_match) { described_class.match("issue show #{issue.iid}") } let(:regex_match) { described_class.match("issue show #{issue.iid}") }
...@@ -16,15 +16,19 @@ describe Gitlab::ChatCommands::IssueShow, service: true do ...@@ -16,15 +16,19 @@ describe Gitlab::ChatCommands::IssueShow, service: true do
end end
context 'the issue exists' do context 'the issue exists' do
let(:title) { subject[:attachments].first[:title] }
it 'returns the issue' do it 'returns the issue' do
expect(subject.iid).to be issue.iid expect(subject[:response_type]).to be(:in_channel)
expect(title).to eq(issue.title)
end end
context 'when its reference is given' do context 'when its reference is given' do
let(:regex_match) { described_class.match("issue show #{issue.to_reference}") } let(:regex_match) { described_class.match("issue show #{issue.to_reference}") }
it 'shows the issue' do it 'shows the issue' do
expect(subject.iid).to be issue.iid expect(subject[:response_type]).to be(:in_channel)
expect(title).to eq(issue.title)
end end
end end
end end
...@@ -32,17 +36,24 @@ describe Gitlab::ChatCommands::IssueShow, service: true do ...@@ -32,17 +36,24 @@ describe Gitlab::ChatCommands::IssueShow, service: true do
context 'the issue does not exist' do context 'the issue does not exist' do
let(:regex_match) { described_class.match("issue show 2343242") } let(:regex_match) { described_class.match("issue show 2343242") }
it "returns nil" do it "returns not found" do
expect(subject).to be_nil expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to match("not found")
end end
end end
end end
describe 'self.match' do describe '.match' do
it 'matches the iid' do it 'matches the iid' do
match = described_class.match("issue show 123") match = described_class.match("issue show 123")
expect(match[:iid]).to eq("123") expect(match[:iid]).to eq("123")
end end
it 'accepts a reference' do
match = described_class.match("issue show #{Issue.reference_prefix}123")
expect(match[:iid]).to eq("123")
end
end end
end end
require 'spec_helper'
describe Gitlab::ChatCommands::Presenters::Access do
describe '#access_denied' do
subject { described_class.new.access_denied }
it { is_expected.to be_a(Hash) }
it 'displays an error message' do
expect(subject[:text]).to match("is not allowed")
expect(subject[:response_type]).to be(:ephemeral)
end
end
describe '#not_found' do
subject { described_class.new.not_found }
it { is_expected.to be_a(Hash) }
it 'tells the user the resource was not found' do
expect(subject[:text]).to match("not found!")
expect(subject[:response_type]).to be(:ephemeral)
end
end
describe '#authorize' do
context 'with an authorization URL' do
subject { described_class.new('http://authorize.me').authorize }
it { is_expected.to be_a(Hash) }
it 'tells the user to authorize' do
expect(subject[:text]).to match("connect your GitLab account")
expect(subject[:response_type]).to be(:ephemeral)
end
end
context 'without authorization url' do
subject { described_class.new.authorize }
it { is_expected.to be_a(Hash) }
it 'tells the user to authorize' do
expect(subject[:text]).to match("Couldn't identify you")
expect(subject[:response_type]).to be(:ephemeral)
end
end
end
end
require 'spec_helper'
describe Gitlab::ChatCommands::Presenters::Deploy do
let(:build) { create(:ci_build) }
describe '#present' do
subject { described_class.new(build).present('staging', 'prod') }
it { is_expected.to have_key(:text) }
it { is_expected.to have_key(:response_type) }
it { is_expected.to have_key(:status) }
it { is_expected.not_to have_key(:attachments) }
it 'messages the channel of the deploy' do
expect(subject[:response_type]).to be(:in_channel)
expect(subject[:text]).to start_with("Deployment started from staging to prod")
end
end
describe '#no_actions' do
subject { described_class.new(nil).no_actions }
it { is_expected.to have_key(:text) }
it { is_expected.to have_key(:response_type) }
it { is_expected.to have_key(:status) }
it { is_expected.not_to have_key(:attachments) }
it 'tells the user there is no action' do
expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to eq("No action found to be executed")
end
end
describe '#too_many_actions' do
subject { described_class.new(nil).too_many_actions }
it { is_expected.to have_key(:text) }
it { is_expected.to have_key(:response_type) }
it { is_expected.to have_key(:status) }
it { is_expected.not_to have_key(:attachments) }
it 'tells the user there is no action' do
expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to eq("Too many actions defined")
end
end
end
require 'spec_helper'
describe Gitlab::ChatCommands::Presenters::ListIssues do
let(:project) { create(:empty_project) }
let(:message) { subject[:text] }
let(:issue) { project.issues.first }
before { create_list(:issue, 2, project: project) }
subject { described_class.new(project.issues).present }
it do
is_expected.to have_key(:text)
is_expected.to have_key(:status)
is_expected.to have_key(:response_type)
is_expected.to have_key(:attachments)
end
it 'shows a list of results' do
expect(subject[:response_type]).to be(:ephemeral)
expect(message).to start_with("Here are the issues I found")
end
end
require 'spec_helper'
describe Gitlab::ChatCommands::Presenters::ShowIssue do
let(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
let(:attachment) { subject[:attachments].first }
subject { described_class.new(issue).present }
it { is_expected.to be_a(Hash) }
it 'shows the issue' do
expect(subject[:response_type]).to be(:in_channel)
expect(subject).to have_key(:attachments)
expect(attachment[:title]).to eq(issue.title)
end
context 'with upvotes' do
before do
create(:award_emoji, :upvote, awardable: issue)
end
it 'shows the upvote count' do
expect(attachment[:text]).to start_with(":+1: 1")
end
end
end
require 'spec_helper'
describe Mattermost::Client do
let(:user) { build(:user) }
subject { described_class.new(user) }
context 'JSON parse error' do
before do
Struct.new("Request", :body, :success?)
end
it 'yields an error on malformed JSON' do
bad_json = Struct::Request.new("I'm not json", true)
expect { subject.send(:json_response, bad_json) }.to raise_error(Mattermost::ClientError)
end
it 'shows a client error if the request was unsuccessful' do
bad_request = Struct::Request.new("true", false)
expect { subject.send(:json_response, bad_request) }.to raise_error(Mattermost::ClientError)
end
end
end
require 'spec_helper'
describe Mattermost::Command do
let(:params) { { 'token' => 'token', team_id: 'abc' } }
before do
Mattermost::Session.base_uri('http://mattermost.example.com')
allow_any_instance_of(Mattermost::Client).to receive(:with_session).
and_yield(Mattermost::Session.new(nil))
end
describe '#create' do
let(:params) do
{ team_id: 'abc',
trigger: 'gitlab'
}
end
subject { described_class.new(nil).create(params) }
context 'for valid trigger word' do
before do
stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create').
with(body: {
team_id: 'abc',
trigger: 'gitlab' }.to_json).
to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: { token: 'token' }.to_json
)
end
it 'returns a token' do
is_expected.to eq('token')
end
end
context 'for error message' do
before do
stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create').
to_return(
status: 500,
headers: { 'Content-Type' => 'application/json' },
body: {
id: 'api.command.duplicate_trigger.app_error',
message: 'This trigger word is already in use. Please choose another word.',
detailed_error: '',
request_id: 'obc374man7bx5r3dbc1q5qhf3r',
status_code: 500
}.to_json
)
end
it 'raises an error with message' do
expect { subject }.to raise_error(Mattermost::Error, 'This trigger word is already in use. Please choose another word.')
end
end
end
end
require 'spec_helper'
describe Mattermost::Session, type: :request do
let(:user) { create(:user) }
let(:gitlab_url) { "http://gitlab.com" }
let(:mattermost_url) { "http://mattermost.com" }
subject { described_class.new(user) }
# Needed for doorkeeper to function
it { is_expected.to respond_to(:current_resource_owner) }
it { is_expected.to respond_to(:request) }
it { is_expected.to respond_to(:authorization) }
it { is_expected.to respond_to(:strategy) }
before do
described_class.base_uri(mattermost_url)
end
describe '#with session' do
let(:location) { 'http://location.tld' }
let!(:stub) do
WebMock.stub_request(:get, "#{mattermost_url}/api/v3/oauth/gitlab/login").
to_return(headers: { 'location' => location }, status: 307)
end
context 'without oauth uri' do
it 'makes a request to the oauth uri' do
expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
end
end
context 'with oauth_uri' do
let!(:doorkeeper) do
Doorkeeper::Application.create(
name: "GitLab Mattermost",
redirect_uri: "#{mattermost_url}/signup/gitlab/complete\n#{mattermost_url}/login/gitlab/complete",
scopes: "")
end
context 'without token_uri' do
it 'can not create a session' do
expect do
subject.with_session
end.to raise_error(Mattermost::NoSessionError)
end
end
context 'with token_uri' do
let(:state) { "state" }
let(:params) do
{ response_type: "code",
client_id: doorkeeper.uid,
redirect_uri: "#{mattermost_url}/signup/gitlab/complete",
state: state }
end
let(:location) do
"#{gitlab_url}/oauth/authorize?#{URI.encode_www_form(params)}"
end
before do
WebMock.stub_request(:get, "#{mattermost_url}/signup/gitlab/complete").
with(query: hash_including({ 'state' => state })).
to_return do |request|
post "/oauth/token",
client_id: doorkeeper.uid,
client_secret: doorkeeper.secret,
redirect_uri: params[:redirect_uri],
grant_type: 'authorization_code',
code: request.uri.query_values['code']
if response.status == 200
{ headers: { 'token' => 'thisworksnow' }, status: 202 }
end
end
WebMock.stub_request(:post, "#{mattermost_url}/api/v3/users/logout").
to_return(headers: { Authorization: 'token thisworksnow' }, status: 200)
end
it 'can setup a session' do
subject.with_session do |session|
end
expect(subject.token).not_to be_nil
end
it 'returns the value of the block' do
result = subject.with_session do |session|
"value"
end
expect(result).to eq("value")
end
end
end
context 'with lease' do
before do
allow(subject).to receive(:lease_try_obtain).and_return('aldkfjsldfk')
end
it 'tries to obtain a lease' do
expect(subject).to receive(:lease_try_obtain)
expect(Gitlab::ExclusiveLease).to receive(:cancel)
# Cannot setup a session, but we should still cancel the lease
expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
end
end
context 'without lease' do
before do
allow(subject).to receive(:lease_try_obtain).and_return(nil)
end
it 'returns a NoSessionError error' do
expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
end
end
end
end
require 'spec_helper'
describe Mattermost::Team do
before do
Mattermost::Session.base_uri('http://mattermost.example.com')
allow_any_instance_of(Mattermost::Client).to receive(:with_session).
and_yield(Mattermost::Session.new(nil))
end
describe '#all' do
subject { described_class.new(nil).all }
context 'for valid request' do
let(:response) do
[{
"id" => "xiyro8huptfhdndadpz8r3wnbo",
"create_at" => 1482174222155,
"update_at" => 1482174222155,
"delete_at" => 0,
"display_name" => "chatops",
"name" => "chatops",
"email" => "admin@example.com",
"type" => "O",
"company_name" => "",
"allowed_domains" => "",
"invite_id" => "o4utakb9jtb7imctdfzbf9r5ro",
"allow_open_invite" => false }]
end
before do
stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all').
to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: response.to_json
)
end
it 'returns a token' do
is_expected.to eq(response)
end
end
context 'for error message' do
before do
stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all').
to_return(
status: 500,
headers: { 'Content-Type' => 'application/json' },
body: {
id: 'api.team.list.app_error',
message: 'Cannot list teams.',
detailed_error: '',
request_id: 'obc374man7bx5r3dbc1q5qhf3r',
status_code: 500
}.to_json
)
end
it 'raises an error with message' do
expect { subject }.to raise_error(Mattermost::Error, 'Cannot list teams.')
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment