From 09f1a9e320306677d0a94c8f8d7b58c3024c3ed7 Mon Sep 17 00:00:00 2001 From: Allison Whilden Date: Sat, 21 Jan 2017 17:00:55 -0800 Subject: [ci skip] UX guide: Update animation guidance to 100ms --- doc/development/ux_guide/animation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/development/ux_guide/animation.md b/doc/development/ux_guide/animation.md index 903e54bf9dc..5dae4bcc905 100644 --- a/doc/development/ux_guide/animation.md +++ b/doc/development/ux_guide/animation.md @@ -19,7 +19,7 @@ Easing specifies the rate of change of a parameter over time (see [easings.net]( ### Hover -Interactive elements (links, buttons, etc.) should have a hover state. A subtle animation for this transition adds a polished feel. We should target a `200ms linear` transition for a color hover effect. +Interactive elements (links, buttons, etc.) should have a hover state. A subtle animation for this transition adds a polished feel. We should target a `100ms - 150ms linear` transition for a color hover effect. View the [interactive example](http://codepen.io/awhildy/full/GNyEvM/) here. -- cgit v1.2.1 From 863efcc60437ccec67be6bd68af9ff3678141b89 Mon Sep 17 00:00:00 2001 From: dimitrieh Date: Mon, 23 Jan 2017 00:44:07 +0100 Subject: Improve pipeline status icon linking in widgets --- app/views/projects/commit/_commit_box.html.haml | 3 ++- app/views/projects/merge_requests/widget/_heading.html.haml | 3 ++- app/views/projects/pipelines/_info.html.haml | 4 ++-- .../26982-improve-pipeline-status-icon-linking-in-widgets.yml | 4 ++++ 4 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 08eb0c57f66..2b1c4e28ce2 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -64,7 +64,8 @@ - if @commit.status .well-segment.pipeline-info %div{ class: "icon-container ci-status-icon-#{@commit.status}" } - = ci_icon_for_status(@commit.status) + = link_to pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) do + = ci_icon_for_status(@commit.status) Pipeline = link_to "##{@commit.pipelines.last.id}", pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "monospace" for diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index c80dc33058d..cc939ab9441 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -2,7 +2,8 @@ .mr-widget-heading - %w[success success_with_warnings skipped canceled failed running pending].each do |status| .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: ("display:none" unless @pipeline.status == status) } - = ci_icon_for_status(status) + = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id) do + = ci_icon_for_status(status) %span Pipeline = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline' diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index ca76f13ef5e..6caa5f16dc6 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -23,8 +23,8 @@ .info-well - if @commit.status .well-segment.pipeline-info - %div{ class: "icon-container ci-status-icon-#{@commit.status}" } - = ci_icon_for_status(@commit.status) + .icon-container + = icon('clock-o') = pluralize @pipeline.statuses.count(:id), "build" - if @pipeline.ref from diff --git a/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml b/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml new file mode 100644 index 00000000000..c5c57af5aaf --- /dev/null +++ b/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml @@ -0,0 +1,4 @@ +--- +title: Improve pipeline status icon linking in widgets +merge_request: +author: -- cgit v1.2.1 From bc9c245b87375abafd9050648bf020b879172a79 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Tue, 10 Jan 2017 19:43:58 +0100 Subject: Chat Commands have presenters This improves the styling and readability of the code. This is supported by both Mattermost and Slack. --- .../chat_slash_commands_service.rb | 20 +-- lib/gitlab/chat_commands/base_command.rb | 4 - lib/gitlab/chat_commands/command.rb | 22 +-- lib/gitlab/chat_commands/deploy.rb | 24 ++-- lib/gitlab/chat_commands/issue_create.rb | 18 ++- lib/gitlab/chat_commands/issue_search.rb | 10 +- lib/gitlab/chat_commands/issue_show.rb | 8 +- lib/gitlab/chat_commands/presenter.rb | 131 ----------------- lib/gitlab/chat_commands/presenters/access.rb | 22 +++ lib/gitlab/chat_commands/presenters/base.rb | 73 ++++++++++ lib/gitlab/chat_commands/presenters/deploy.rb | 24 ++++ lib/gitlab/chat_commands/presenters/issuable.rb | 33 +++++ lib/gitlab/chat_commands/presenters/list_issues.rb | 32 +++++ lib/gitlab/chat_commands/presenters/show_issue.rb | 38 +++++ lib/mattermost/client.rb | 41 ------ lib/mattermost/command.rb | 10 -- lib/mattermost/error.rb | 3 - lib/mattermost/session.rb | 160 --------------------- lib/mattermost/team.rb | 7 - spec/lib/gitlab/chat_commands/command_spec.rb | 60 +------- spec/lib/gitlab/chat_commands/deploy_spec.rb | 24 ++-- spec/lib/gitlab/chat_commands/issue_create_spec.rb | 12 +- spec/lib/gitlab/chat_commands/issue_search_spec.rb | 12 +- spec/lib/gitlab/chat_commands/issue_show_spec.rb | 25 +++- .../gitlab/chat_commands/presenters/access_spec.rb | 49 +++++++ .../gitlab/chat_commands/presenters/deploy_spec.rb | 47 ++++++ .../chat_commands/presenters/list_issues_spec.rb | 24 ++++ .../chat_commands/presenters/show_issue_spec.rb | 27 ++++ spec/lib/mattermost/client_spec.rb | 24 ---- spec/lib/mattermost/command_spec.rb | 61 -------- spec/lib/mattermost/session_spec.rb | 123 ---------------- spec/lib/mattermost/team_spec.rb | 66 --------- 32 files changed, 476 insertions(+), 758 deletions(-) delete mode 100644 lib/gitlab/chat_commands/presenter.rb create mode 100644 lib/gitlab/chat_commands/presenters/access.rb create mode 100644 lib/gitlab/chat_commands/presenters/base.rb create mode 100644 lib/gitlab/chat_commands/presenters/deploy.rb create mode 100644 lib/gitlab/chat_commands/presenters/issuable.rb create mode 100644 lib/gitlab/chat_commands/presenters/list_issues.rb create mode 100644 lib/gitlab/chat_commands/presenters/show_issue.rb delete mode 100644 lib/mattermost/client.rb delete mode 100644 lib/mattermost/command.rb delete mode 100644 lib/mattermost/error.rb delete mode 100644 lib/mattermost/session.rb delete mode 100644 lib/mattermost/team.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/access_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb delete mode 100644 spec/lib/mattermost/client_spec.rb delete mode 100644 spec/lib/mattermost/command_spec.rb delete mode 100644 spec/lib/mattermost/session_spec.rb delete mode 100644 spec/lib/mattermost/team_spec.rb diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb index 2bcff541cc0..608754f3035 100644 --- a/app/models/project_services/chat_slash_commands_service.rb +++ b/app/models/project_services/chat_slash_commands_service.rb @@ -28,20 +28,24 @@ class ChatSlashCommandsService < Service end def trigger(params) - return unless valid_token?(params[:token]) + return access_presenter unless valid_token?(params[:token]) user = find_chat_user(params) - unless user + + if user + Gitlab::ChatCommands::Command.new(project, user, params).execute + else url = authorize_chat_name_url(params) - return presenter.authorize_chat_name(url) + access_presenter(url).authorize end - - Gitlab::ChatCommands::Command.new(project, user, - params).execute end private + def access_presenter(url = nil) + Gitlab::ChatCommands::Presenters::Access.new(url) + end + def find_chat_user(params) ChatNames::FindUserService.new(self, params).execute end @@ -49,8 +53,4 @@ class ChatSlashCommandsService < Service def authorize_chat_name_url(params) ChatNames::AuthorizeUserService.new(self, params).execute end - - def presenter - Gitlab::ChatCommands::Presenter.new - end end diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/chat_commands/base_command.rb index 4fe53ce93a9..25da8474e95 100644 --- a/lib/gitlab/chat_commands/base_command.rb +++ b/lib/gitlab/chat_commands/base_command.rb @@ -42,10 +42,6 @@ module Gitlab def find_by_iid(iid) collection.find_by(iid: iid) end - - def presenter - Gitlab::ChatCommands::Presenter.new - end end end end diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index 145086755e4..ac7ee868402 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -13,9 +13,9 @@ module Gitlab if command if command.allowed?(project, current_user) - present command.new(project, current_user, params).execute(match) + command.new(project, current_user, params).execute(match) else - access_denied + Gitlab::ChatCommands::Presenters::Access.new.access_denied end else help(help_messages) @@ -25,7 +25,7 @@ module Gitlab def match_command match = nil service = available_commands.find do |klass| - match = klass.match(command) + match = klass.match(params[:text]) end [service, match] @@ -42,22 +42,6 @@ module Gitlab klass.available?(project) 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 diff --git a/lib/gitlab/chat_commands/deploy.rb b/lib/gitlab/chat_commands/deploy.rb index 7127d2f6d04..458d90f84e8 100644 --- a/lib/gitlab/chat_commands/deploy.rb +++ b/lib/gitlab/chat_commands/deploy.rb @@ -1,8 +1,6 @@ module Gitlab module ChatCommands class Deploy < BaseCommand - include Gitlab::Routing.url_helpers - def self.match(text) /\Adeploy\s+(?\S+.*)\s+to+\s+(?\S+.*)\z/.match(text) end @@ -24,35 +22,29 @@ module Gitlab to = match[:to] actions = find_actions(from, to) - return unless actions.present? - if actions.one? - play!(from, to, actions.first) + if actions.none? + 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 - Result.new(:error, 'Too many actions defined') + Gitlab::ChatCommands::Presenters::Deploy.new(actions).too_many_actions end end private def play!(from, to, action) - new_action = action.play(current_user) - - Result.new(:success, "Deployment from #{from} to #{to} started. Follow the progress: #{url(new_action)}.") + action.play(current_user) end def find_actions(from, to) environment = project.environments.find_by(name: from) - return unless environment + return [] unless environment environment.actions_for(to).select(&:starts_environment?) end - - def url(subject) - polymorphic_url( - [subject.project.namespace.becomes(Namespace), subject.project, subject] - ) - end end end end diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb index cefb6775db8..a06f13b0f72 100644 --- a/lib/gitlab/chat_commands/issue_create.rb +++ b/lib/gitlab/chat_commands/issue_create.rb @@ -2,7 +2,7 @@ module Gitlab module ChatCommands class IssueCreate < IssueCommand 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 /\Aissue\s+(new|create)\s+(?[^\n]*)\n*(?<description>(.|\n)*)/.match(text) end @@ -19,8 +19,24 @@ module Gitlab title = match[:title] 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 end + + def presenter(issue) + Gitlab::ChatCommands::Presenters::ShowIssue.new(issue) + end end end end diff --git a/lib/gitlab/chat_commands/issue_search.rb b/lib/gitlab/chat_commands/issue_search.rb index 51bf80c800b..e2d3a0f466a 100644 --- a/lib/gitlab/chat_commands/issue_search.rb +++ b/lib/gitlab/chat_commands/issue_search.rb @@ -10,7 +10,15 @@ module Gitlab end 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 diff --git a/lib/gitlab/chat_commands/issue_show.rb b/lib/gitlab/chat_commands/issue_show.rb index 2a45d49cf6b..9f3e1b9a64b 100644 --- a/lib/gitlab/chat_commands/issue_show.rb +++ b/lib/gitlab/chat_commands/issue_show.rb @@ -10,7 +10,13 @@ module Gitlab end 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 diff --git a/lib/gitlab/chat_commands/presenter.rb b/lib/gitlab/chat_commands/presenter.rb deleted file mode 100644 index 8930a21f406..00000000000 --- a/lib/gitlab/chat_commands/presenter.rb +++ /dev/null @@ -1,131 +0,0 @@ -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 diff --git a/lib/gitlab/chat_commands/presenters/access.rb b/lib/gitlab/chat_commands/presenters/access.rb new file mode 100644 index 00000000000..6d18d745608 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/access.rb @@ -0,0 +1,22 @@ +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 diff --git a/lib/gitlab/chat_commands/presenters/base.rb b/lib/gitlab/chat_commands/presenters/base.rb new file mode 100644 index 00000000000..0897025d85f --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/base.rb @@ -0,0 +1,73 @@ +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 diff --git a/lib/gitlab/chat_commands/presenters/deploy.rb b/lib/gitlab/chat_commands/presenters/deploy.rb new file mode 100644 index 00000000000..4f6333812ff --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/deploy.rb @@ -0,0 +1,24 @@ +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 diff --git a/lib/gitlab/chat_commands/presenters/issuable.rb b/lib/gitlab/chat_commands/presenters/issuable.rb new file mode 100644 index 00000000000..9623387f188 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/issuable.rb @@ -0,0 +1,33 @@ +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 diff --git a/lib/gitlab/chat_commands/presenters/list_issues.rb b/lib/gitlab/chat_commands/presenters/list_issues.rb new file mode 100644 index 00000000000..5a7b3fca5c2 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/list_issues.rb @@ -0,0 +1,32 @@ +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 diff --git a/lib/gitlab/chat_commands/presenters/show_issue.rb b/lib/gitlab/chat_commands/presenters/show_issue.rb new file mode 100644 index 00000000000..2a89c30b972 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/show_issue.rb @@ -0,0 +1,38 @@ +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 diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb deleted file mode 100644 index ec2903b7ec6..00000000000 --- a/lib/mattermost/client.rb +++ /dev/null @@ -1,41 +0,0 @@ -module Mattermost - class ClientError < Mattermost::Error; end - - class Client - attr_reader :user - - def initialize(user) - @user = user - end - - private - - def with_session(&blk) - Mattermost::Session.new(user).with_session(&blk) - end - - def json_get(path, options = {}) - with_session do |session| - json_response session.get(path, options) - end - end - - def json_post(path, options = {}) - with_session do |session| - json_response session.post(path, options) - end - end - - def json_response(response) - json_response = JSON.parse(response.body) - - unless response.success? - raise Mattermost::ClientError.new(json_response['message'] || 'Undefined error') - end - - json_response - rescue JSON::JSONError - raise Mattermost::ClientError.new('Cannot parse response') - end - end -end diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb deleted file mode 100644 index d1e4bb0eccf..00000000000 --- a/lib/mattermost/command.rb +++ /dev/null @@ -1,10 +0,0 @@ -module Mattermost - class Command < Client - def create(params) - response = json_post("/api/v3/teams/#{params[:team_id]}/commands/create", - body: params.to_json) - - response['token'] - end - end -end diff --git a/lib/mattermost/error.rb b/lib/mattermost/error.rb deleted file mode 100644 index 014df175be0..00000000000 --- a/lib/mattermost/error.rb +++ /dev/null @@ -1,3 +0,0 @@ -module Mattermost - class Error < StandardError; end -end diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb deleted file mode 100644 index 377cb7b1021..00000000000 --- a/lib/mattermost/session.rb +++ /dev/null @@ -1,160 +0,0 @@ -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 diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb deleted file mode 100644 index 784eca6ab5a..00000000000 --- a/lib/mattermost/team.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Mattermost - class Team < Client - def all - json_get('/api/v3/teams/all') - end - end -end diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index a2d84977f58..b634df52b68 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -5,19 +5,7 @@ describe Gitlab::ChatCommands::Command, service: true do let(:user) { create(:user) } describe '#execute' do - subject do - described_class.new(project, user, params).execute - end - - 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 + subject { described_class.new(project, user, params).execute } context 'when an unknown command is triggered' do let(:params) { { command: '/gitlab', text: "unknown command 123" } } @@ -34,47 +22,7 @@ describe Gitlab::ChatCommands::Command, service: true do 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 - - 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 + expect(subject[:text]).to start_with('Whoops! This action is not allowed') end end @@ -90,7 +38,7 @@ describe Gitlab::ChatCommands::Command, service: true do context 'and user can not create deployment' do it 'returns action' do 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 @@ -100,7 +48,7 @@ describe Gitlab::ChatCommands::Command, service: true do end 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) end diff --git a/spec/lib/gitlab/chat_commands/deploy_spec.rb b/spec/lib/gitlab/chat_commands/deploy_spec.rb index bd8099c92da..b3358a32161 100644 --- a/spec/lib/gitlab/chat_commands/deploy_spec.rb +++ b/spec/lib/gitlab/chat_commands/deploy_spec.rb @@ -15,8 +15,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do end context 'if no environment is defined' do - it 'returns nil' do - expect(subject).to be_nil + it 'does not execute an action' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to eq("No action found to be executed") end end @@ -26,8 +27,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do let!(:deployment) { create(:deployment, environment: staging, deployable: build) } context 'without actions' do - it 'returns nil' do - expect(subject).to be_nil + it 'does not execute an action' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to eq("No action found to be executed") end end @@ -37,8 +39,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do end it 'returns success result' do - expect(subject.type).to eq(:success) - expect(subject.message).to include('Deployment from staging to production started') + expect(subject[:response_type]).to be(:in_channel) + expect(subject[:text]).to start_with('Deployment started from staging to production') end context 'when duplicate action exists' do @@ -47,8 +49,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do end it 'returns error' do - expect(subject.type).to eq(:error) - expect(subject.message).to include('Too many actions defined') + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to eq('Too many actions defined') end end @@ -59,9 +61,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do name: 'teardown', environment: 'production') end - it 'returns success result' do - expect(subject.type).to eq(:success) - expect(subject.message).to include('Deployment from staging to production started') + it 'returns the success message' do + expect(subject[:response_type]).to be(:in_channel) + expect(subject[:text]).to start_with('Deployment started from staging to production') end end end diff --git a/spec/lib/gitlab/chat_commands/issue_create_spec.rb b/spec/lib/gitlab/chat_commands/issue_create_spec.rb index 6c71e79ff6d..0f84b19a5a4 100644 --- a/spec/lib/gitlab/chat_commands/issue_create_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_create_spec.rb @@ -18,7 +18,7 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do it 'creates the issue' do 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 @@ -41,6 +41,16 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do expect { subject }.to change { project.issues.count }.by(1) 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 describe '.match' do diff --git a/spec/lib/gitlab/chat_commands/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/issue_search_spec.rb index 24c06a967fa..04d10ad52a1 100644 --- a/spec/lib/gitlab/chat_commands/issue_search_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_search_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' describe Gitlab::ChatCommands::IssueSearch, service: true 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(:project) { issue.project } + let(:project) { create(:empty_project) } let(:user) { issue.author } let(:regex_match) { described_class.match("issue search find") } @@ -14,7 +14,8 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do context 'when the user has no access' 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 @@ -24,13 +25,14 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do end 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 context 'without hits on the query' do it 'returns an empty collection' do - expect(subject).to be_empty + expect(subject[:text]).to match("not found") end end end diff --git a/spec/lib/gitlab/chat_commands/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/issue_show_spec.rb index 2eab73e49e5..89932c395c6 100644 --- a/spec/lib/gitlab/chat_commands/issue_show_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_show_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' describe Gitlab::ChatCommands::IssueShow, service: true do describe '#execute' do - let(:issue) { create(:issue) } - let(:project) { issue.project } + let(:issue) { create(:issue, project: project) } + let(:project) { create(:empty_project) } let(:user) { issue.author } let(:regex_match) { described_class.match("issue show #{issue.iid}") } @@ -16,15 +16,19 @@ describe Gitlab::ChatCommands::IssueShow, service: true do end context 'the issue exists' do + let(:title) { subject[:attachments].first[:title] } + 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 context 'when its reference is given' do let(:regex_match) { described_class.match("issue show #{issue.to_reference}") } 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 @@ -32,17 +36,24 @@ describe Gitlab::ChatCommands::IssueShow, service: true do 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 + it "returns not found" do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to match("not found") end end end - describe 'self.match' do + describe '.match' do it 'matches the iid' do match = described_class.match("issue show 123") expect(match[:iid]).to eq("123") 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 diff --git a/spec/lib/gitlab/chat_commands/presenters/access_spec.rb b/spec/lib/gitlab/chat_commands/presenters/access_spec.rb new file mode 100644 index 00000000000..ae41d75ab0c --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/access_spec.rb @@ -0,0 +1,49 @@ +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 diff --git a/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb new file mode 100644 index 00000000000..1c48c727e30 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb @@ -0,0 +1,47 @@ +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 diff --git a/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb b/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb new file mode 100644 index 00000000000..1852395fc97 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb @@ -0,0 +1,24 @@ +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 diff --git a/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb b/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb new file mode 100644 index 00000000000..13a318fe680 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb @@ -0,0 +1,27 @@ +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 diff --git a/spec/lib/mattermost/client_spec.rb b/spec/lib/mattermost/client_spec.rb deleted file mode 100644 index dc11a414717..00000000000 --- a/spec/lib/mattermost/client_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -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 diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb deleted file mode 100644 index 5ccf1100898..00000000000 --- a/spec/lib/mattermost/command_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -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 diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb deleted file mode 100644 index 74d12e37181..00000000000 --- a/spec/lib/mattermost/session_spec.rb +++ /dev/null @@ -1,123 +0,0 @@ -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 diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb deleted file mode 100644 index 2d14be6bcc2..00000000000 --- a/spec/lib/mattermost/team_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -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 -- cgit v1.2.1 From 19c55a47b77f6c63db39a45946dc47f3c95fc744 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Wed, 11 Jan 2017 08:54:44 -0500 Subject: Revert removing of some files --- lib/mattermost/client.rb | 41 ++++++++++++ lib/mattermost/command.rb | 10 +++ lib/mattermost/error.rb | 3 + lib/mattermost/session.rb | 160 ++++++++++++++++++++++++++++++++++++++++++++++ lib/mattermost/team.rb | 7 ++ 5 files changed, 221 insertions(+) create mode 100644 lib/mattermost/client.rb create mode 100644 lib/mattermost/command.rb create mode 100644 lib/mattermost/error.rb create mode 100644 lib/mattermost/session.rb create mode 100644 lib/mattermost/team.rb diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb new file mode 100644 index 00000000000..ec2903b7ec6 --- /dev/null +++ b/lib/mattermost/client.rb @@ -0,0 +1,41 @@ +module Mattermost + class ClientError < Mattermost::Error; end + + class Client + attr_reader :user + + def initialize(user) + @user = user + end + + private + + def with_session(&blk) + Mattermost::Session.new(user).with_session(&blk) + end + + def json_get(path, options = {}) + with_session do |session| + json_response session.get(path, options) + end + end + + def json_post(path, options = {}) + with_session do |session| + json_response session.post(path, options) + end + end + + def json_response(response) + json_response = JSON.parse(response.body) + + unless response.success? + raise Mattermost::ClientError.new(json_response['message'] || 'Undefined error') + end + + json_response + rescue JSON::JSONError + raise Mattermost::ClientError.new('Cannot parse response') + end + end +end diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb new file mode 100644 index 00000000000..d1e4bb0eccf --- /dev/null +++ b/lib/mattermost/command.rb @@ -0,0 +1,10 @@ +module Mattermost + class Command < Client + def create(params) + response = json_post("/api/v3/teams/#{params[:team_id]}/commands/create", + body: params.to_json) + + response['token'] + end + end +end diff --git a/lib/mattermost/error.rb b/lib/mattermost/error.rb new file mode 100644 index 00000000000..014df175be0 --- /dev/null +++ b/lib/mattermost/error.rb @@ -0,0 +1,3 @@ +module Mattermost + class Error < StandardError; end +end diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb new file mode 100644 index 00000000000..377cb7b1021 --- /dev/null +++ b/lib/mattermost/session.rb @@ -0,0 +1,160 @@ +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 diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb new file mode 100644 index 00000000000..784eca6ab5a --- /dev/null +++ b/lib/mattermost/team.rb @@ -0,0 +1,7 @@ +module Mattermost + class Team < Client + def all + json_get('/api/v3/teams/all') + end + end +end -- cgit v1.2.1 From e96ddef3deaac499bcdd67800839e59f46ae67b5 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Wed, 11 Jan 2017 09:04:49 -0500 Subject: Add help command --- lib/gitlab/chat_commands/command.rb | 13 +++++-------- lib/gitlab/chat_commands/help.rb | 28 ++++++++++++++++++++++++++++ lib/gitlab/chat_commands/presenters/help.rb | 20 ++++++++++++++++++++ 3 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 lib/gitlab/chat_commands/help.rb create mode 100644 lib/gitlab/chat_commands/presenters/help.rb diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index ac7ee868402..4e5031a8a26 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -18,25 +18,22 @@ module Gitlab Gitlab::ChatCommands::Presenters::Access.new.access_denied end else - help(help_messages) + Gitlab::ChatCommands::Help.new(project, current_user, params).execute(available_commands) end end def match_command match = nil - service = available_commands.find do |klass| - match = klass.match(params[:text]) - end + service = + available_commands.find do |klass| + match = klass.match(params[:text]) + end [service, match] end private - def help_messages - available_commands.map(&:help_message) - end - def available_commands COMMANDS.select do |klass| klass.available?(project) diff --git a/lib/gitlab/chat_commands/help.rb b/lib/gitlab/chat_commands/help.rb new file mode 100644 index 00000000000..e76733f5445 --- /dev/null +++ b/lib/gitlab/chat_commands/help.rb @@ -0,0 +1,28 @@ +module Gitlab + module ChatCommands + class Help < BaseCommand + # This class has to be used last, as it always matches. It has to match + # because other commands were not triggered and we want to show the help + # command + def self.match(_text) + true + end + + def self.help_message + 'help' + end + + def self.allowed?(_project, _user) + true + end + + def execute(commands) + Gitlab::ChatCommands::Presenters::Help.new(commands).present(trigger) + end + + def trigger + params[:command] + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/help.rb b/lib/gitlab/chat_commands/presenters/help.rb new file mode 100644 index 00000000000..133b707231f --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/help.rb @@ -0,0 +1,20 @@ +module Gitlab::ChatCommands::Presenters + class Help < Gitlab::ChatCommands::Presenters::Base + def present(trigger) + message = + if @resource.none? + "No commands available :thinking_face:" + else + header_with_list("Available commands", full_commands(trigger)) + end + + ephemeral_response(text: message) + end + + private + + def full_commands(trigger) + @resource.map { |command| "#{trigger} #{command.help_message}" } + end + end +end -- cgit v1.2.1 From e9d163865bc6f987e7fa277536c6bc3950fbf1e0 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Thu, 12 Jan 2017 09:04:21 -0500 Subject: Fix tests --- app/models/project_services/chat_slash_commands_service.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb index 608754f3035..5eb1bd86e9d 100644 --- a/app/models/project_services/chat_slash_commands_service.rb +++ b/app/models/project_services/chat_slash_commands_service.rb @@ -28,7 +28,7 @@ class ChatSlashCommandsService < Service end def trigger(params) - return access_presenter unless valid_token?(params[:token]) + return unless valid_token?(params[:token]) user = find_chat_user(params) @@ -36,16 +36,12 @@ class ChatSlashCommandsService < Service Gitlab::ChatCommands::Command.new(project, user, params).execute else url = authorize_chat_name_url(params) - access_presenter(url).authorize + Gitlab::ChatCommands::Presenters::Access.new(url).authorize end end private - def access_presenter(url = nil) - Gitlab::ChatCommands::Presenters::Access.new(url) - end - def find_chat_user(params) ChatNames::FindUserService.new(self, params).execute end -- cgit v1.2.1 From 52ca0d2c9ecb7e7d4d0130f19bf0fb7b772a357d Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Thu, 19 Jan 2017 09:22:09 +0100 Subject: Incorporate feedback --- changelogs/unreleased/zj-format-chat-messages.yml | 4 + lib/gitlab/chat_commands/issue_create.rb | 2 +- lib/gitlab/chat_commands/presenters/access.rb | 36 +++--- lib/gitlab/chat_commands/presenters/base.rb | 112 ++++++++++--------- lib/gitlab/chat_commands/presenters/deploy.rb | 39 ++++--- lib/gitlab/chat_commands/presenters/help.rb | 31 +++--- lib/gitlab/chat_commands/presenters/issuable.rb | 66 ++++++----- lib/gitlab/chat_commands/presenters/list_issues.rb | 59 ++++++---- lib/gitlab/chat_commands/presenters/new_issue.rb | 42 +++++++ lib/gitlab/chat_commands/presenters/show_issue.rb | 72 +++++++----- spec/lib/gitlab/chat_commands/issue_search_spec.rb | 2 +- spec/lib/gitlab/chat_commands/issue_show_spec.rb | 4 +- .../gitlab/chat_commands/presenters/deploy_spec.rb | 2 +- .../chat_commands/presenters/list_issues_spec.rb | 5 +- .../chat_commands/presenters/show_issue_spec.rb | 4 +- spec/lib/mattermost/client_spec.rb | 24 ++++ spec/lib/mattermost/command_spec.rb | 61 ++++++++++ spec/lib/mattermost/session_spec.rb | 123 +++++++++++++++++++++ spec/lib/mattermost/team_spec.rb | 66 +++++++++++ 19 files changed, 565 insertions(+), 189 deletions(-) create mode 100644 changelogs/unreleased/zj-format-chat-messages.yml create mode 100644 lib/gitlab/chat_commands/presenters/new_issue.rb create mode 100644 spec/lib/mattermost/client_spec.rb create mode 100644 spec/lib/mattermost/command_spec.rb create mode 100644 spec/lib/mattermost/session_spec.rb create mode 100644 spec/lib/mattermost/team_spec.rb diff --git a/changelogs/unreleased/zj-format-chat-messages.yml b/changelogs/unreleased/zj-format-chat-messages.yml new file mode 100644 index 00000000000..2494884f5c9 --- /dev/null +++ b/changelogs/unreleased/zj-format-chat-messages.yml @@ -0,0 +1,4 @@ +--- +title: Reformat messages ChatOps +merge_request: 8528 +author: diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb index a06f13b0f72..3f3d7de8b2e 100644 --- a/lib/gitlab/chat_commands/issue_create.rb +++ b/lib/gitlab/chat_commands/issue_create.rb @@ -35,7 +35,7 @@ module Gitlab end def presenter(issue) - Gitlab::ChatCommands::Presenters::ShowIssue.new(issue) + Gitlab::ChatCommands::Presenters::NewIssue.new(issue) end end end diff --git a/lib/gitlab/chat_commands/presenters/access.rb b/lib/gitlab/chat_commands/presenters/access.rb index 6d18d745608..b66ef48d6a8 100644 --- a/lib/gitlab/chat_commands/presenters/access.rb +++ b/lib/gitlab/chat_commands/presenters/access.rb @@ -1,22 +1,26 @@ -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 +module Gitlab + module ChatCommands + module Presenters + class Access < 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 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!" + def not_found + ephemeral_response(text: "404 not found! GitLab couldn't find what you were looking for! :boom:") end - ephemeral_response(text: message) + 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 end end diff --git a/lib/gitlab/chat_commands/presenters/base.rb b/lib/gitlab/chat_commands/presenters/base.rb index 0897025d85f..2700a5a2ad5 100644 --- a/lib/gitlab/chat_commands/presenters/base.rb +++ b/lib/gitlab/chat_commands/presenters/base.rb @@ -1,73 +1,77 @@ -module Gitlab::ChatCommands::Presenters - class Base - include Gitlab::Routing.url_helpers +module Gitlab + module ChatCommands + module Presenters + class Base + include Gitlab::Routing.url_helpers + + def initialize(resource = nil) + @resource = resource + end - def initialize(resource = nil) - @resource = resource - end + def display_errors + message = header_with_list("The action was not successful, because:", @resource.errors.full_messages) - def display_errors - message = header_with_list("The action was not successful, because:", @resource.errors.full_messages) + ephemeral_response(text: message) + end - ephemeral_response(text: message) - end + private - private + def header_with_list(header, items) + message = [header] - def header_with_list(header, items) - message = [header] + items.each do |item| + message << "- #{item}" + end - items.each do |item| - message << "- #{item}" - end + message.join("\n") + end - message.join("\n") - end + def ephemeral_response(message) + response = { + response_type: :ephemeral, + status: 200 + }.merge(message) - def ephemeral_response(message) - response = { - response_type: :ephemeral, - status: 200 - }.merge(message) + format_response(response) + end - format_response(response) - end + def in_channel_response(message) + response = { + response_type: :in_channel, + status: 200 + }.merge(message) - def in_channel_response(message) - response = { - response_type: :in_channel, - status: 200 - }.merge(message) + format_response(response) + end - format_response(response) - end + def format_response(response) + response[:text] = format(response[:text]) if response.has_key?(:text) - 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 - 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] + response end - end - - response - end - # Convert Markdown to slacks format - def format(string) - Slack::Notifier::LinkFormatter.format(string) - 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 - ] - ) + def resource_url + url_for( + [ + @resource.project.namespace.becomes(Namespace), + @resource.project, + @resource + ] + ) + end + end end end end diff --git a/lib/gitlab/chat_commands/presenters/deploy.rb b/lib/gitlab/chat_commands/presenters/deploy.rb index 4f6333812ff..b1cfaac15af 100644 --- a/lib/gitlab/chat_commands/presenters/deploy.rb +++ b/lib/gitlab/chat_commands/presenters/deploy.rb @@ -1,24 +1,29 @@ -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 +module Gitlab + module ChatCommands + module Presenters + class Deploy < Presenters::Base + def present(from, to) + message = "Deployment started from #{from} to #{to}. [Follow its progress](#{resource_url})." - def no_actions - ephemeral_response(text: "No action found to be executed") - end + in_channel_response(text: message) + end - def too_many_actions - ephemeral_response(text: "Too many actions defined") - 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 + private - def resource_url - polymorphic_url( - [ @resource.project.namespace.becomes(Namespace), @resource.project, @resource] - ) + def resource_url + polymorphic_url( + [ @resource.project.namespace.becomes(Namespace), @resource.project, @resource] + ) + end + end end end end diff --git a/lib/gitlab/chat_commands/presenters/help.rb b/lib/gitlab/chat_commands/presenters/help.rb index 133b707231f..c7a67467b7e 100644 --- a/lib/gitlab/chat_commands/presenters/help.rb +++ b/lib/gitlab/chat_commands/presenters/help.rb @@ -1,20 +1,25 @@ -module Gitlab::ChatCommands::Presenters - class Help < Gitlab::ChatCommands::Presenters::Base - def present(trigger) - message = - if @resource.none? - "No commands available :thinking_face:" - else - header_with_list("Available commands", full_commands(trigger)) +module Gitlab + module ChatCommands + module Presenters + class Help < Presenters::Base + def present(trigger) + ephemeral_response(text: help_message(trigger)) end - ephemeral_response(text: message) - end + private - private + def help_message(trigger) + if @resource.none? + "No commands available :thinking_face:" + else + header_with_list("Available commands", full_commands(trigger)) + end + end - def full_commands(trigger) - @resource.map { |command| "#{trigger} #{command.help_message}" } + def full_commands(trigger) + @resource.map { |command| "#{trigger} #{command.help_message}" } + end + end end end end diff --git a/lib/gitlab/chat_commands/presenters/issuable.rb b/lib/gitlab/chat_commands/presenters/issuable.rb index 9623387f188..2cb6b1525fc 100644 --- a/lib/gitlab/chat_commands/presenters/issuable.rb +++ b/lib/gitlab/chat_commands/presenters/issuable.rb @@ -1,33 +1,45 @@ -module Gitlab::ChatCommands::Presenters - class Issuable < Gitlab::ChatCommands::Presenters::Base - private +module Gitlab + module ChatCommands + module Presenters + class Issuable < Presenters::Base + private - def project - @resource.project - end + def color(issuable) + issuable.open? ? '#38ae67' : '#d22852' + end - def author - @resource.author - end + def status_text(issuable) + issuable.open? ? 'Open' : 'Closed' + end + + 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 - } - ] + 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 end end diff --git a/lib/gitlab/chat_commands/presenters/list_issues.rb b/lib/gitlab/chat_commands/presenters/list_issues.rb index 5a7b3fca5c2..2458b9356b7 100644 --- a/lib/gitlab/chat_commands/presenters/list_issues.rb +++ b/lib/gitlab/chat_commands/presenters/list_issues.rb @@ -1,32 +1,43 @@ -module Gitlab::ChatCommands::Presenters - class ListIssues < Gitlab::ChatCommands::Presenters::Base - def present - ephemeral_response(text: "Here are the issues I found:", attachments: attachments) - end +module Gitlab + module ChatCommands + module Presenters + class ListIssues < Presenters::Issuable + def present + text = if @resource.count >= 5 + "Here are the first 5 issues I found:" + else + "Here are the #{@resource.count} issues I found:" + end - private + ephemeral_response(text: text, attachments: attachments) + end - def attachments - @resource.map do |issue| - state = issue.open? ? "Open" : "Closed" + private - { - 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 attachments + @resource.map do |issue| + url = "[#{issue.to_reference}](#{url_for([namespace, project, issue])})" - def project - @project ||= @resource.first.project - end + { + color: color(issue), + fallback: "#{issue.to_reference} #{issue.title}", + text: "#{url} · #{issue.title} (#{status_text(issue)})", - def namespace - @namespace ||= project.namespace.becomes(Namespace) + mrkdwn_in: [ + "text" + ] + } + end + end + + def project + @project ||= @resource.first.project + end + + def namespace + @namespace ||= project.namespace.becomes(Namespace) + end + end end end end diff --git a/lib/gitlab/chat_commands/presenters/new_issue.rb b/lib/gitlab/chat_commands/presenters/new_issue.rb new file mode 100644 index 00000000000..c7c6febb56e --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/new_issue.rb @@ -0,0 +1,42 @@ +module Gitlab + module ChatCommands + module Presenters + class NewIssue < Presenters::Issuable + def present + in_channel_response(show_issue) + end + + private + + def show_issue + { + attachments: [ + { + title: "#{@resource.title} · #{@resource.to_reference}", + title_link: resource_url, + author_name: author.name, + author_icon: author.avatar_url, + fallback: "New issue #{@resource.to_reference}: #{@resource.title}", + pretext: pretext, + color: color(@resource), + fields: fields, + mrkdwn_in: [ + :title, + :text + ] + } + ] + } + end + + def pretext + "I opened an issue on behalf on #{author_profile_link}: *#{@resource.to_reference}* from #{project.name_with_namespace}" + end + + def author_profile_link + "[#{author.to_reference}](#{url_for(author)})" + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/show_issue.rb b/lib/gitlab/chat_commands/presenters/show_issue.rb index 2a89c30b972..e5644a4ad7e 100644 --- a/lib/gitlab/chat_commands/presenters/show_issue.rb +++ b/lib/gitlab/chat_commands/presenters/show_issue.rb @@ -1,38 +1,54 @@ -module Gitlab::ChatCommands::Presenters - class ShowIssue < Gitlab::ChatCommands::Presenters::Issuable - def present - in_channel_response(show_issue) - end +module Gitlab + module ChatCommands + module Presenters + class ShowIssue < Presenters::Issuable + def present + in_channel_response(show_issue) + end - private + private - def show_issue - { - attachments: [ + def show_issue { - 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 + attachments: [ + { + title: "#{@resource.title} · #{@resource.to_reference}", + title_link: resource_url, + author_name: author.name, + author_icon: author.avatar_url, + fallback: "New issue #{@resource.to_reference}: #{@resource.title}", + pretext: pretext, + text: text, + color: color(@resource), + fields: fields, + mrkdwn_in: [ + :pretext, + :text + ] + } ] } - ] - } - end + end + + def text + message = "**#{status_text(@resource)}**" + + if @resource.upvotes.zero? && @resource.downvotes.zero? && @resource.user_notes_count.zero? + return message + end + + 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? - 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 - message + def pretext + "Issue *#{@resource.to_reference} from #{project.name_with_namespace}" + end + end end end end diff --git a/spec/lib/gitlab/chat_commands/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/issue_search_spec.rb index 04d10ad52a1..551ccb79a58 100644 --- a/spec/lib/gitlab/chat_commands/issue_search_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_search_spec.rb @@ -26,7 +26,7 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do it 'returns all results' do expect(subject).to have_key(:attachments) - expect(subject[:text]).to match("Here are the issues I found:") + expect(subject[:text]).to eq("Here are the 2 issues I found:") end end diff --git a/spec/lib/gitlab/chat_commands/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/issue_show_spec.rb index 89932c395c6..1f20d0a44ce 100644 --- a/spec/lib/gitlab/chat_commands/issue_show_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_show_spec.rb @@ -20,7 +20,7 @@ describe Gitlab::ChatCommands::IssueShow, service: true do it 'returns the issue' do expect(subject[:response_type]).to be(:in_channel) - expect(title).to eq(issue.title) + expect(title).to start_with(issue.title) end context 'when its reference is given' do @@ -28,7 +28,7 @@ describe Gitlab::ChatCommands::IssueShow, service: true do it 'shows the issue' do expect(subject[:response_type]).to be(:in_channel) - expect(title).to eq(issue.title) + expect(title).to start_with(issue.title) end end end diff --git a/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb index 1c48c727e30..dc2dd300072 100644 --- a/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb +++ b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb @@ -32,7 +32,7 @@ describe Gitlab::ChatCommands::Presenters::Deploy do end describe '#too_many_actions' do - subject { described_class.new(nil).too_many_actions } + subject { described_class.new([]).too_many_actions } it { is_expected.to have_key(:text) } it { is_expected.to have_key(:response_type) } diff --git a/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb b/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb index 1852395fc97..13a1f70fe78 100644 --- a/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb +++ b/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb @@ -3,13 +3,12 @@ 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 + it 'formats the message correct' do is_expected.to have_key(:text) is_expected.to have_key(:status) is_expected.to have_key(:response_type) @@ -19,6 +18,6 @@ describe Gitlab::ChatCommands::Presenters::ListIssues do 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") + expect(message).to start_with("Here are the 2 issues I found") end end diff --git a/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb b/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb index 13a318fe680..ca4062e692a 100644 --- a/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb +++ b/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb @@ -12,7 +12,7 @@ describe Gitlab::ChatCommands::Presenters::ShowIssue do 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) + expect(attachment[:title]).to start_with(issue.title) end context 'with upvotes' do @@ -21,7 +21,7 @@ describe Gitlab::ChatCommands::Presenters::ShowIssue do end it 'shows the upvote count' do - expect(attachment[:text]).to start_with(":+1: 1") + expect(attachment[:text]).to start_with("**Open** · :+1: 1") end end end diff --git a/spec/lib/mattermost/client_spec.rb b/spec/lib/mattermost/client_spec.rb new file mode 100644 index 00000000000..dc11a414717 --- /dev/null +++ b/spec/lib/mattermost/client_spec.rb @@ -0,0 +1,24 @@ +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 diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb new file mode 100644 index 00000000000..5ccf1100898 --- /dev/null +++ b/spec/lib/mattermost/command_spec.rb @@ -0,0 +1,61 @@ +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 diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb new file mode 100644 index 00000000000..74d12e37181 --- /dev/null +++ b/spec/lib/mattermost/session_spec.rb @@ -0,0 +1,123 @@ +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 diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb new file mode 100644 index 00000000000..2d14be6bcc2 --- /dev/null +++ b/spec/lib/mattermost/team_spec.rb @@ -0,0 +1,66 @@ +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 -- cgit v1.2.1 From 6a3d29c73d2578c7b2a40f2dfcb823b681a70f7e Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Mon, 14 Nov 2016 01:56:39 -0200 Subject: Add ability to define a coverage regex in the .gitlab-ci.yml * Instead of using the proposed `coverage` key, this expects `coverage_regex` --- app/models/ci/build.rb | 7 ++++-- .../20161114024742_add_coverage_regex_to_builds.rb | 13 +++++++++++ db/schema.rb | 1 + lib/ci/gitlab_ci_yaml_processor.rb | 1 + lib/gitlab/ci/config/entry/job.rb | 8 +++++-- .../ci/config/entry/legacy_validation_helpers.rb | 10 ++++++--- lib/gitlab/ci/config/entry/validators.rb | 10 +++++++++ lib/gitlab/ci/config/node/regexp.rb | 26 ++++++++++++++++++++++ 8 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 db/migrate/20161114024742_add_coverage_regex_to_builds.rb create mode 100644 lib/gitlab/ci/config/node/regexp.rb diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 5fe8ddf69d7..4691b33ee9b 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -276,8 +276,7 @@ module Ci def update_coverage return unless project - coverage_regex = project.build_coverage_regex - return unless coverage_regex + return unless coverage_regex = self.coverage_regex coverage = extract_coverage(trace, coverage_regex) if coverage.is_a? Numeric @@ -522,6 +521,10 @@ module Ci self.update(artifacts_expire_at: nil) end + def coverage_regex + read_attribute(:coverage_regex) || build_attributes_from_config[:coverage] || project.build_coverage_regex + end + def when read_attribute(:when) || build_attributes_from_config[:when] || 'on_success' end diff --git a/db/migrate/20161114024742_add_coverage_regex_to_builds.rb b/db/migrate/20161114024742_add_coverage_regex_to_builds.rb new file mode 100644 index 00000000000..88aa5d52b39 --- /dev/null +++ b/db/migrate/20161114024742_add_coverage_regex_to_builds.rb @@ -0,0 +1,13 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddCoverageRegexToBuilds < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def change + add_column :ci_builds, :coverage_regex, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 3c836db27fc..1cc9e7eec5e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -215,6 +215,7 @@ ActiveRecord::Schema.define(version: 20170121130655) do t.datetime "queued_at" t.string "token" t.integer "lock_version" + t.string "coverage_regex" end add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 7463bd719d5..2f5ef4d36bd 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -61,6 +61,7 @@ module Ci allow_failure: job[:allow_failure] || false, when: job[:when] || 'on_success', environment: job[:environment_name], + coverage_regex: job[:coverage_regex], yaml_variables: yaml_variables(name), options: { image: job[:image], diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index a55362f0b6b..3c7ef99cefc 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -11,7 +11,7 @@ module Gitlab ALLOWED_KEYS = %i[tags script only except type image services allow_failure type stage when artifacts cache dependencies before_script - after_script variables environment] + after_script variables environment coverage_regex] validations do validates :config, allowed_keys: ALLOWED_KEYS @@ -71,9 +71,12 @@ module Gitlab entry :environment, Entry::Environment, description: 'Environment configuration for this job.' + node :coverage_regex, Node::Regexp, + description: 'Coverage scanning regex configuration for this job.' + helpers :before_script, :script, :stage, :type, :after_script, :cache, :image, :services, :only, :except, :variables, - :artifacts, :commands, :environment + :artifacts, :commands, :environment, :coverage_regex attributes :script, :tags, :allow_failure, :when, :dependencies @@ -130,6 +133,7 @@ module Gitlab variables: variables_defined? ? variables_value : nil, environment: environment_defined? ? environment_value : nil, environment_name: environment_defined? ? environment_value[:name] : nil, + coverage_regex: coverage_regex_defined? ? coverage_regex_value : nil, artifacts: artifacts_value, after_script: after_script_value } end diff --git a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb index f01975aab5c..34e7052befc 100644 --- a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb +++ b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb @@ -28,17 +28,21 @@ module Gitlab value.is_a?(String) || value.is_a?(Symbol) end + def validate_regexp(value) + !!::Regexp.new(value) + rescue RegexpError + false + end + def validate_string_or_regexp(value) return true if value.is_a?(Symbol) return false unless value.is_a?(String) if value.first == '/' && value.last == '/' - Regexp.new(value[1...-1]) + validate_regexp(value[1...-1]) else true end - rescue RegexpError - false end def validate_boolean(value) diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb index 8632dd0e233..03a8205b081 100644 --- a/lib/gitlab/ci/config/entry/validators.rb +++ b/lib/gitlab/ci/config/entry/validators.rb @@ -54,6 +54,16 @@ module Gitlab end end + class RegexpValidator < ActiveModel::EachValidator + include LegacyValidationHelpers + + def validate_each(record, attribute, value) + unless validate_regexp(value) + record.errors.add(attribute, 'must be a regular expression') + end + end + end + class TypeValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) type = options[:with] diff --git a/lib/gitlab/ci/config/node/regexp.rb b/lib/gitlab/ci/config/node/regexp.rb new file mode 100644 index 00000000000..7c5843eb8b6 --- /dev/null +++ b/lib/gitlab/ci/config/node/regexp.rb @@ -0,0 +1,26 @@ +module Gitlab + module Ci + class Config + module Node + ## + # Entry that represents a Regular Expression. + # + class Regexp < Entry + include Validatable + + validations do + validates :config, regexp: true + end + + def value + if @config.first == '/' && @config.last == '/' + @config[1...-1] + else + @config + end + end + end + end + end + end +end -- cgit v1.2.1 From 646b9c54d043edf17924e82d8e80a56e18d14ce4 Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Fri, 18 Nov 2016 01:42:35 -0200 Subject: Comply to requests made in the review and adjust to the Entry/Node changes This commit: * Turns `coverage_regex` into `coverage` entry in yml file * Fixes smaller requests from code reviewers for the previous commit * This commit is temporary (will be squashed afterwards) This commit does not (further commits will do though): * Add global `coverage` entry handling in yml file as suggested by Grzegorz * Add specs * Create changelog * Create docs --- app/models/ci/build.rb | 6 ++--- lib/ci/gitlab_ci_yaml_processor.rb | 2 +- lib/gitlab/ci/config/entry/coverage.rb | 26 ++++++++++++++++++++++ lib/gitlab/ci/config/entry/job.rb | 8 +++---- .../ci/config/entry/legacy_validation_helpers.rb | 3 ++- lib/gitlab/ci/config/node/regexp.rb | 26 ---------------------- 6 files changed, 36 insertions(+), 35 deletions(-) create mode 100644 lib/gitlab/ci/config/entry/coverage.rb delete mode 100644 lib/gitlab/ci/config/node/regexp.rb diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 4691b33ee9b..46a6b4c724a 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -276,8 +276,8 @@ module Ci def update_coverage return unless project - return unless coverage_regex = self.coverage_regex - coverage = extract_coverage(trace, coverage_regex) + return unless regex = self.coverage_regex + coverage = extract_coverage(trace, regex) if coverage.is_a? Numeric update_attributes(coverage: coverage) @@ -522,7 +522,7 @@ module Ci end def coverage_regex - read_attribute(:coverage_regex) || build_attributes_from_config[:coverage] || project.build_coverage_regex + read_attribute(:coverage_regex) || project.build_coverage_regex end def when diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 2f5ef4d36bd..649ee4d018b 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -61,7 +61,7 @@ module Ci allow_failure: job[:allow_failure] || false, when: job[:when] || 'on_success', environment: job[:environment_name], - coverage_regex: job[:coverage_regex], + coverage_regex: job[:coverage], yaml_variables: yaml_variables(name), options: { image: job[:image], diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb new file mode 100644 index 00000000000..88fc03db2d9 --- /dev/null +++ b/lib/gitlab/ci/config/entry/coverage.rb @@ -0,0 +1,26 @@ +module Gitlab + module Ci + class Config + module Entry + ## + # Entry that represents a Regular Expression. + # + class Coverage < Node + include Validatable + + validations do + validates :config, regexp: true + end + + def value + if @config.first == '/' && @config.last == '/' + @config[1...-1] + else + @config + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index 3c7ef99cefc..bde6663344a 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -11,7 +11,7 @@ module Gitlab ALLOWED_KEYS = %i[tags script only except type image services allow_failure type stage when artifacts cache dependencies before_script - after_script variables environment coverage_regex] + after_script variables environment coverage] validations do validates :config, allowed_keys: ALLOWED_KEYS @@ -71,12 +71,12 @@ module Gitlab entry :environment, Entry::Environment, description: 'Environment configuration for this job.' - node :coverage_regex, Node::Regexp, + entry :coverage, Entry::Coverage, description: 'Coverage scanning regex configuration for this job.' helpers :before_script, :script, :stage, :type, :after_script, :cache, :image, :services, :only, :except, :variables, - :artifacts, :commands, :environment, :coverage_regex + :artifacts, :commands, :environment, :coverage attributes :script, :tags, :allow_failure, :when, :dependencies @@ -133,7 +133,7 @@ module Gitlab variables: variables_defined? ? variables_value : nil, environment: environment_defined? ? environment_value : nil, environment_name: environment_defined? ? environment_value[:name] : nil, - coverage_regex: coverage_regex_defined? ? coverage_regex_value : nil, + coverage: coverage_defined? ? coverage_value : nil, artifacts: artifacts_value, after_script: after_script_value } end diff --git a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb index 34e7052befc..98db4632dad 100644 --- a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb +++ b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb @@ -29,7 +29,8 @@ module Gitlab end def validate_regexp(value) - !!::Regexp.new(value) + Regexp.new(value) + true rescue RegexpError false end diff --git a/lib/gitlab/ci/config/node/regexp.rb b/lib/gitlab/ci/config/node/regexp.rb deleted file mode 100644 index 7c5843eb8b6..00000000000 --- a/lib/gitlab/ci/config/node/regexp.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Gitlab - module Ci - class Config - module Node - ## - # Entry that represents a Regular Expression. - # - class Regexp < Entry - include Validatable - - validations do - validates :config, regexp: true - end - - def value - if @config.first == '/' && @config.last == '/' - @config[1...-1] - else - @config - end - end - end - end - end - end -end -- cgit v1.2.1 From d0afc500e30ad0fe334d6dc16dd1766d8f6c523a Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Sat, 19 Nov 2016 22:48:02 -0200 Subject: Change expected `coverage` structure for CI configuration YAML file Instead of using: `coverage: /\(\d+.\d+%\) covered/` This structure must be used now: ``` coverage: output_filter: /\(\d+.\d+%\) covered/` ``` The surrounding '/' is optional. --- lib/ci/gitlab_ci_yaml_processor.rb | 2 +- lib/gitlab/ci/config/entry/coverage.rb | 20 +++++++++++++++----- lib/gitlab/ci/config/entry/job.rb | 2 +- .../ci/config/entry/legacy_validation_helpers.rb | 4 ++-- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 649ee4d018b..02944e0385a 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -61,7 +61,7 @@ module Ci allow_failure: job[:allow_failure] || false, when: job[:when] || 'on_success', environment: job[:environment_name], - coverage_regex: job[:coverage], + coverage_regex: job[:coverage][:output_filter], yaml_variables: yaml_variables(name), options: { image: job[:image], diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb index 88fc03db2d9..e5da3cf23fd 100644 --- a/lib/gitlab/ci/config/entry/coverage.rb +++ b/lib/gitlab/ci/config/entry/coverage.rb @@ -8,17 +8,27 @@ module Gitlab class Coverage < Node include Validatable + ALLOWED_KEYS = %i[output_filter] + validations do - validates :config, regexp: true + validates :config, type: Hash + validates :config, allowed_keys: ALLOWED_KEYS + validates :output_filter, regexp: true end - def value - if @config.first == '/' && @config.last == '/' - @config[1...-1] + def output_filter + output_filter_value = @config[:output_filter].to_s + + if output_filter_value.start_with?('/') && output_filter_value.end_with?('/') + output_filter_value[1...-1] else - @config + value[:output_filter] end end + + def value + @config.merge(output_filter: output_filter) + end end end end diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index bde6663344a..69a5e6f433d 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -72,7 +72,7 @@ module Gitlab description: 'Environment configuration for this job.' entry :coverage, Entry::Coverage, - description: 'Coverage scanning regex configuration for this job.' + description: 'Coverage configuration for this job.' helpers :before_script, :script, :stage, :type, :after_script, :cache, :image, :services, :only, :except, :variables, diff --git a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb index 98db4632dad..d8e74b15712 100644 --- a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb +++ b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb @@ -31,7 +31,7 @@ module Gitlab def validate_regexp(value) Regexp.new(value) true - rescue RegexpError + rescue RegexpError, TypeError false end @@ -39,7 +39,7 @@ module Gitlab return true if value.is_a?(Symbol) return false unless value.is_a?(String) - if value.first == '/' && value.last == '/' + if value.start_with?('/') && value.end_with?('/') validate_regexp(value[1...-1]) else true -- cgit v1.2.1 From 9f97cc6515ac1254c443673c84700942690bbc15 Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Sun, 20 Nov 2016 00:05:49 -0200 Subject: Add `coverage` to the Global config entry as well --- lib/gitlab/ci/config/entry/global.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/ci/config/entry/global.rb b/lib/gitlab/ci/config/entry/global.rb index a4ec8f0ff2f..ede97cc0504 100644 --- a/lib/gitlab/ci/config/entry/global.rb +++ b/lib/gitlab/ci/config/entry/global.rb @@ -33,8 +33,11 @@ module Gitlab entry :cache, Entry::Cache, description: 'Configure caching between build jobs.' + entry :coverage, Entry::Coverage, + description: 'Coverage configuration for this pipeline.' + helpers :before_script, :image, :services, :after_script, - :variables, :stages, :types, :cache, :jobs + :variables, :stages, :types, :cache, :coverage, :jobs def compose!(_deps = nil) super(self) do -- cgit v1.2.1 From 94eb2f47c732dc9485aba4ebe52238e882a43473 Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Sun, 20 Nov 2016 02:18:58 -0200 Subject: Add changelog entry and extend the documentation accordingly --- changelogs/unreleased/issue-20428.yml | 4 +++ doc/ci/yaml/README.md | 51 +++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 changelogs/unreleased/issue-20428.yml diff --git a/changelogs/unreleased/issue-20428.yml b/changelogs/unreleased/issue-20428.yml new file mode 100644 index 00000000000..60da1c14702 --- /dev/null +++ b/changelogs/unreleased/issue-20428.yml @@ -0,0 +1,4 @@ +--- +title: Add ability to define a coverage regex in the .gitlab-ci.yml +merge_request: 7447 +author: Leandro Camargo diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 75a0897eb15..a8c0721bbcc 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -76,6 +76,7 @@ There are a few reserved `keywords` that **cannot** be used as job names: | after_script | no | Define commands that run after each job's script | | variables | no | Define build variables | | cache | no | Define list of files that should be cached between subsequent runs | +| coverage | no | Define coverage settings for all jobs | ### image and services @@ -278,6 +279,33 @@ cache: untracked: true ``` +### coverage + +`coverage` allows you to configure how coverage will be filtered out from the +build outputs. Setting this up globally will make all the jobs to use this +setting for output filtering and extracting the coverage information from your +builds. + +#### coverage:output_filter + +For now, there is only the `output_filter` directive expected to be inside the +`coverage` entry. And it is expected to be a regular expression. + +So, in the end, you're going to have something like the following: + +```yaml +coverage: + output_filter: /\(\d+\.\d+\) covered\./ +``` + +It's worth to keep in mind that the surrounding `/` is optional. So, the above +example is the same as the following: + +```yaml +coverage: + output_filter: \(\d+\.\d+\) covered\. +``` + ## Jobs `.gitlab-ci.yml` allows you to specify an unlimited number of jobs. Each job @@ -319,6 +347,8 @@ job_name: | before_script | no | Override a set of commands that are executed before build | | after_script | no | Override a set of commands that are executed after build | | environment | no | Defines a name of environment to which deployment is done by this build | +| environment | no | Defines a name of environment to which deployment is done by this build | +| coverage | no | Define coverage settings for a given job | ### script @@ -993,6 +1023,27 @@ job: - execute this after my script ``` +### job coverage + +This entry is pretty much the same as described in the global context in +[`coverage`](#coverage). The only difference is that, by setting it inside +the job level, whatever is set in there will take precedence over what has +been defined in the global level. A quick example of one overwritting the +other would be: + +```yaml +coverage: + output_filter: /\(\d+\.\d+\) covered\./ + +job1: + coverage: + output_filter: /Code coverage: \d+\.\d+/ +``` + +In the example above, considering the context of the job `job1`, the coverage +regex that would be used is `/Code coverage: \d+\.\d+/` instead of +`/\(\d+\.\d+\) covered\./`. + ## Git Strategy > Introduced in GitLab 8.9 as an experimental feature. May change or be removed -- cgit v1.2.1 From 0713a7c3a9eb1bcfdf6adde0c3365549c19a3ee1 Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Mon, 21 Nov 2016 02:38:03 -0200 Subject: Add specs to cover the implemented feature and fix a small bug --- lib/gitlab/ci/config/entry/coverage.rb | 2 +- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 31 ++++++++++++++++++ spec/lib/gitlab/ci/config/entry/coverage_spec.rb | 40 ++++++++++++++++++++++++ spec/lib/gitlab/ci/config/entry/global_spec.rb | 17 +++++++--- spec/lib/gitlab/ci/config/entry/job_spec.rb | 14 +++++++++ spec/models/ci/build_spec.rb | 33 +++++++++++++++++++ 6 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 spec/lib/gitlab/ci/config/entry/coverage_spec.rb diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb index e5da3cf23fd..af12837130c 100644 --- a/lib/gitlab/ci/config/entry/coverage.rb +++ b/lib/gitlab/ci/config/entry/coverage.rb @@ -22,7 +22,7 @@ module Gitlab if output_filter_value.start_with?('/') && output_filter_value.end_with?('/') output_filter_value[1...-1] else - value[:output_filter] + @config[:output_filter] end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index f824e2e1efe..eb2d9c6e0e3 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -4,6 +4,37 @@ module Ci describe GitlabCiYamlProcessor, lib: true do let(:path) { 'path' } + describe '#build_attributes' do + context 'Coverage entry' do + subject { described_class.new(config, path).build_attributes(:rspec) } + + let(:config_base) { { rspec: { script: "rspec" } } } + let(:config) { YAML.dump(config_base) } + + context 'when config has coverage set at the global scope' do + before do + config_base.update( + coverage: { output_filter: '\(\d+\.\d+\) covered' } + ) + end + + context 'and \'rspec\' job doesn\'t have coverage set' do + it { is_expected.to include(coverage_regex: '\(\d+\.\d+\) covered') } + end + + context 'but \'rspec\' job also has coverage set' do + before do + config_base[:rspec].update( + coverage: { output_filter: '/Code coverage: \d+\.\d+/' } + ) + end + + it { is_expected.to include(coverage_regex: 'Code coverage: \d+\.\d+') } + end + end + end + end + describe "#builds_for_ref" do let(:type) { 'test' } diff --git a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb new file mode 100644 index 00000000000..9e59755d9f8 --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Entry::Coverage do + let(:entry) { described_class.new(config) } + + describe 'validations' do + context 'when entry config value is correct' do + let(:config) { { output_filter: 'Code coverage: \d+\.\d+' } } + + describe '#value' do + subject { entry.value } + it { is_expected.to eq config } + end + + describe '#errors' do + subject { entry.errors } + it { is_expected.to be_empty } + end + + describe '#valid?' do + subject { entry } + it { is_expected.to be_valid } + end + end + + context 'when entry value is not correct' do + let(:config) { { output_filter: '(malformed regexp' } } + + describe '#errors' do + subject { entry.errors } + it { is_expected.to include /coverage output filter must be a regular expression/ } + end + + describe '#valid?' do + subject { entry } + it { is_expected.not_to be_valid } + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index e64c8d46bd8..66a1380bc61 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -4,12 +4,19 @@ describe Gitlab::Ci::Config::Entry::Global do let(:global) { described_class.new(hash) } describe '.nodes' do - it 'can contain global config keys' do - expect(described_class.nodes).to include :before_script - end + subject { described_class.nodes } + + it { is_expected.to be_a Hash } + + context 'when filtering all the entry/node names' do + subject { described_class.nodes.keys } + + let(:result) do + %i[before_script image services after_script variables stages types + cache coverage] + end - it 'returns a hash' do - expect(described_class.nodes).to be_a Hash + it { is_expected.to match_array result } end end diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index fc9b8b86dc4..d20f4ec207d 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -3,6 +3,20 @@ require 'spec_helper' describe Gitlab::Ci::Config::Entry::Job do let(:entry) { described_class.new(config, name: :rspec) } + describe '.nodes' do + context 'when filtering all the entry/node names' do + subject { described_class.nodes.keys } + + let(:result) do + %i[before_script script stage type after_script cache + image services only except variables artifacts + environment coverage] + end + + it { is_expected.to match_array result } + end + end + describe 'validations' do before { entry.compose! } diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index f031876e812..9e5481017e2 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -221,6 +221,39 @@ describe Ci::Build, :models do end end + describe '#coverage_regex' do + subject { build.coverage_regex } + let(:project_regex) { '\(\d+\.\d+\) covered' } + let(:build_regex) { 'Code coverage: \d+\.\d+' } + + context 'when project has build_coverage_regex set' do + before { project.build_coverage_regex = project_regex } + + context 'and coverage_regex attribute is not set' do + it { is_expected.to eq(project_regex) } + end + + context 'but coverage_regex attribute is also set' do + before { build.coverage_regex = build_regex } + it { is_expected.to eq(build_regex) } + end + end + + context 'when neither project nor build has coverage regex set' do + it { is_expected.to be_nil } + end + end + + describe '#update_coverage' do + it 'grants coverage_regex method is called inside of it' do + build.coverage_regex = '\(\d+.\d+\%\) covered' + allow(build).to receive(:trace) { 'Coverage 1033 / 1051 LOC (98.29%) covered' } + allow(build).to receive(:coverage_regex).and_call_original + allow(build).to receive(:update_attributes).with(coverage: 98.29) { true } + expect(build.update_coverage).to be true + end + end + describe 'deployment' do describe '#last_deployment' do subject { build.last_deployment } -- cgit v1.2.1 From bb12ee051f95ee747c0e2b98a85675de53dca8ea Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Mon, 21 Nov 2016 02:51:29 -0200 Subject: Fix wrong description for Coverage entry (in ruby comments) --- lib/gitlab/ci/config/entry/coverage.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb index af12837130c..41e1d6e0c86 100644 --- a/lib/gitlab/ci/config/entry/coverage.rb +++ b/lib/gitlab/ci/config/entry/coverage.rb @@ -3,7 +3,7 @@ module Gitlab class Config module Entry ## - # Entry that represents a Regular Expression. + # Entry that represents Coverage settings. # class Coverage < Node include Validatable -- cgit v1.2.1 From f1e920ed86133bfea0abfc66ca44282813822073 Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Sat, 26 Nov 2016 01:02:08 -0200 Subject: Simplify coverage setting and comply to some requests in code review --- doc/ci/yaml/README.md | 30 ++++++------------------ lib/ci/gitlab_ci_yaml_processor.rb | 2 +- lib/gitlab/ci/config/entry/coverage.rb | 20 ++++------------ spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 17 ++++---------- spec/lib/gitlab/ci/config/entry/coverage_spec.rb | 14 +++++------ spec/models/ci/build_spec.rb | 7 +++--- 6 files changed, 28 insertions(+), 62 deletions(-) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index a8c0721bbcc..0a264c0e228 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -286,24 +286,11 @@ build outputs. Setting this up globally will make all the jobs to use this setting for output filtering and extracting the coverage information from your builds. -#### coverage:output_filter - -For now, there is only the `output_filter` directive expected to be inside the -`coverage` entry. And it is expected to be a regular expression. - -So, in the end, you're going to have something like the following: +Regular expressions are used by default. So using surrounding `/` is optional, given it'll always be read as a regular expression. Don't forget to escape special characters whenever you want to match them in the regular expression. +A simple example: ```yaml -coverage: - output_filter: /\(\d+\.\d+\) covered\./ -``` - -It's worth to keep in mind that the surrounding `/` is optional. So, the above -example is the same as the following: - -```yaml -coverage: - output_filter: \(\d+\.\d+\) covered\. +coverage: \(\d+\.\d+\) covered\. ``` ## Jobs @@ -347,7 +334,6 @@ job_name: | before_script | no | Override a set of commands that are executed before build | | after_script | no | Override a set of commands that are executed after build | | environment | no | Defines a name of environment to which deployment is done by this build | -| environment | no | Defines a name of environment to which deployment is done by this build | | coverage | no | Define coverage settings for a given job | ### script @@ -1032,17 +1018,15 @@ been defined in the global level. A quick example of one overwritting the other would be: ```yaml -coverage: - output_filter: /\(\d+\.\d+\) covered\./ +coverage: \(\d+\.\d+\) covered\. job1: - coverage: - output_filter: /Code coverage: \d+\.\d+/ + coverage: Code coverage: \d+\.\d+ ``` In the example above, considering the context of the job `job1`, the coverage -regex that would be used is `/Code coverage: \d+\.\d+/` instead of -`/\(\d+\.\d+\) covered\./`. +regex that would be used is `Code coverage: \d+\.\d+` instead of +`\(\d+\.\d+\) covered\.`. ## Git Strategy diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 02944e0385a..649ee4d018b 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -61,7 +61,7 @@ module Ci allow_failure: job[:allow_failure] || false, when: job[:when] || 'on_success', environment: job[:environment_name], - coverage_regex: job[:coverage][:output_filter], + coverage_regex: job[:coverage], yaml_variables: yaml_variables(name), options: { image: job[:image], diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb index 41e1d6e0c86..aa738fcfd11 100644 --- a/lib/gitlab/ci/config/entry/coverage.rb +++ b/lib/gitlab/ci/config/entry/coverage.rb @@ -8,27 +8,17 @@ module Gitlab class Coverage < Node include Validatable - ALLOWED_KEYS = %i[output_filter] - validations do - validates :config, type: Hash - validates :config, allowed_keys: ALLOWED_KEYS - validates :output_filter, regexp: true + validates :config, regexp: true end - def output_filter - output_filter_value = @config[:output_filter].to_s - - if output_filter_value.start_with?('/') && output_filter_value.end_with?('/') - output_filter_value[1...-1] + def value + if @config.start_with?('/') && @config.end_with?('/') + @config[1...-1] else - @config[:output_filter] + @config end end - - def value - @config.merge(output_filter: output_filter) - end end end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index eb2d9c6e0e3..ac706216d5a 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -9,26 +9,17 @@ module Ci subject { described_class.new(config, path).build_attributes(:rspec) } let(:config_base) { { rspec: { script: "rspec" } } } - let(:config) { YAML.dump(config_base) } + let(:config) { YAML.dump(config_base) } context 'when config has coverage set at the global scope' do - before do - config_base.update( - coverage: { output_filter: '\(\d+\.\d+\) covered' } - ) - end + before { config_base.update(coverage: '\(\d+\.\d+\) covered') } - context 'and \'rspec\' job doesn\'t have coverage set' do + context "and 'rspec' job doesn't have coverage set" do it { is_expected.to include(coverage_regex: '\(\d+\.\d+\) covered') } end context 'but \'rspec\' job also has coverage set' do - before do - config_base[:rspec].update( - coverage: { output_filter: '/Code coverage: \d+\.\d+/' } - ) - end - + before { config_base[:rspec][:coverage] = '/Code coverage: \d+\.\d+/' } it { is_expected.to include(coverage_regex: 'Code coverage: \d+\.\d+') } end end diff --git a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb index 9e59755d9f8..0549dbc732b 100644 --- a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb @@ -5,35 +5,35 @@ describe Gitlab::Ci::Config::Entry::Coverage do describe 'validations' do context 'when entry config value is correct' do - let(:config) { { output_filter: 'Code coverage: \d+\.\d+' } } + let(:config) { 'Code coverage: \d+\.\d+' } describe '#value' do subject { entry.value } - it { is_expected.to eq config } + it { is_expected.to eq config } end describe '#errors' do subject { entry.errors } - it { is_expected.to be_empty } + it { is_expected.to be_empty } end describe '#valid?' do subject { entry } - it { is_expected.to be_valid } + it { is_expected.to be_valid } end end context 'when entry value is not correct' do - let(:config) { { output_filter: '(malformed regexp' } } + let(:config) { '(malformed regexp' } describe '#errors' do subject { entry.errors } - it { is_expected.to include /coverage output filter must be a regular expression/ } + it { is_expected.to include /coverage config must be a regular expression/ } end describe '#valid?' do subject { entry } - it { is_expected.not_to be_valid } + it { is_expected.not_to be_valid } end end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 9e5481017e2..7c054dd95f5 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -222,9 +222,10 @@ describe Ci::Build, :models do end describe '#coverage_regex' do - subject { build.coverage_regex } + subject { build.coverage_regex } + let(:project_regex) { '\(\d+\.\d+\) covered' } - let(:build_regex) { 'Code coverage: \d+\.\d+' } + let(:build_regex) { 'Code coverage: \d+\.\d+' } context 'when project has build_coverage_regex set' do before { project.build_coverage_regex = project_regex } @@ -235,7 +236,7 @@ describe Ci::Build, :models do context 'but coverage_regex attribute is also set' do before { build.coverage_regex = build_regex } - it { is_expected.to eq(build_regex) } + it { is_expected.to eq(build_regex) } end end -- cgit v1.2.1 From 6323cd7203dbf1850e7939e81db4b1a9c6cf6d76 Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Mon, 5 Dec 2016 02:00:47 -0200 Subject: Comply to more requirements and requests made in the code review --- app/models/ci/build.rb | 2 +- doc/ci/yaml/README.md | 16 +++++++++------- lib/gitlab/ci/config/entry/coverage.rb | 2 +- lib/gitlab/ci/config/entry/legacy_validation_helpers.rb | 5 ++--- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 9 +++++++-- spec/models/ci/build_spec.rb | 11 ++++++++--- 6 files changed, 28 insertions(+), 17 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 46a6b4c724a..951818ad561 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -522,7 +522,7 @@ module Ci end def coverage_regex - read_attribute(:coverage_regex) || project.build_coverage_regex + super || project.build_coverage_regex end def when diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 0a264c0e228..5e2d9788f33 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -286,11 +286,13 @@ build outputs. Setting this up globally will make all the jobs to use this setting for output filtering and extracting the coverage information from your builds. -Regular expressions are used by default. So using surrounding `/` is optional, given it'll always be read as a regular expression. Don't forget to escape special characters whenever you want to match them in the regular expression. +Regular expressions are used by default. So using surrounding `/` is optional, +given it'll always be read as a regular expression. Don't forget to escape +special characters whenever you want to match them literally. A simple example: ```yaml -coverage: \(\d+\.\d+\) covered\. +coverage: /\(\d+\.\d+\) covered\./ ``` ## Jobs @@ -1014,19 +1016,19 @@ job: This entry is pretty much the same as described in the global context in [`coverage`](#coverage). The only difference is that, by setting it inside the job level, whatever is set in there will take precedence over what has -been defined in the global level. A quick example of one overwritting the +been defined in the global level. A quick example of one overriding the other would be: ```yaml -coverage: \(\d+\.\d+\) covered\. +coverage: /\(\d+\.\d+\) covered\./ job1: - coverage: Code coverage: \d+\.\d+ + coverage: /Code coverage: \d+\.\d+/ ``` In the example above, considering the context of the job `job1`, the coverage -regex that would be used is `Code coverage: \d+\.\d+` instead of -`\(\d+\.\d+\) covered\.`. +regex that would be used is `/Code coverage: \d+\.\d+/` instead of +`/\(\d+\.\d+\) covered\./`. ## Git Strategy diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb index aa738fcfd11..706bfc882de 100644 --- a/lib/gitlab/ci/config/entry/coverage.rb +++ b/lib/gitlab/ci/config/entry/coverage.rb @@ -13,7 +13,7 @@ module Gitlab end def value - if @config.start_with?('/') && @config.end_with?('/') + if @config.first == '/' && @config.last == '/' @config[1...-1] else @config diff --git a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb index d8e74b15712..9b9a0a8125a 100644 --- a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb +++ b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb @@ -29,8 +29,7 @@ module Gitlab end def validate_regexp(value) - Regexp.new(value) - true + !value.nil? && Regexp.new(value.to_s) && true rescue RegexpError, TypeError false end @@ -39,7 +38,7 @@ module Gitlab return true if value.is_a?(Symbol) return false unless value.is_a?(String) - if value.start_with?('/') && value.end_with?('/') + if value.first == '/' && value.last == '/' validate_regexp(value[1...-1]) else true diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index ac706216d5a..3ffcfaa1f29 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -12,14 +12,19 @@ module Ci let(:config) { YAML.dump(config_base) } context 'when config has coverage set at the global scope' do - before { config_base.update(coverage: '\(\d+\.\d+\) covered') } + before do + config_base.update(coverage: '\(\d+\.\d+\) covered') + end context "and 'rspec' job doesn't have coverage set" do it { is_expected.to include(coverage_regex: '\(\d+\.\d+\) covered') } end context 'but \'rspec\' job also has coverage set' do - before { config_base[:rspec][:coverage] = '/Code coverage: \d+\.\d+/' } + before do + config_base[:rspec][:coverage] = '/Code coverage: \d+\.\d+/' + end + it { is_expected.to include(coverage_regex: 'Code coverage: \d+\.\d+') } end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 7c054dd95f5..7baaef9c85e 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -228,14 +228,19 @@ describe Ci::Build, :models do let(:build_regex) { 'Code coverage: \d+\.\d+' } context 'when project has build_coverage_regex set' do - before { project.build_coverage_regex = project_regex } + before do + project.build_coverage_regex = project_regex + end context 'and coverage_regex attribute is not set' do it { is_expected.to eq(project_regex) } end context 'but coverage_regex attribute is also set' do - before { build.coverage_regex = build_regex } + before do + build.coverage_regex = build_regex + end + it { is_expected.to eq(build_regex) } end end @@ -250,7 +255,7 @@ describe Ci::Build, :models do build.coverage_regex = '\(\d+.\d+\%\) covered' allow(build).to receive(:trace) { 'Coverage 1033 / 1051 LOC (98.29%) covered' } allow(build).to receive(:coverage_regex).and_call_original - allow(build).to receive(:update_attributes).with(coverage: 98.29) { true } + expect(build).to receive(:update_attributes).with(coverage: 98.29) { true } expect(build.update_coverage).to be true end end -- cgit v1.2.1 From f8bec0d1fb05d2c3e87a0470579ee7a650ade23c Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Tue, 6 Dec 2016 02:39:59 -0200 Subject: Improve specs styles/organization and add some more specs --- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 2 +- spec/lib/gitlab/ci/config/entry/coverage_spec.rb | 25 +++++++++++++++++++++--- spec/lib/gitlab/ci/config/entry/global_spec.rb | 18 ++++++++--------- spec/models/ci/build_spec.rb | 13 ++++++------ 4 files changed, 38 insertions(+), 20 deletions(-) diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 3ffcfaa1f29..b1e09350847 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -20,7 +20,7 @@ module Ci it { is_expected.to include(coverage_regex: '\(\d+\.\d+\) covered') } end - context 'but \'rspec\' job also has coverage set' do + context "but 'rspec' job also has coverage set" do before do config_base[:rspec][:coverage] = '/Code coverage: \d+\.\d+/' end diff --git a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb index 0549dbc732b..8f989ebd732 100644 --- a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb @@ -4,12 +4,31 @@ describe Gitlab::Ci::Config::Entry::Coverage do let(:entry) { described_class.new(config) } describe 'validations' do - context 'when entry config value is correct' do + context "when entry config value is correct without surrounding '/'" do let(:config) { 'Code coverage: \d+\.\d+' } describe '#value' do subject { entry.value } - it { is_expected.to eq config } + it { is_expected.to eq(config) } + end + + describe '#errors' do + subject { entry.errors } + it { is_expected.to be_empty } + end + + describe '#valid?' do + subject { entry } + it { is_expected.to be_valid } + end + end + + context "when entry config value is correct with surrounding '/'" do + let(:config) { '/Code coverage: \d+\.\d+/' } + + describe '#value' do + subject { entry.value } + it { is_expected.to eq(config[1...-1]) } end describe '#errors' do @@ -28,7 +47,7 @@ describe Gitlab::Ci::Config::Entry::Coverage do describe '#errors' do subject { entry.errors } - it { is_expected.to include /coverage config must be a regular expression/ } + it { is_expected.to include(/coverage config must be a regular expression/) } end describe '#valid?' do diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index 66a1380bc61..7b7f5761ebd 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -4,19 +4,17 @@ describe Gitlab::Ci::Config::Entry::Global do let(:global) { described_class.new(hash) } describe '.nodes' do - subject { described_class.nodes } - - it { is_expected.to be_a Hash } + it 'returns a hash' do + expect(described_class.nodes).to be_a(Hash) + end context 'when filtering all the entry/node names' do - subject { described_class.nodes.keys } - - let(:result) do - %i[before_script image services after_script variables stages types - cache coverage] + it 'contains the expected node names' do + node_names = described_class.nodes.keys + expect(node_names).to match_array(%i[before_script image services + after_script variables stages + types cache coverage]) end - - it { is_expected.to match_array result } end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 7baaef9c85e..52cc45f07b2 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -251,12 +251,13 @@ describe Ci::Build, :models do end describe '#update_coverage' do - it 'grants coverage_regex method is called inside of it' do - build.coverage_regex = '\(\d+.\d+\%\) covered' - allow(build).to receive(:trace) { 'Coverage 1033 / 1051 LOC (98.29%) covered' } - allow(build).to receive(:coverage_regex).and_call_original - expect(build).to receive(:update_attributes).with(coverage: 98.29) { true } - expect(build.update_coverage).to be true + context "regarding coverage_regex's value," do + it "saves the correct extracted coverage value" do + build.coverage_regex = '\(\d+.\d+\%\) covered' + allow(build).to receive(:trace) { 'Coverage 1033 / 1051 LOC (98.29%) covered' } + expect(build).to receive(:update_attributes).with(coverage: 98.29) { true } + expect(build.update_coverage).to be true + end end end -- cgit v1.2.1 From be7106a145b1e3d4c6e06503e0f7f3032ace3764 Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Wed, 7 Dec 2016 03:01:34 -0200 Subject: Force coverage value to always be surrounded by '/' --- app/models/ci/build.rb | 10 ++++-- doc/ci/yaml/README.md | 7 +++-- lib/gitlab/ci/config/entry/coverage.rb | 8 ----- lib/gitlab/ci/config/entry/trigger.rb | 10 +----- lib/gitlab/ci/config/entry/validators.rb | 39 ++++++++++++++++++++++++ spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 6 ++-- spec/lib/gitlab/ci/config/entry/coverage_spec.rb | 17 ++++------- spec/models/ci/build_spec.rb | 9 +++--- 8 files changed, 65 insertions(+), 41 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 951818ad561..e3753869b67 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -275,8 +275,10 @@ module Ci end def update_coverage - return unless project - return unless regex = self.coverage_regex + regex = coverage_regex.to_s[1...-1] + + return if regex.blank? + coverage = extract_coverage(trace, regex) if coverage.is_a? Numeric @@ -522,7 +524,9 @@ module Ci end def coverage_regex - super || project.build_coverage_regex + super || + project.try(:build_coverage_regex).presence && + "/#{project.try(:build_coverage_regex)}/" end def when diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 5e2d9788f33..85b2c75cee8 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -286,9 +286,10 @@ build outputs. Setting this up globally will make all the jobs to use this setting for output filtering and extracting the coverage information from your builds. -Regular expressions are used by default. So using surrounding `/` is optional, -given it'll always be read as a regular expression. Don't forget to escape -special characters whenever you want to match them literally. +Regular expressions are the only valid kind of value expected here. So, using +surrounding `/` is mandatory in order to consistently and explicitly represent +a regular expression string. You must escape special characters if you want to +match them literally. A simple example: ```yaml diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb index 706bfc882de..25546f363fb 100644 --- a/lib/gitlab/ci/config/entry/coverage.rb +++ b/lib/gitlab/ci/config/entry/coverage.rb @@ -11,14 +11,6 @@ module Gitlab validations do validates :config, regexp: true end - - def value - if @config.first == '/' && @config.last == '/' - @config[1...-1] - else - @config - end - end end end end diff --git a/lib/gitlab/ci/config/entry/trigger.rb b/lib/gitlab/ci/config/entry/trigger.rb index 28b0a9ffe01..16b234e6c59 100644 --- a/lib/gitlab/ci/config/entry/trigger.rb +++ b/lib/gitlab/ci/config/entry/trigger.rb @@ -9,15 +9,7 @@ module Gitlab include Validatable validations do - include LegacyValidationHelpers - - validate :array_of_strings_or_regexps - - def array_of_strings_or_regexps - unless validate_array_of_strings_or_regexps(config) - errors.add(:config, 'should be an array of strings or regexps') - end - end + validates :config, array_of_strings_or_regexps: true end end end diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb index 03a8205b081..5f50b80af6c 100644 --- a/lib/gitlab/ci/config/entry/validators.rb +++ b/lib/gitlab/ci/config/entry/validators.rb @@ -62,6 +62,45 @@ module Gitlab record.errors.add(attribute, 'must be a regular expression') end end + + private + + def look_like_regexp?(value) + value =~ %r{\A/.*/\z} + end + + def validate_regexp(value) + look_like_regexp?(value) && + Regexp.new(value.to_s[1...-1]) && + true + rescue RegexpError + false + end + end + + class ArrayOfStringsOrRegexps < RegexpValidator + def validate_each(record, attribute, value) + unless validate_array_of_strings_or_regexps(value) + record.errors.add(attribute, 'should be an array of strings or regexps') + end + end + + private + + def validate_array_of_strings_or_regexps(values) + values.is_a?(Array) && values.all?(&method(:validate_string_or_regexp)) + end + + def validate_string_or_regexp(value) + return true if value.is_a?(Symbol) + return false unless value.is_a?(String) + + if look_like_regexp?(value) + validate_regexp(value) + else + true + end + end end class TypeValidator < ActiveModel::EachValidator diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index b1e09350847..e2302f5968a 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -13,11 +13,11 @@ module Ci context 'when config has coverage set at the global scope' do before do - config_base.update(coverage: '\(\d+\.\d+\) covered') + config_base.update(coverage: '/\(\d+\.\d+\) covered/') end context "and 'rspec' job doesn't have coverage set" do - it { is_expected.to include(coverage_regex: '\(\d+\.\d+\) covered') } + it { is_expected.to include(coverage_regex: '/\(\d+\.\d+\) covered/') } end context "but 'rspec' job also has coverage set" do @@ -25,7 +25,7 @@ module Ci config_base[:rspec][:coverage] = '/Code coverage: \d+\.\d+/' end - it { is_expected.to include(coverage_regex: 'Code coverage: \d+\.\d+') } + it { is_expected.to include(coverage_regex: '/Code coverage: \d+\.\d+/') } end end end diff --git a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb index 8f989ebd732..eb04075f1be 100644 --- a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb @@ -4,31 +4,26 @@ describe Gitlab::Ci::Config::Entry::Coverage do let(:entry) { described_class.new(config) } describe 'validations' do - context "when entry config value is correct without surrounding '/'" do + context "when entry config value doesn't have the surrounding '/'" do let(:config) { 'Code coverage: \d+\.\d+' } - describe '#value' do - subject { entry.value } - it { is_expected.to eq(config) } - end - describe '#errors' do subject { entry.errors } - it { is_expected.to be_empty } + it { is_expected.to include(/coverage config must be a regular expression/) } end describe '#valid?' do subject { entry } - it { is_expected.to be_valid } + it { is_expected.not_to be_valid } end end - context "when entry config value is correct with surrounding '/'" do + context "when entry config value has the surrounding '/'" do let(:config) { '/Code coverage: \d+\.\d+/' } describe '#value' do subject { entry.value } - it { is_expected.to eq(config[1...-1]) } + it { is_expected.to eq(config) } end describe '#errors' do @@ -42,7 +37,7 @@ describe Gitlab::Ci::Config::Entry::Coverage do end end - context 'when entry value is not correct' do + context 'when entry value is not valid' do let(:config) { '(malformed regexp' } describe '#errors' do diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 52cc45f07b2..f23155a5d13 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -224,19 +224,20 @@ describe Ci::Build, :models do describe '#coverage_regex' do subject { build.coverage_regex } - let(:project_regex) { '\(\d+\.\d+\) covered' } - let(:build_regex) { 'Code coverage: \d+\.\d+' } - context 'when project has build_coverage_regex set' do + let(:project_regex) { '\(\d+\.\d+\) covered' } + before do project.build_coverage_regex = project_regex end context 'and coverage_regex attribute is not set' do - it { is_expected.to eq(project_regex) } + it { is_expected.to eq("/#{project_regex}/") } end context 'but coverage_regex attribute is also set' do + let(:build_regex) { '/Code coverage: \d+\.\d+/' } + before do build.coverage_regex = build_regex end -- cgit v1.2.1 From 518fd2eb93711e1e9c3d597a6bdf13366d9abdb5 Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Fri, 9 Dec 2016 00:20:39 -0200 Subject: Improve/polish code logic for some Ci::Build methods --- app/models/ci/build.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index e3753869b67..2a613d12913 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -275,11 +275,11 @@ module Ci end def update_coverage - regex = coverage_regex.to_s[1...-1] + regex = coverage_regex - return if regex.blank? + return unless regex - coverage = extract_coverage(trace, regex) + coverage = extract_coverage(trace, regex[1...-1]) if coverage.is_a? Numeric update_attributes(coverage: coverage) @@ -526,7 +526,7 @@ module Ci def coverage_regex super || project.try(:build_coverage_regex).presence && - "/#{project.try(:build_coverage_regex)}/" + "/#{project.build_coverage_regex}/" end def when -- cgit v1.2.1 From 8fe708f4a2850d71c11234b234e039b2a9422299 Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Tue, 13 Dec 2016 02:53:12 -0200 Subject: Make more code improvements around the '/' stripping logic --- app/models/ci/build.rb | 35 +++++++++--------------- lib/gitlab/ci/config/entry/coverage.rb | 4 +++ lib/gitlab/ci/config/entry/validators.rb | 12 +++----- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 11 ++++++-- spec/lib/gitlab/ci/config/entry/coverage_spec.rb | 2 +- spec/lib/gitlab/ci/config/entry/global_spec.rb | 4 +-- spec/models/ci/build_spec.rb | 4 +-- 7 files changed, 35 insertions(+), 37 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 2a613d12913..62fec28d2d5 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -275,30 +275,23 @@ module Ci end def update_coverage - regex = coverage_regex - - return unless regex - - coverage = extract_coverage(trace, regex[1...-1]) - - if coverage.is_a? Numeric - update_attributes(coverage: coverage) - end + coverage = extract_coverage(trace, coverage_regex) + update_attributes(coverage: coverage) if coverage.is_a?(Numeric) end def extract_coverage(text, regex) - begin - matches = text.scan(Regexp.new(regex)).last - matches = matches.last if matches.kind_of?(Array) - coverage = matches.gsub(/\d+(\.\d+)?/).first + return unless regex - if coverage.present? - coverage.to_f - end - rescue - # if bad regex or something goes wrong we dont want to interrupt transition - # so we just silentrly ignore error for now + matches = text.scan(Regexp.new(regex)).last + matches = matches.last if matches.kind_of?(Array) + coverage = matches.gsub(/\d+(\.\d+)?/).first + + if coverage.present? + coverage.to_f end + rescue + # if bad regex or something goes wrong we dont want to interrupt transition + # so we just silentrly ignore error for now end def has_trace_file? @@ -524,9 +517,7 @@ module Ci end def coverage_regex - super || - project.try(:build_coverage_regex).presence && - "/#{project.build_coverage_regex}/" + super || project.try(:build_coverage_regex) end def when diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb index 25546f363fb..12a063059cb 100644 --- a/lib/gitlab/ci/config/entry/coverage.rb +++ b/lib/gitlab/ci/config/entry/coverage.rb @@ -11,6 +11,10 @@ module Gitlab validations do validates :config, regexp: true end + + def value + @config[1...-1] + end end end end diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb index 5f50b80af6c..30c52dd65e8 100644 --- a/lib/gitlab/ci/config/entry/validators.rb +++ b/lib/gitlab/ci/config/entry/validators.rb @@ -66,7 +66,7 @@ module Gitlab private def look_like_regexp?(value) - value =~ %r{\A/.*/\z} + value.start_with?('/') && value.end_with?('/') end def validate_regexp(value) @@ -78,7 +78,7 @@ module Gitlab end end - class ArrayOfStringsOrRegexps < RegexpValidator + class ArrayOfStringsOrRegexpsValidator < RegexpValidator def validate_each(record, attribute, value) unless validate_array_of_strings_or_regexps(value) record.errors.add(attribute, 'should be an array of strings or regexps') @@ -94,12 +94,8 @@ module Gitlab def validate_string_or_regexp(value) return true if value.is_a?(Symbol) return false unless value.is_a?(String) - - if look_like_regexp?(value) - validate_regexp(value) - else - true - end + return validate_regexp(value) if look_like_regexp?(value) + true end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index e2302f5968a..49349035b3b 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -17,7 +17,7 @@ module Ci end context "and 'rspec' job doesn't have coverage set" do - it { is_expected.to include(coverage_regex: '/\(\d+\.\d+\) covered/') } + it { is_expected.to include(coverage_regex: '\(\d+\.\d+\) covered') } end context "but 'rspec' job also has coverage set" do @@ -25,7 +25,7 @@ module Ci config_base[:rspec][:coverage] = '/Code coverage: \d+\.\d+/' end - it { is_expected.to include(coverage_regex: '/Code coverage: \d+\.\d+/') } + it { is_expected.to include(coverage_regex: 'Code coverage: \d+\.\d+') } end end end @@ -48,6 +48,7 @@ module Ci stage_idx: 1, name: "rspec", commands: "pwd\nrspec", + coverage_regex: nil, tag_list: [], options: {}, allow_failure: false, @@ -462,6 +463,7 @@ module Ci stage_idx: 1, name: "rspec", commands: "pwd\nrspec", + coverage_regex: nil, tag_list: [], options: { image: "ruby:2.1", @@ -490,6 +492,7 @@ module Ci stage_idx: 1, name: "rspec", commands: "pwd\nrspec", + coverage_regex: nil, tag_list: [], options: { image: "ruby:2.5", @@ -729,6 +732,7 @@ module Ci stage_idx: 1, name: "rspec", commands: "pwd\nrspec", + coverage_regex: nil, tag_list: [], options: { image: "ruby:2.1", @@ -940,6 +944,7 @@ module Ci stage_idx: 1, name: "normal_job", commands: "test", + coverage_regex: nil, tag_list: [], options: {}, when: "on_success", @@ -985,6 +990,7 @@ module Ci stage_idx: 0, name: "job1", commands: "execute-script-for-job", + coverage_regex: nil, tag_list: [], options: {}, when: "on_success", @@ -997,6 +1003,7 @@ module Ci stage_idx: 0, name: "job2", commands: "execute-script-for-job", + coverage_regex: nil, tag_list: [], options: {}, when: "on_success", diff --git a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb index eb04075f1be..4c6bd859552 100644 --- a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb @@ -23,7 +23,7 @@ describe Gitlab::Ci::Config::Entry::Coverage do describe '#value' do subject { entry.value } - it { is_expected.to eq(config) } + it { is_expected.to eq(config[1...-1]) } end describe '#errors' do diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index 7b7f5761ebd..d4f1780b174 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -40,7 +40,7 @@ describe Gitlab::Ci::Config::Entry::Global do end it 'creates node object for each entry' do - expect(global.descendants.count).to eq 8 + expect(global.descendants.count).to eq 9 end it 'creates node object using valid class' do @@ -181,7 +181,7 @@ describe Gitlab::Ci::Config::Entry::Global do describe '#nodes' do it 'instantizes all nodes' do - expect(global.descendants.count).to eq 8 + expect(global.descendants.count).to eq 9 end it 'contains unspecified nodes' do diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index f23155a5d13..fe0a9707b2a 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -232,11 +232,11 @@ describe Ci::Build, :models do end context 'and coverage_regex attribute is not set' do - it { is_expected.to eq("/#{project_regex}/") } + it { is_expected.to eq(project_regex) } end context 'but coverage_regex attribute is also set' do - let(:build_regex) { '/Code coverage: \d+\.\d+/' } + let(:build_regex) { 'Code coverage: \d+\.\d+' } before do build.coverage_regex = build_regex -- cgit v1.2.1 From 441a9beec3e6834d3fe5e047e65c4d8b32ff86d5 Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Wed, 18 Jan 2017 01:42:38 -0200 Subject: Make some other refinements to validation logic --- lib/gitlab/ci/config/entry/validators.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb index 30c52dd65e8..bd7428b1272 100644 --- a/lib/gitlab/ci/config/entry/validators.rb +++ b/lib/gitlab/ci/config/entry/validators.rb @@ -66,7 +66,8 @@ module Gitlab private def look_like_regexp?(value) - value.start_with?('/') && value.end_with?('/') + value.is_a?(String) && value.start_with?('/') && + value.end_with?('/') end def validate_regexp(value) @@ -92,7 +93,6 @@ module Gitlab end def validate_string_or_regexp(value) - return true if value.is_a?(Symbol) return false unless value.is_a?(String) return validate_regexp(value) if look_like_regexp?(value) true -- cgit v1.2.1 From 1c24c79a83bff0d1535d813eb8146fc799d5d8ac Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Wed, 25 Jan 2017 01:48:03 -0200 Subject: Be more lenient on build coverage value updates and fix specs --- app/models/ci/build.rb | 2 +- spec/lib/gitlab/import_export/safe_model_attributes.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 62fec28d2d5..b1f77bf242c 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -276,7 +276,7 @@ module Ci def update_coverage coverage = extract_coverage(trace, coverage_regex) - update_attributes(coverage: coverage) if coverage.is_a?(Numeric) + update_attributes(coverage: coverage) if coverage.present? end def extract_coverage(text, regex) diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 493bc2db21a..95b230e4f5c 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -222,6 +222,7 @@ CommitStatus: - queued_at - token - lock_version +- coverage_regex Ci::Variable: - id - project_id -- cgit v1.2.1 From dbda72a79999998bfd1d77b3102bc16053a2685e Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Thu, 26 Jan 2017 15:30:34 +0100 Subject: Rename presenters for consitency --- lib/gitlab/chat_commands/command.rb | 2 +- lib/gitlab/chat_commands/issue_create.rb | 42 ------------ lib/gitlab/chat_commands/issue_new.rb | 42 ++++++++++++ lib/gitlab/chat_commands/issue_search.rb | 8 +-- lib/gitlab/chat_commands/issue_show.rb | 2 +- lib/gitlab/chat_commands/presenters/deploy.rb | 8 --- lib/gitlab/chat_commands/presenters/help.rb | 6 +- lib/gitlab/chat_commands/presenters/issuable.rb | 4 +- lib/gitlab/chat_commands/presenters/issue_new.rb | 48 +++++++++++++ .../chat_commands/presenters/issue_search.rb | 45 +++++++++++++ lib/gitlab/chat_commands/presenters/issue_show.rb | 56 ++++++++++++++++ lib/gitlab/chat_commands/presenters/list_issues.rb | 43 ------------ lib/gitlab/chat_commands/presenters/new_issue.rb | 42 ------------ lib/gitlab/chat_commands/presenters/show_issue.rb | 54 --------------- spec/lib/gitlab/chat_commands/command_spec.rb | 2 +- spec/lib/gitlab/chat_commands/issue_create_spec.rb | 78 ---------------------- spec/lib/gitlab/chat_commands/issue_new_spec.rb | 78 ++++++++++++++++++++++ .../chat_commands/presenters/issue_new_spec.rb | 17 +++++ .../chat_commands/presenters/issue_search_spec.rb | 23 +++++++ .../chat_commands/presenters/issue_show_spec.rb | 27 ++++++++ .../chat_commands/presenters/list_issues_spec.rb | 23 ------- .../chat_commands/presenters/show_issue_spec.rb | 27 -------- 22 files changed, 346 insertions(+), 331 deletions(-) delete mode 100644 lib/gitlab/chat_commands/issue_create.rb create mode 100644 lib/gitlab/chat_commands/issue_new.rb create mode 100644 lib/gitlab/chat_commands/presenters/issue_new.rb create mode 100644 lib/gitlab/chat_commands/presenters/issue_search.rb create mode 100644 lib/gitlab/chat_commands/presenters/issue_show.rb delete mode 100644 lib/gitlab/chat_commands/presenters/list_issues.rb delete mode 100644 lib/gitlab/chat_commands/presenters/new_issue.rb delete mode 100644 lib/gitlab/chat_commands/presenters/show_issue.rb delete mode 100644 spec/lib/gitlab/chat_commands/issue_create_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/issue_new_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb delete mode 100644 spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb delete mode 100644 spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index 4e5031a8a26..e7baa20356c 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -3,7 +3,7 @@ module Gitlab class Command < BaseCommand COMMANDS = [ Gitlab::ChatCommands::IssueShow, - Gitlab::ChatCommands::IssueCreate, + Gitlab::ChatCommands::IssueNew, Gitlab::ChatCommands::IssueSearch, Gitlab::ChatCommands::Deploy, ].freeze diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb deleted file mode 100644 index 3f3d7de8b2e..00000000000 --- a/lib/gitlab/chat_commands/issue_create.rb +++ /dev/null @@ -1,42 +0,0 @@ -module Gitlab - module ChatCommands - class IssueCreate < IssueCommand - def self.match(text) - # we can not match \n with the dot by passing the m modifier as than - # the title and description are not seperated - /\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text) - end - - def self.help_message - 'issue new <title> *`⇧ Shift`*+*`↵ Enter`* <description>' - end - - def self.allowed?(project, user) - can?(user, :create_issue, project) - end - - def execute(match) - title = match[:title] - 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 - end - - def presenter(issue) - Gitlab::ChatCommands::Presenters::NewIssue.new(issue) - end - end - end -end diff --git a/lib/gitlab/chat_commands/issue_new.rb b/lib/gitlab/chat_commands/issue_new.rb new file mode 100644 index 00000000000..016054ecd46 --- /dev/null +++ b/lib/gitlab/chat_commands/issue_new.rb @@ -0,0 +1,42 @@ +module Gitlab + module ChatCommands + class IssueNew < IssueCommand + def self.match(text) + # we can not match \n with the dot by passing the m modifier as than + # the title and description are not seperated + /\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text) + end + + def self.help_message + 'issue new <title> *`⇧ Shift`*+*`↵ Enter`* <description>' + end + + def self.allowed?(project, user) + can?(user, :create_issue, project) + end + + def execute(match) + title = match[:title] + description = match[:description].to_s.rstrip + + issue = create_issue(title: title, description: description) + + if issue.persisted? + presenter(issue).present + else + presenter(issue).display_errors + end + end + + private + + def create_issue(title:, description:) + Issues::CreateService.new(project, current_user, title: title, description: description).execute + end + + def presenter(issue) + Gitlab::ChatCommands::Presenters::IssueNew.new(issue) + end + end + end +end diff --git a/lib/gitlab/chat_commands/issue_search.rb b/lib/gitlab/chat_commands/issue_search.rb index e2d3a0f466a..3491b53093e 100644 --- a/lib/gitlab/chat_commands/issue_search.rb +++ b/lib/gitlab/chat_commands/issue_search.rb @@ -12,12 +12,10 @@ module Gitlab def execute(match) 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 + if issues.present? + Presenters::IssueSearch.new(issues).present else - Presenters::ListIssues.new(issues).present + Presenters::Access.new(issues).not_found end end end diff --git a/lib/gitlab/chat_commands/issue_show.rb b/lib/gitlab/chat_commands/issue_show.rb index 9f3e1b9a64b..d6013f4d10c 100644 --- a/lib/gitlab/chat_commands/issue_show.rb +++ b/lib/gitlab/chat_commands/issue_show.rb @@ -13,7 +13,7 @@ module Gitlab issue = find_by_iid(match[:iid]) if issue - Gitlab::ChatCommands::Presenters::ShowIssue.new(issue).present + Gitlab::ChatCommands::Presenters::IssueShow.new(issue).present else Gitlab::ChatCommands::Presenters::Access.new.not_found end diff --git a/lib/gitlab/chat_commands/presenters/deploy.rb b/lib/gitlab/chat_commands/presenters/deploy.rb index b1cfaac15af..863d0bf99ca 100644 --- a/lib/gitlab/chat_commands/presenters/deploy.rb +++ b/lib/gitlab/chat_commands/presenters/deploy.rb @@ -15,14 +15,6 @@ module Gitlab 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 end diff --git a/lib/gitlab/chat_commands/presenters/help.rb b/lib/gitlab/chat_commands/presenters/help.rb index c7a67467b7e..39ad3249f5b 100644 --- a/lib/gitlab/chat_commands/presenters/help.rb +++ b/lib/gitlab/chat_commands/presenters/help.rb @@ -9,10 +9,10 @@ module Gitlab private def help_message(trigger) - if @resource.none? - "No commands available :thinking_face:" - else + if @resource.present? header_with_list("Available commands", full_commands(trigger)) + else + "No commands available :thinking_face:" end end diff --git a/lib/gitlab/chat_commands/presenters/issuable.rb b/lib/gitlab/chat_commands/presenters/issuable.rb index 2cb6b1525fc..dfb1c8f6616 100644 --- a/lib/gitlab/chat_commands/presenters/issuable.rb +++ b/lib/gitlab/chat_commands/presenters/issuable.rb @@ -1,9 +1,7 @@ module Gitlab module ChatCommands module Presenters - class Issuable < Presenters::Base - private - + module Issuable def color(issuable) issuable.open? ? '#38ae67' : '#d22852' end diff --git a/lib/gitlab/chat_commands/presenters/issue_new.rb b/lib/gitlab/chat_commands/presenters/issue_new.rb new file mode 100644 index 00000000000..d26dd22b2a0 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/issue_new.rb @@ -0,0 +1,48 @@ +module Gitlab + module ChatCommands + module Presenters + class IssueNew < Presenters::Base + include Presenters::Issuable + + def present + in_channel_response(new_issue) + end + + private + + def new_issue + { + attachments: [ + { + title: "#{@resource.title} · #{@resource.to_reference}", + title_link: resource_url, + author_name: author.name, + author_icon: author.avatar_url, + fallback: "New issue #{@resource.to_reference}: #{@resource.title}", + pretext: pretext, + color: color(@resource), + fields: fields, + mrkdwn_in: [ + :title, + :text + ] + } + ] + } + end + + def pretext + "I opened an issue on behalf on #{author_profile_link}: *#{@resource.to_reference}* from #{project.name_with_namespace}" + end + + def project_link + "[#{project.name_with_namespace}](#{url_for(project)})" + end + + def author_profile_link + "[#{author.to_reference}](#{url_for(author)})" + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/issue_search.rb b/lib/gitlab/chat_commands/presenters/issue_search.rb new file mode 100644 index 00000000000..d58a6d6114a --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/issue_search.rb @@ -0,0 +1,45 @@ +module Gitlab + module ChatCommands + module Presenters + class IssueSearch < Presenters::Base + include Presenters::Issuable + + def present + text = if @resource.count >= 5 + "Here are the first 5 issues I found:" + else + "Here are the #{@resource.count} issues I found:" + end + + ephemeral_response(text: text, attachments: attachments) + end + + private + + def attachments + @resource.map do |issue| + url = "[#{issue.to_reference}](#{url_for([namespace, project, issue])})" + + { + color: color(issue), + fallback: "#{issue.to_reference} #{issue.title}", + text: "#{url} · #{issue.title} (#{status_text(issue)})", + + mrkdwn_in: [ + "text" + ] + } + end + end + + def project + @project ||= @resource.first.project + end + + def namespace + @namespace ||= project.namespace.becomes(Namespace) + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/issue_show.rb b/lib/gitlab/chat_commands/presenters/issue_show.rb new file mode 100644 index 00000000000..2fc671f13a6 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/issue_show.rb @@ -0,0 +1,56 @@ +module Gitlab + module ChatCommands + module Presenters + class IssueShow < Presenters::Base + include Presenters::Issuable + + def present + in_channel_response(show_issue) + end + + private + + def show_issue + { + attachments: [ + { + title: "#{@resource.title} · #{@resource.to_reference}", + title_link: resource_url, + author_name: author.name, + author_icon: author.avatar_url, + fallback: "Issue #{@resource.to_reference}: #{@resource.title}", + pretext: pretext, + text: text, + color: color(@resource), + fields: fields, + mrkdwn_in: [ + :pretext, + :text + ] + } + ] + } + end + + def text + message = "**#{status_text(@resource)}**" + + if @resource.upvotes.zero? && @resource.downvotes.zero? && @resource.user_notes_count.zero? + return message + end + + 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 + + def pretext + "Issue *#{@resource.to_reference} from #{project.name_with_namespace}" + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/list_issues.rb b/lib/gitlab/chat_commands/presenters/list_issues.rb deleted file mode 100644 index 2458b9356b7..00000000000 --- a/lib/gitlab/chat_commands/presenters/list_issues.rb +++ /dev/null @@ -1,43 +0,0 @@ -module Gitlab - module ChatCommands - module Presenters - class ListIssues < Presenters::Issuable - def present - text = if @resource.count >= 5 - "Here are the first 5 issues I found:" - else - "Here are the #{@resource.count} issues I found:" - end - - ephemeral_response(text: text, attachments: attachments) - end - - private - - def attachments - @resource.map do |issue| - url = "[#{issue.to_reference}](#{url_for([namespace, project, issue])})" - - { - color: color(issue), - fallback: "#{issue.to_reference} #{issue.title}", - text: "#{url} · #{issue.title} (#{status_text(issue)})", - - mrkdwn_in: [ - "text" - ] - } - end - end - - def project - @project ||= @resource.first.project - end - - def namespace - @namespace ||= project.namespace.becomes(Namespace) - end - end - end - end -end diff --git a/lib/gitlab/chat_commands/presenters/new_issue.rb b/lib/gitlab/chat_commands/presenters/new_issue.rb deleted file mode 100644 index c7c6febb56e..00000000000 --- a/lib/gitlab/chat_commands/presenters/new_issue.rb +++ /dev/null @@ -1,42 +0,0 @@ -module Gitlab - module ChatCommands - module Presenters - class NewIssue < Presenters::Issuable - def present - in_channel_response(show_issue) - end - - private - - def show_issue - { - attachments: [ - { - title: "#{@resource.title} · #{@resource.to_reference}", - title_link: resource_url, - author_name: author.name, - author_icon: author.avatar_url, - fallback: "New issue #{@resource.to_reference}: #{@resource.title}", - pretext: pretext, - color: color(@resource), - fields: fields, - mrkdwn_in: [ - :title, - :text - ] - } - ] - } - end - - def pretext - "I opened an issue on behalf on #{author_profile_link}: *#{@resource.to_reference}* from #{project.name_with_namespace}" - end - - def author_profile_link - "[#{author.to_reference}](#{url_for(author)})" - end - end - end - end -end diff --git a/lib/gitlab/chat_commands/presenters/show_issue.rb b/lib/gitlab/chat_commands/presenters/show_issue.rb deleted file mode 100644 index e5644a4ad7e..00000000000 --- a/lib/gitlab/chat_commands/presenters/show_issue.rb +++ /dev/null @@ -1,54 +0,0 @@ -module Gitlab - module ChatCommands - module Presenters - class ShowIssue < Presenters::Issuable - def present - in_channel_response(show_issue) - end - - private - - def show_issue - { - attachments: [ - { - title: "#{@resource.title} · #{@resource.to_reference}", - title_link: resource_url, - author_name: author.name, - author_icon: author.avatar_url, - fallback: "New issue #{@resource.to_reference}: #{@resource.title}", - pretext: pretext, - text: text, - color: color(@resource), - fields: fields, - mrkdwn_in: [ - :pretext, - :text - ] - } - ] - } - end - - def text - message = "**#{status_text(@resource)}**" - - if @resource.upvotes.zero? && @resource.downvotes.zero? && @resource.user_notes_count.zero? - return message - end - - 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 - - def pretext - "Issue *#{@resource.to_reference} from #{project.name_with_namespace}" - end - end - end - end -end diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index b634df52b68..f4441f9f93c 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -78,7 +78,7 @@ describe Gitlab::ChatCommands::Command, service: true do context 'IssueCreate is triggered' do let(:params) { { text: 'issue create my title' } } - it { is_expected.to eq(Gitlab::ChatCommands::IssueCreate) } + it { is_expected.to eq(Gitlab::ChatCommands::IssueNew) } end context 'IssueSearch is triggered' do diff --git a/spec/lib/gitlab/chat_commands/issue_create_spec.rb b/spec/lib/gitlab/chat_commands/issue_create_spec.rb deleted file mode 100644 index 0f84b19a5a4..00000000000 --- a/spec/lib/gitlab/chat_commands/issue_create_spec.rb +++ /dev/null @@ -1,78 +0,0 @@ -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[:response_type]).to be(:in_channel) - 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 - - context "with more newlines between the title and the description" do - let(:description) { "Surfin bird" } - let(:regex_match) { described_class.match("issue create bird is the word\n\n#{description}\n") } - - it 'creates the issue' do - expect { subject }.to change { project.issues.count }.by(1) - 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 - - 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 - - it 'matches the alias new' do - match = described_class.match("issue new my title") - - expect(match).not_to be_nil - expect(match[:title]).to eq('my title') - end - end -end diff --git a/spec/lib/gitlab/chat_commands/issue_new_spec.rb b/spec/lib/gitlab/chat_commands/issue_new_spec.rb new file mode 100644 index 00000000000..84c22328064 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/issue_new_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::IssueNew, 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[:response_type]).to be(:in_channel) + 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 + + context "with more newlines between the title and the description" do + let(:description) { "Surfin bird" } + let(:regex_match) { described_class.match("issue create bird is the word\n\n#{description}\n") } + + it 'creates the issue' do + expect { subject }.to change { project.issues.count }.by(1) + 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 + + 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 + + it 'matches the alias new' do + match = described_class.match("issue new my title") + + expect(match).not_to be_nil + expect(match[:title]).to eq('my title') + end + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb new file mode 100644 index 00000000000..17fcdbc2452 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::IssueNew 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 start_with(issue.title) + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb new file mode 100644 index 00000000000..ec6d3e34a96 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::IssueSearch do + let(:project) { create(:empty_project) } + let(:message) { subject[:text] } + + before { create_list(:issue, 2, project: project) } + + subject { described_class.new(project.issues).present } + + it 'formats the message correct' 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 2 issues I found") + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb new file mode 100644 index 00000000000..89d154e26e4 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::IssueShow 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 start_with(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("**Open** · :+1: 1") + end + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb b/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb deleted file mode 100644 index 13a1f70fe78..00000000000 --- a/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'spec_helper' - -describe Gitlab::ChatCommands::Presenters::ListIssues do - let(:project) { create(:empty_project) } - let(:message) { subject[:text] } - - before { create_list(:issue, 2, project: project) } - - subject { described_class.new(project.issues).present } - - it 'formats the message correct' 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 2 issues I found") - end -end diff --git a/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb b/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb deleted file mode 100644 index ca4062e692a..00000000000 --- a/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb +++ /dev/null @@ -1,27 +0,0 @@ -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 start_with(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("**Open** · :+1: 1") - end - end -end -- cgit v1.2.1 From 34a1e3dcdbb7fdfcc1bafdc9dbaeee3c79b94c1c Mon Sep 17 00:00:00 2001 From: Eric Eastwood <contact@ericeastwood.com> Date: Tue, 24 Jan 2017 23:12:06 -0600 Subject: Fix permalink discussion note being collapsed --- .../javascripts/behaviors/toggler_behavior.js | 26 +++++++++++-------- ...151-fix-discussion-note-permalink-collapsed.yml | 4 +++ .../merge_requests/toggler_behavior_spec.rb | 29 ++++++++++++++++++++++ 3 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml create mode 100644 spec/features/merge_requests/toggler_behavior_spec.rb diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index 6a49715590c..a7181904ac9 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -1,6 +1,19 @@ /* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, vars-on-top, no-var, max-len */ (function(w) { $(function() { + var toggleContainer = function(container, /* optional */toggleState) { + var $container = $(container); + + $container + .find('.js-toggle-button .fa') + .toggleClass('fa-chevron-up', toggleState) + .toggleClass('fa-chevron-down', toggleState !== undefined ? !toggleState : undefined); + + $container + .find('.js-toggle-content') + .toggle(toggleState); + }; + // Toggle button. Show/hide content inside parent container. // Button does not change visibility. If button has icon - it changes chevron style. // @@ -10,14 +23,7 @@ // $('body').on('click', '.js-toggle-button', function(e) { e.preventDefault(); - $(this) - .find('.fa') - .toggleClass('fa-chevron-down fa-chevron-up') - .end() - .closest('.js-toggle-container') - .find('.js-toggle-content') - .toggle() - ; + toggleContainer($(this).closest('.js-toggle-container')); }); // If we're accessing a permalink, ensure it is not inside a @@ -26,8 +32,8 @@ var anchor = hash && document.getElementById(hash); var container = anchor && $(anchor).closest('.js-toggle-container'); - if (container && container.find('.js-toggle-content').is(':hidden')) { - container.find('.js-toggle-button').trigger('click'); + if (container) { + toggleContainer(container, true); anchor.scrollIntoView(); } }); diff --git a/changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml b/changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml new file mode 100644 index 00000000000..ddd454da376 --- /dev/null +++ b/changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml @@ -0,0 +1,4 @@ +--- +title: Fix permalink discussion note being collapsed +merge_request: +author: diff --git a/spec/features/merge_requests/toggler_behavior_spec.rb b/spec/features/merge_requests/toggler_behavior_spec.rb new file mode 100644 index 00000000000..6958f6a2c9f --- /dev/null +++ b/spec/features/merge_requests/toggler_behavior_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +feature 'toggler_behavior', js: true, feature: true do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:merge_request) { create(:merge_request, source_project: project, author: user) } + let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) } + let(:fragment_id) { "#note_#{note.id}" } + + before do + login_as :admin + project = merge_request.source_project + visit "#{namespace_project_merge_request_path(project.namespace, project, merge_request)}#{fragment_id}" + page.current_window.resize_to(1000, 300) + end + + describe 'scroll position' do + it 'should be scrolled down to fragment' do + page_height = page.current_window.size[1] + page_scroll_y = page.evaluate_script("window.scrollY") + fragment_position_top = page.evaluate_script("document.querySelector('#{fragment_id}').getBoundingClientRect().top") + + expect(find('.js-toggle-content').visible?).to eq true + expect(find(fragment_id).visible?).to eq true + expect(fragment_position_top).to be > page_scroll_y + expect(fragment_position_top).to be < (page_scroll_y + page_height) + end + end +end -- cgit v1.2.1 From 444ac6aa02e5b4b7025a9058a98dc6ae8db8e806 Mon Sep 17 00:00:00 2001 From: Clement Ho <ClemMakesApps@gmail.com> Date: Fri, 27 Jan 2017 13:22:12 -0600 Subject: Fix filtering usernames with multiple words --- .../filtered_search/dropdown_user.js.es6 | 9 ++++- .../fix-filtering-username-with-multiple-words.yml | 4 +++ .../filtered_search/dropdown_user_spec.js.es6 | 40 ++++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/fix-filtering-username-with-multiple-words.yml create mode 100644 spec/javascripts/filtered_search/dropdown_user_spec.js.es6 diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index 7bf199d9274..162fd6044e5 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -39,8 +39,15 @@ getSearchInput() { const query = gl.DropdownUtils.getSearchInput(this.input); const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); + let value = lastToken.value || ''; - return lastToken.value || ''; + // Removes the first character if it is a quotation so that we can search + // with multiple words + if (value[0] === '"' || value[0] === '\'') { + value = value.slice(1); + } + + return value; } init() { diff --git a/changelogs/unreleased/fix-filtering-username-with-multiple-words.yml b/changelogs/unreleased/fix-filtering-username-with-multiple-words.yml new file mode 100644 index 00000000000..3513f5afdfb --- /dev/null +++ b/changelogs/unreleased/fix-filtering-username-with-multiple-words.yml @@ -0,0 +1,4 @@ +--- +title: Fix filtering usernames with multiple words +merge_request: 8851 +author: diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 new file mode 100644 index 00000000000..5eba4343a1d --- /dev/null +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 @@ -0,0 +1,40 @@ +//= require filtered_search/dropdown_utils +//= require filtered_search/filtered_search_tokenizer +//= require filtered_search/filtered_search_dropdown +//= require filtered_search/dropdown_user + +(() => { + describe('Dropdown User', () => { + describe('getSearchInput', () => { + let dropdownUser; + + beforeEach(() => { + spyOn(gl.FilteredSearchDropdown.prototype, 'constructor').and.callFake(() => {}); + spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); + spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {}); + + dropdownUser = new gl.DropdownUser(); + }); + + it('should not return the double quote found in value', () => { + spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ + lastToken: { + value: '"johnny appleseed', + }, + }); + + expect(dropdownUser.getSearchInput()).toBe('johnny appleseed'); + }); + + it('should not return the single quote found in value', () => { + spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ + lastToken: { + value: '\'larry boy', + }, + }); + + expect(dropdownUser.getSearchInput()).toBe('larry boy'); + }); + }); + }); +})(); -- cgit v1.2.1 From 84af4e485352a1834527a94b462d2034abb9a091 Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Fri, 27 Jan 2017 16:44:02 -0500 Subject: Update CHANGELOG.md for 8.16.3 [ci skip] --- CHANGELOG.md | 11 +++++++++++ .../unreleased/26775-fix-auto-complete-initial-loading.yml | 4 ---- ...844-new-search-bar-performs-a-new-request-for-each-tag.yml | 4 ---- .../unreleased/27044-fix-explore-sorting-on-trending.yml | 4 ---- changelogs/unreleased/fix-26518.yml | 4 ---- changelogs/unreleased/label-select-toggle.yml | 4 ---- changelogs/unreleased/refresh-authorizations-fork-join.yml | 4 ---- changelogs/unreleased/revert-filter-assigned-to-me.yml | 4 ---- .../unreleased/sh-fix-annotated-tags-pointing-to-blob.yml | 4 ---- 9 files changed, 11 insertions(+), 32 deletions(-) delete mode 100644 changelogs/unreleased/26775-fix-auto-complete-initial-loading.yml delete mode 100644 changelogs/unreleased/26844-new-search-bar-performs-a-new-request-for-each-tag.yml delete mode 100644 changelogs/unreleased/27044-fix-explore-sorting-on-trending.yml delete mode 100644 changelogs/unreleased/fix-26518.yml delete mode 100644 changelogs/unreleased/label-select-toggle.yml delete mode 100644 changelogs/unreleased/refresh-authorizations-fork-join.yml delete mode 100644 changelogs/unreleased/revert-filter-assigned-to-me.yml delete mode 100644 changelogs/unreleased/sh-fix-annotated-tags-pointing-to-blob.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 9712b32232e..90cca93e2da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 8.16.3 (2017-01-27) + +- Add caching of droplab ajax requests. !8725 +- Fix access to the wiki code via HTTP when repository feature disabled. !8758 +- Revert 3f17f29a. !8785 +- Fix race conditions for AuthorizedProjectsWorker. +- Fix autocomplete initial undefined state. +- Fix Error 500 when repositories contain annotated tags pointing to blobs. +- Fix /explore sorting. +- Fixed label dropdown toggle text not correctly updating. + ## 8.16.2 (2017-01-25) - allow issue filter bar to be operated with mouse only. !8681 diff --git a/changelogs/unreleased/26775-fix-auto-complete-initial-loading.yml b/changelogs/unreleased/26775-fix-auto-complete-initial-loading.yml deleted file mode 100644 index 2d4ec482ee0..00000000000 --- a/changelogs/unreleased/26775-fix-auto-complete-initial-loading.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix autocomplete initial undefined state -merge_request: -author: diff --git a/changelogs/unreleased/26844-new-search-bar-performs-a-new-request-for-each-tag.yml b/changelogs/unreleased/26844-new-search-bar-performs-a-new-request-for-each-tag.yml deleted file mode 100644 index 4678297cfd4..00000000000 --- a/changelogs/unreleased/26844-new-search-bar-performs-a-new-request-for-each-tag.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add caching of droplab ajax requests -merge_request: 8725 -author: diff --git a/changelogs/unreleased/27044-fix-explore-sorting-on-trending.yml b/changelogs/unreleased/27044-fix-explore-sorting-on-trending.yml deleted file mode 100644 index 0f0a8940f72..00000000000 --- a/changelogs/unreleased/27044-fix-explore-sorting-on-trending.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix /explore sorting -merge_request: -author: diff --git a/changelogs/unreleased/fix-26518.yml b/changelogs/unreleased/fix-26518.yml deleted file mode 100644 index 961ac2642fb..00000000000 --- a/changelogs/unreleased/fix-26518.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix access to the wiki code via HTTP when repository feature disabled -merge_request: 8758 -author: diff --git a/changelogs/unreleased/label-select-toggle.yml b/changelogs/unreleased/label-select-toggle.yml deleted file mode 100644 index af5b4246521..00000000000 --- a/changelogs/unreleased/label-select-toggle.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed label dropdown toggle text not correctly updating -merge_request: -author: diff --git a/changelogs/unreleased/refresh-authorizations-fork-join.yml b/changelogs/unreleased/refresh-authorizations-fork-join.yml deleted file mode 100644 index b1349b9447d..00000000000 --- a/changelogs/unreleased/refresh-authorizations-fork-join.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix race conditions for AuthorizedProjectsWorker -merge_request: -author: diff --git a/changelogs/unreleased/revert-filter-assigned-to-me.yml b/changelogs/unreleased/revert-filter-assigned-to-me.yml deleted file mode 100644 index 37f9d2f5fc4..00000000000 --- a/changelogs/unreleased/revert-filter-assigned-to-me.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Revert 3f17f29a -merge_request: 8785 -author: diff --git a/changelogs/unreleased/sh-fix-annotated-tags-pointing-to-blob.yml b/changelogs/unreleased/sh-fix-annotated-tags-pointing-to-blob.yml deleted file mode 100644 index ff2b38f21f2..00000000000 --- a/changelogs/unreleased/sh-fix-annotated-tags-pointing-to-blob.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix Error 500 when repositories contain annotated tags pointing to blobs -merge_request: -author: -- cgit v1.2.1 From dc6921bdbbabd08be4426345140cb507b286eac7 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Tue, 10 Jan 2017 19:43:58 +0100 Subject: Chat Commands have presenters This improves the styling and readability of the code. This is supported by both Mattermost and Slack. --- .../chat_slash_commands_service.rb | 20 +-- lib/gitlab/chat_commands/base_command.rb | 4 - lib/gitlab/chat_commands/command.rb | 22 +-- lib/gitlab/chat_commands/deploy.rb | 24 ++-- lib/gitlab/chat_commands/issue_create.rb | 18 ++- lib/gitlab/chat_commands/issue_search.rb | 10 +- lib/gitlab/chat_commands/issue_show.rb | 8 +- lib/gitlab/chat_commands/presenter.rb | 131 ----------------- lib/gitlab/chat_commands/presenters/access.rb | 22 +++ lib/gitlab/chat_commands/presenters/base.rb | 73 ++++++++++ lib/gitlab/chat_commands/presenters/deploy.rb | 24 ++++ lib/gitlab/chat_commands/presenters/issuable.rb | 33 +++++ lib/gitlab/chat_commands/presenters/list_issues.rb | 32 +++++ lib/gitlab/chat_commands/presenters/show_issue.rb | 38 +++++ lib/mattermost/error.rb | 3 - lib/mattermost/session.rb | 160 --------------------- spec/lib/gitlab/chat_commands/command_spec.rb | 50 +------ spec/lib/gitlab/chat_commands/deploy_spec.rb | 24 ++-- spec/lib/gitlab/chat_commands/issue_create_spec.rb | 12 +- spec/lib/gitlab/chat_commands/issue_search_spec.rb | 12 +- spec/lib/gitlab/chat_commands/issue_show_spec.rb | 25 +++- .../gitlab/chat_commands/presenters/access_spec.rb | 49 +++++++ .../gitlab/chat_commands/presenters/deploy_spec.rb | 47 ++++++ .../chat_commands/presenters/list_issues_spec.rb | 24 ++++ .../chat_commands/presenters/show_issue_spec.rb | 27 ++++ spec/lib/mattermost/client_spec.rb | 24 ---- spec/lib/mattermost/command_spec.rb | 61 -------- spec/lib/mattermost/session_spec.rb | 123 ---------------- spec/lib/mattermost/team_spec.rb | 66 --------- 29 files changed, 479 insertions(+), 687 deletions(-) delete mode 100644 lib/gitlab/chat_commands/presenter.rb create mode 100644 lib/gitlab/chat_commands/presenters/access.rb create mode 100644 lib/gitlab/chat_commands/presenters/base.rb create mode 100644 lib/gitlab/chat_commands/presenters/deploy.rb create mode 100644 lib/gitlab/chat_commands/presenters/issuable.rb create mode 100644 lib/gitlab/chat_commands/presenters/list_issues.rb create mode 100644 lib/gitlab/chat_commands/presenters/show_issue.rb delete mode 100644 lib/mattermost/error.rb delete mode 100644 lib/mattermost/session.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/access_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb delete mode 100644 spec/lib/mattermost/client_spec.rb delete mode 100644 spec/lib/mattermost/command_spec.rb delete mode 100644 spec/lib/mattermost/session_spec.rb delete mode 100644 spec/lib/mattermost/team_spec.rb diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb index 2bcff541cc0..608754f3035 100644 --- a/app/models/project_services/chat_slash_commands_service.rb +++ b/app/models/project_services/chat_slash_commands_service.rb @@ -28,20 +28,24 @@ class ChatSlashCommandsService < Service end def trigger(params) - return unless valid_token?(params[:token]) + return access_presenter unless valid_token?(params[:token]) user = find_chat_user(params) - unless user + + if user + Gitlab::ChatCommands::Command.new(project, user, params).execute + else url = authorize_chat_name_url(params) - return presenter.authorize_chat_name(url) + access_presenter(url).authorize end - - Gitlab::ChatCommands::Command.new(project, user, - params).execute end private + def access_presenter(url = nil) + Gitlab::ChatCommands::Presenters::Access.new(url) + end + def find_chat_user(params) ChatNames::FindUserService.new(self, params).execute end @@ -49,8 +53,4 @@ class ChatSlashCommandsService < Service def authorize_chat_name_url(params) ChatNames::AuthorizeUserService.new(self, params).execute end - - def presenter - Gitlab::ChatCommands::Presenter.new - end end diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/chat_commands/base_command.rb index 4fe53ce93a9..25da8474e95 100644 --- a/lib/gitlab/chat_commands/base_command.rb +++ b/lib/gitlab/chat_commands/base_command.rb @@ -42,10 +42,6 @@ module Gitlab def find_by_iid(iid) collection.find_by(iid: iid) end - - def presenter - Gitlab::ChatCommands::Presenter.new - end end end end diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index 145086755e4..ac7ee868402 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -13,9 +13,9 @@ module Gitlab if command if command.allowed?(project, current_user) - present command.new(project, current_user, params).execute(match) + command.new(project, current_user, params).execute(match) else - access_denied + Gitlab::ChatCommands::Presenters::Access.new.access_denied end else help(help_messages) @@ -25,7 +25,7 @@ module Gitlab def match_command match = nil service = available_commands.find do |klass| - match = klass.match(command) + match = klass.match(params[:text]) end [service, match] @@ -42,22 +42,6 @@ module Gitlab klass.available?(project) 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 diff --git a/lib/gitlab/chat_commands/deploy.rb b/lib/gitlab/chat_commands/deploy.rb index 7127d2f6d04..458d90f84e8 100644 --- a/lib/gitlab/chat_commands/deploy.rb +++ b/lib/gitlab/chat_commands/deploy.rb @@ -1,8 +1,6 @@ module Gitlab module ChatCommands class Deploy < BaseCommand - include Gitlab::Routing.url_helpers - def self.match(text) /\Adeploy\s+(?<from>\S+.*)\s+to+\s+(?<to>\S+.*)\z/.match(text) end @@ -24,35 +22,29 @@ module Gitlab to = match[:to] actions = find_actions(from, to) - return unless actions.present? - if actions.one? - play!(from, to, actions.first) + if actions.none? + 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 - Result.new(:error, 'Too many actions defined') + Gitlab::ChatCommands::Presenters::Deploy.new(actions).too_many_actions end end private def play!(from, to, action) - new_action = action.play(current_user) - - Result.new(:success, "Deployment from #{from} to #{to} started. Follow the progress: #{url(new_action)}.") + action.play(current_user) end def find_actions(from, to) environment = project.environments.find_by(name: from) - return unless environment + return [] unless environment environment.actions_for(to).select(&:starts_environment?) end - - def url(subject) - polymorphic_url( - [subject.project.namespace.becomes(Namespace), subject.project, subject] - ) - end end end end diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb index cefb6775db8..a06f13b0f72 100644 --- a/lib/gitlab/chat_commands/issue_create.rb +++ b/lib/gitlab/chat_commands/issue_create.rb @@ -2,7 +2,7 @@ module Gitlab module ChatCommands class IssueCreate < IssueCommand 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 /\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text) end @@ -19,8 +19,24 @@ module Gitlab title = match[:title] 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 end + + def presenter(issue) + Gitlab::ChatCommands::Presenters::ShowIssue.new(issue) + end end end end diff --git a/lib/gitlab/chat_commands/issue_search.rb b/lib/gitlab/chat_commands/issue_search.rb index 51bf80c800b..e2d3a0f466a 100644 --- a/lib/gitlab/chat_commands/issue_search.rb +++ b/lib/gitlab/chat_commands/issue_search.rb @@ -10,7 +10,15 @@ module Gitlab end 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 diff --git a/lib/gitlab/chat_commands/issue_show.rb b/lib/gitlab/chat_commands/issue_show.rb index 2a45d49cf6b..9f3e1b9a64b 100644 --- a/lib/gitlab/chat_commands/issue_show.rb +++ b/lib/gitlab/chat_commands/issue_show.rb @@ -10,7 +10,13 @@ module Gitlab end 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 diff --git a/lib/gitlab/chat_commands/presenter.rb b/lib/gitlab/chat_commands/presenter.rb deleted file mode 100644 index 8930a21f406..00000000000 --- a/lib/gitlab/chat_commands/presenter.rb +++ /dev/null @@ -1,131 +0,0 @@ -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 diff --git a/lib/gitlab/chat_commands/presenters/access.rb b/lib/gitlab/chat_commands/presenters/access.rb new file mode 100644 index 00000000000..6d18d745608 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/access.rb @@ -0,0 +1,22 @@ +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 diff --git a/lib/gitlab/chat_commands/presenters/base.rb b/lib/gitlab/chat_commands/presenters/base.rb new file mode 100644 index 00000000000..0897025d85f --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/base.rb @@ -0,0 +1,73 @@ +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 diff --git a/lib/gitlab/chat_commands/presenters/deploy.rb b/lib/gitlab/chat_commands/presenters/deploy.rb new file mode 100644 index 00000000000..4f6333812ff --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/deploy.rb @@ -0,0 +1,24 @@ +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 diff --git a/lib/gitlab/chat_commands/presenters/issuable.rb b/lib/gitlab/chat_commands/presenters/issuable.rb new file mode 100644 index 00000000000..9623387f188 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/issuable.rb @@ -0,0 +1,33 @@ +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 diff --git a/lib/gitlab/chat_commands/presenters/list_issues.rb b/lib/gitlab/chat_commands/presenters/list_issues.rb new file mode 100644 index 00000000000..5a7b3fca5c2 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/list_issues.rb @@ -0,0 +1,32 @@ +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 diff --git a/lib/gitlab/chat_commands/presenters/show_issue.rb b/lib/gitlab/chat_commands/presenters/show_issue.rb new file mode 100644 index 00000000000..2a89c30b972 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/show_issue.rb @@ -0,0 +1,38 @@ +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 diff --git a/lib/mattermost/error.rb b/lib/mattermost/error.rb deleted file mode 100644 index 014df175be0..00000000000 --- a/lib/mattermost/error.rb +++ /dev/null @@ -1,3 +0,0 @@ -module Mattermost - class Error < StandardError; end -end diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb deleted file mode 100644 index 377cb7b1021..00000000000 --- a/lib/mattermost/session.rb +++ /dev/null @@ -1,160 +0,0 @@ -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 diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index 1e81eaef18c..d8b2303555c 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -5,6 +5,7 @@ describe Gitlab::ChatCommands::Command, service: true do let(:user) { create(:user) } describe '#execute' do +<<<<<<< HEAD subject do described_class.new(project, user, params).execute end @@ -18,6 +19,9 @@ describe Gitlab::ChatCommands::Command, service: true do expect(subject[:text]).to start_with('404 not found') end end +======= + subject { described_class.new(project, user, params).execute } +>>>>>>> Chat Commands have presenters context 'when an unknown command is triggered' do let(:params) { { command: '/gitlab', text: "unknown command 123" } } @@ -34,47 +38,7 @@ describe Gitlab::ChatCommands::Command, service: true do 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 - - 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 + expect(subject[:text]).to start_with('Whoops! This action is not allowed') end end @@ -90,7 +54,7 @@ describe Gitlab::ChatCommands::Command, service: true do context 'and user can not create deployment' do it 'returns action' do 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 @@ -100,7 +64,7 @@ describe Gitlab::ChatCommands::Command, service: true do end 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) end diff --git a/spec/lib/gitlab/chat_commands/deploy_spec.rb b/spec/lib/gitlab/chat_commands/deploy_spec.rb index bd8099c92da..b3358a32161 100644 --- a/spec/lib/gitlab/chat_commands/deploy_spec.rb +++ b/spec/lib/gitlab/chat_commands/deploy_spec.rb @@ -15,8 +15,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do end context 'if no environment is defined' do - it 'returns nil' do - expect(subject).to be_nil + it 'does not execute an action' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to eq("No action found to be executed") end end @@ -26,8 +27,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do let!(:deployment) { create(:deployment, environment: staging, deployable: build) } context 'without actions' do - it 'returns nil' do - expect(subject).to be_nil + it 'does not execute an action' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to eq("No action found to be executed") end end @@ -37,8 +39,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do end it 'returns success result' do - expect(subject.type).to eq(:success) - expect(subject.message).to include('Deployment from staging to production started') + expect(subject[:response_type]).to be(:in_channel) + expect(subject[:text]).to start_with('Deployment started from staging to production') end context 'when duplicate action exists' do @@ -47,8 +49,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do end it 'returns error' do - expect(subject.type).to eq(:error) - expect(subject.message).to include('Too many actions defined') + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to eq('Too many actions defined') end end @@ -59,9 +61,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do name: 'teardown', environment: 'production') end - it 'returns success result' do - expect(subject.type).to eq(:success) - expect(subject.message).to include('Deployment from staging to production started') + it 'returns the success message' do + expect(subject[:response_type]).to be(:in_channel) + expect(subject[:text]).to start_with('Deployment started from staging to production') end end end diff --git a/spec/lib/gitlab/chat_commands/issue_create_spec.rb b/spec/lib/gitlab/chat_commands/issue_create_spec.rb index 6c71e79ff6d..0f84b19a5a4 100644 --- a/spec/lib/gitlab/chat_commands/issue_create_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_create_spec.rb @@ -18,7 +18,7 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do it 'creates the issue' do 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 @@ -41,6 +41,16 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do expect { subject }.to change { project.issues.count }.by(1) 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 describe '.match' do diff --git a/spec/lib/gitlab/chat_commands/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/issue_search_spec.rb index 24c06a967fa..04d10ad52a1 100644 --- a/spec/lib/gitlab/chat_commands/issue_search_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_search_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' describe Gitlab::ChatCommands::IssueSearch, service: true 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(:project) { issue.project } + let(:project) { create(:empty_project) } let(:user) { issue.author } let(:regex_match) { described_class.match("issue search find") } @@ -14,7 +14,8 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do context 'when the user has no access' 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 @@ -24,13 +25,14 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do end 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 context 'without hits on the query' do it 'returns an empty collection' do - expect(subject).to be_empty + expect(subject[:text]).to match("not found") end end end diff --git a/spec/lib/gitlab/chat_commands/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/issue_show_spec.rb index 2eab73e49e5..89932c395c6 100644 --- a/spec/lib/gitlab/chat_commands/issue_show_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_show_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' describe Gitlab::ChatCommands::IssueShow, service: true do describe '#execute' do - let(:issue) { create(:issue) } - let(:project) { issue.project } + let(:issue) { create(:issue, project: project) } + let(:project) { create(:empty_project) } let(:user) { issue.author } let(:regex_match) { described_class.match("issue show #{issue.iid}") } @@ -16,15 +16,19 @@ describe Gitlab::ChatCommands::IssueShow, service: true do end context 'the issue exists' do + let(:title) { subject[:attachments].first[:title] } + 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 context 'when its reference is given' do let(:regex_match) { described_class.match("issue show #{issue.to_reference}") } 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 @@ -32,17 +36,24 @@ describe Gitlab::ChatCommands::IssueShow, service: true do 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 + it "returns not found" do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to match("not found") end end end - describe 'self.match' do + describe '.match' do it 'matches the iid' do match = described_class.match("issue show 123") expect(match[:iid]).to eq("123") 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 diff --git a/spec/lib/gitlab/chat_commands/presenters/access_spec.rb b/spec/lib/gitlab/chat_commands/presenters/access_spec.rb new file mode 100644 index 00000000000..ae41d75ab0c --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/access_spec.rb @@ -0,0 +1,49 @@ +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 diff --git a/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb new file mode 100644 index 00000000000..1c48c727e30 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb @@ -0,0 +1,47 @@ +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 diff --git a/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb b/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb new file mode 100644 index 00000000000..1852395fc97 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb @@ -0,0 +1,24 @@ +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 diff --git a/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb b/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb new file mode 100644 index 00000000000..13a318fe680 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb @@ -0,0 +1,27 @@ +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 diff --git a/spec/lib/mattermost/client_spec.rb b/spec/lib/mattermost/client_spec.rb deleted file mode 100644 index dc11a414717..00000000000 --- a/spec/lib/mattermost/client_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -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 diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb deleted file mode 100644 index 5ccf1100898..00000000000 --- a/spec/lib/mattermost/command_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -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 diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb deleted file mode 100644 index 74d12e37181..00000000000 --- a/spec/lib/mattermost/session_spec.rb +++ /dev/null @@ -1,123 +0,0 @@ -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 diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb deleted file mode 100644 index 2d14be6bcc2..00000000000 --- a/spec/lib/mattermost/team_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -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 -- cgit v1.2.1 From 746f47208dc52cd6ca68c0893de5513c250f524b Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Wed, 11 Jan 2017 08:54:44 -0500 Subject: Revert removing of some files --- lib/mattermost/command.rb | 4 ++ lib/mattermost/error.rb | 3 + lib/mattermost/session.rb | 160 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 lib/mattermost/error.rb create mode 100644 lib/mattermost/session.rb diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb index 33e450d7f0a..2e4f7705f86 100644 --- a/lib/mattermost/command.rb +++ b/lib/mattermost/command.rb @@ -1,7 +1,11 @@ module Mattermost class Command < Client def create(params) +<<<<<<< HEAD response = session_post("/api/v3/teams/#{params[:team_id]}/commands/create", +======= + response = json_post("/api/v3/teams/#{params[:team_id]}/commands/create", +>>>>>>> Revert removing of some files body: params.to_json) response['token'] diff --git a/lib/mattermost/error.rb b/lib/mattermost/error.rb new file mode 100644 index 00000000000..014df175be0 --- /dev/null +++ b/lib/mattermost/error.rb @@ -0,0 +1,3 @@ +module Mattermost + class Error < StandardError; end +end diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb new file mode 100644 index 00000000000..377cb7b1021 --- /dev/null +++ b/lib/mattermost/session.rb @@ -0,0 +1,160 @@ +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 -- cgit v1.2.1 From 53846da2c7fe3879b4f26383d6367b0bb69e5dc8 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Wed, 11 Jan 2017 09:04:49 -0500 Subject: Add help command --- lib/gitlab/chat_commands/command.rb | 13 +++++-------- lib/gitlab/chat_commands/help.rb | 28 ++++++++++++++++++++++++++++ lib/gitlab/chat_commands/presenters/help.rb | 20 ++++++++++++++++++++ 3 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 lib/gitlab/chat_commands/help.rb create mode 100644 lib/gitlab/chat_commands/presenters/help.rb diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index ac7ee868402..4e5031a8a26 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -18,25 +18,22 @@ module Gitlab Gitlab::ChatCommands::Presenters::Access.new.access_denied end else - help(help_messages) + Gitlab::ChatCommands::Help.new(project, current_user, params).execute(available_commands) end end def match_command match = nil - service = available_commands.find do |klass| - match = klass.match(params[:text]) - end + service = + available_commands.find do |klass| + match = klass.match(params[:text]) + end [service, match] end private - def help_messages - available_commands.map(&:help_message) - end - def available_commands COMMANDS.select do |klass| klass.available?(project) diff --git a/lib/gitlab/chat_commands/help.rb b/lib/gitlab/chat_commands/help.rb new file mode 100644 index 00000000000..e76733f5445 --- /dev/null +++ b/lib/gitlab/chat_commands/help.rb @@ -0,0 +1,28 @@ +module Gitlab + module ChatCommands + class Help < BaseCommand + # This class has to be used last, as it always matches. It has to match + # because other commands were not triggered and we want to show the help + # command + def self.match(_text) + true + end + + def self.help_message + 'help' + end + + def self.allowed?(_project, _user) + true + end + + def execute(commands) + Gitlab::ChatCommands::Presenters::Help.new(commands).present(trigger) + end + + def trigger + params[:command] + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/help.rb b/lib/gitlab/chat_commands/presenters/help.rb new file mode 100644 index 00000000000..133b707231f --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/help.rb @@ -0,0 +1,20 @@ +module Gitlab::ChatCommands::Presenters + class Help < Gitlab::ChatCommands::Presenters::Base + def present(trigger) + message = + if @resource.none? + "No commands available :thinking_face:" + else + header_with_list("Available commands", full_commands(trigger)) + end + + ephemeral_response(text: message) + end + + private + + def full_commands(trigger) + @resource.map { |command| "#{trigger} #{command.help_message}" } + end + end +end -- cgit v1.2.1 From 72843e021dba0022b75f3fd3988115691c19a4fb Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Thu, 12 Jan 2017 09:04:21 -0500 Subject: Fix tests --- app/models/project_services/chat_slash_commands_service.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb index 608754f3035..5eb1bd86e9d 100644 --- a/app/models/project_services/chat_slash_commands_service.rb +++ b/app/models/project_services/chat_slash_commands_service.rb @@ -28,7 +28,7 @@ class ChatSlashCommandsService < Service end def trigger(params) - return access_presenter unless valid_token?(params[:token]) + return unless valid_token?(params[:token]) user = find_chat_user(params) @@ -36,16 +36,12 @@ class ChatSlashCommandsService < Service Gitlab::ChatCommands::Command.new(project, user, params).execute else url = authorize_chat_name_url(params) - access_presenter(url).authorize + Gitlab::ChatCommands::Presenters::Access.new(url).authorize end end private - def access_presenter(url = nil) - Gitlab::ChatCommands::Presenters::Access.new(url) - end - def find_chat_user(params) ChatNames::FindUserService.new(self, params).execute end -- cgit v1.2.1 From 4ce1a17c9767a80dfae0b47cee236d2a5d88918b Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Thu, 19 Jan 2017 09:22:09 +0100 Subject: Incorporate feedback --- changelogs/unreleased/zj-format-chat-messages.yml | 4 + lib/gitlab/chat_commands/issue_create.rb | 2 +- lib/gitlab/chat_commands/presenters/access.rb | 36 +++--- lib/gitlab/chat_commands/presenters/base.rb | 112 ++++++++++--------- lib/gitlab/chat_commands/presenters/deploy.rb | 39 ++++--- lib/gitlab/chat_commands/presenters/help.rb | 31 +++--- lib/gitlab/chat_commands/presenters/issuable.rb | 66 ++++++----- lib/gitlab/chat_commands/presenters/list_issues.rb | 59 ++++++---- lib/gitlab/chat_commands/presenters/new_issue.rb | 42 +++++++ lib/gitlab/chat_commands/presenters/show_issue.rb | 72 +++++++----- spec/lib/gitlab/chat_commands/issue_search_spec.rb | 2 +- spec/lib/gitlab/chat_commands/issue_show_spec.rb | 4 +- .../gitlab/chat_commands/presenters/deploy_spec.rb | 2 +- .../chat_commands/presenters/list_issues_spec.rb | 5 +- .../chat_commands/presenters/show_issue_spec.rb | 4 +- spec/lib/mattermost/client_spec.rb | 24 ++++ spec/lib/mattermost/command_spec.rb | 61 ++++++++++ spec/lib/mattermost/session_spec.rb | 123 +++++++++++++++++++++ spec/lib/mattermost/team_spec.rb | 66 +++++++++++ 19 files changed, 565 insertions(+), 189 deletions(-) create mode 100644 changelogs/unreleased/zj-format-chat-messages.yml create mode 100644 lib/gitlab/chat_commands/presenters/new_issue.rb create mode 100644 spec/lib/mattermost/client_spec.rb create mode 100644 spec/lib/mattermost/command_spec.rb create mode 100644 spec/lib/mattermost/session_spec.rb create mode 100644 spec/lib/mattermost/team_spec.rb diff --git a/changelogs/unreleased/zj-format-chat-messages.yml b/changelogs/unreleased/zj-format-chat-messages.yml new file mode 100644 index 00000000000..2494884f5c9 --- /dev/null +++ b/changelogs/unreleased/zj-format-chat-messages.yml @@ -0,0 +1,4 @@ +--- +title: Reformat messages ChatOps +merge_request: 8528 +author: diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb index a06f13b0f72..3f3d7de8b2e 100644 --- a/lib/gitlab/chat_commands/issue_create.rb +++ b/lib/gitlab/chat_commands/issue_create.rb @@ -35,7 +35,7 @@ module Gitlab end def presenter(issue) - Gitlab::ChatCommands::Presenters::ShowIssue.new(issue) + Gitlab::ChatCommands::Presenters::NewIssue.new(issue) end end end diff --git a/lib/gitlab/chat_commands/presenters/access.rb b/lib/gitlab/chat_commands/presenters/access.rb index 6d18d745608..b66ef48d6a8 100644 --- a/lib/gitlab/chat_commands/presenters/access.rb +++ b/lib/gitlab/chat_commands/presenters/access.rb @@ -1,22 +1,26 @@ -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 +module Gitlab + module ChatCommands + module Presenters + class Access < 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 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!" + def not_found + ephemeral_response(text: "404 not found! GitLab couldn't find what you were looking for! :boom:") end - ephemeral_response(text: message) + 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 end end diff --git a/lib/gitlab/chat_commands/presenters/base.rb b/lib/gitlab/chat_commands/presenters/base.rb index 0897025d85f..2700a5a2ad5 100644 --- a/lib/gitlab/chat_commands/presenters/base.rb +++ b/lib/gitlab/chat_commands/presenters/base.rb @@ -1,73 +1,77 @@ -module Gitlab::ChatCommands::Presenters - class Base - include Gitlab::Routing.url_helpers +module Gitlab + module ChatCommands + module Presenters + class Base + include Gitlab::Routing.url_helpers + + def initialize(resource = nil) + @resource = resource + end - def initialize(resource = nil) - @resource = resource - end + def display_errors + message = header_with_list("The action was not successful, because:", @resource.errors.full_messages) - def display_errors - message = header_with_list("The action was not successful, because:", @resource.errors.full_messages) + ephemeral_response(text: message) + end - ephemeral_response(text: message) - end + private - private + def header_with_list(header, items) + message = [header] - def header_with_list(header, items) - message = [header] + items.each do |item| + message << "- #{item}" + end - items.each do |item| - message << "- #{item}" - end + message.join("\n") + end - message.join("\n") - end + def ephemeral_response(message) + response = { + response_type: :ephemeral, + status: 200 + }.merge(message) - def ephemeral_response(message) - response = { - response_type: :ephemeral, - status: 200 - }.merge(message) + format_response(response) + end - format_response(response) - end + def in_channel_response(message) + response = { + response_type: :in_channel, + status: 200 + }.merge(message) - def in_channel_response(message) - response = { - response_type: :in_channel, - status: 200 - }.merge(message) + format_response(response) + end - format_response(response) - end + def format_response(response) + response[:text] = format(response[:text]) if response.has_key?(:text) - 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 - 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] + response end - end - - response - end - # Convert Markdown to slacks format - def format(string) - Slack::Notifier::LinkFormatter.format(string) - 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 - ] - ) + def resource_url + url_for( + [ + @resource.project.namespace.becomes(Namespace), + @resource.project, + @resource + ] + ) + end + end end end end diff --git a/lib/gitlab/chat_commands/presenters/deploy.rb b/lib/gitlab/chat_commands/presenters/deploy.rb index 4f6333812ff..b1cfaac15af 100644 --- a/lib/gitlab/chat_commands/presenters/deploy.rb +++ b/lib/gitlab/chat_commands/presenters/deploy.rb @@ -1,24 +1,29 @@ -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 +module Gitlab + module ChatCommands + module Presenters + class Deploy < Presenters::Base + def present(from, to) + message = "Deployment started from #{from} to #{to}. [Follow its progress](#{resource_url})." - def no_actions - ephemeral_response(text: "No action found to be executed") - end + in_channel_response(text: message) + end - def too_many_actions - ephemeral_response(text: "Too many actions defined") - 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 + private - def resource_url - polymorphic_url( - [ @resource.project.namespace.becomes(Namespace), @resource.project, @resource] - ) + def resource_url + polymorphic_url( + [ @resource.project.namespace.becomes(Namespace), @resource.project, @resource] + ) + end + end end end end diff --git a/lib/gitlab/chat_commands/presenters/help.rb b/lib/gitlab/chat_commands/presenters/help.rb index 133b707231f..c7a67467b7e 100644 --- a/lib/gitlab/chat_commands/presenters/help.rb +++ b/lib/gitlab/chat_commands/presenters/help.rb @@ -1,20 +1,25 @@ -module Gitlab::ChatCommands::Presenters - class Help < Gitlab::ChatCommands::Presenters::Base - def present(trigger) - message = - if @resource.none? - "No commands available :thinking_face:" - else - header_with_list("Available commands", full_commands(trigger)) +module Gitlab + module ChatCommands + module Presenters + class Help < Presenters::Base + def present(trigger) + ephemeral_response(text: help_message(trigger)) end - ephemeral_response(text: message) - end + private - private + def help_message(trigger) + if @resource.none? + "No commands available :thinking_face:" + else + header_with_list("Available commands", full_commands(trigger)) + end + end - def full_commands(trigger) - @resource.map { |command| "#{trigger} #{command.help_message}" } + def full_commands(trigger) + @resource.map { |command| "#{trigger} #{command.help_message}" } + end + end end end end diff --git a/lib/gitlab/chat_commands/presenters/issuable.rb b/lib/gitlab/chat_commands/presenters/issuable.rb index 9623387f188..2cb6b1525fc 100644 --- a/lib/gitlab/chat_commands/presenters/issuable.rb +++ b/lib/gitlab/chat_commands/presenters/issuable.rb @@ -1,33 +1,45 @@ -module Gitlab::ChatCommands::Presenters - class Issuable < Gitlab::ChatCommands::Presenters::Base - private +module Gitlab + module ChatCommands + module Presenters + class Issuable < Presenters::Base + private - def project - @resource.project - end + def color(issuable) + issuable.open? ? '#38ae67' : '#d22852' + end - def author - @resource.author - end + def status_text(issuable) + issuable.open? ? 'Open' : 'Closed' + end + + 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 - } - ] + 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 end end diff --git a/lib/gitlab/chat_commands/presenters/list_issues.rb b/lib/gitlab/chat_commands/presenters/list_issues.rb index 5a7b3fca5c2..2458b9356b7 100644 --- a/lib/gitlab/chat_commands/presenters/list_issues.rb +++ b/lib/gitlab/chat_commands/presenters/list_issues.rb @@ -1,32 +1,43 @@ -module Gitlab::ChatCommands::Presenters - class ListIssues < Gitlab::ChatCommands::Presenters::Base - def present - ephemeral_response(text: "Here are the issues I found:", attachments: attachments) - end +module Gitlab + module ChatCommands + module Presenters + class ListIssues < Presenters::Issuable + def present + text = if @resource.count >= 5 + "Here are the first 5 issues I found:" + else + "Here are the #{@resource.count} issues I found:" + end - private + ephemeral_response(text: text, attachments: attachments) + end - def attachments - @resource.map do |issue| - state = issue.open? ? "Open" : "Closed" + private - { - 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 attachments + @resource.map do |issue| + url = "[#{issue.to_reference}](#{url_for([namespace, project, issue])})" - def project - @project ||= @resource.first.project - end + { + color: color(issue), + fallback: "#{issue.to_reference} #{issue.title}", + text: "#{url} · #{issue.title} (#{status_text(issue)})", - def namespace - @namespace ||= project.namespace.becomes(Namespace) + mrkdwn_in: [ + "text" + ] + } + end + end + + def project + @project ||= @resource.first.project + end + + def namespace + @namespace ||= project.namespace.becomes(Namespace) + end + end end end end diff --git a/lib/gitlab/chat_commands/presenters/new_issue.rb b/lib/gitlab/chat_commands/presenters/new_issue.rb new file mode 100644 index 00000000000..c7c6febb56e --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/new_issue.rb @@ -0,0 +1,42 @@ +module Gitlab + module ChatCommands + module Presenters + class NewIssue < Presenters::Issuable + def present + in_channel_response(show_issue) + end + + private + + def show_issue + { + attachments: [ + { + title: "#{@resource.title} · #{@resource.to_reference}", + title_link: resource_url, + author_name: author.name, + author_icon: author.avatar_url, + fallback: "New issue #{@resource.to_reference}: #{@resource.title}", + pretext: pretext, + color: color(@resource), + fields: fields, + mrkdwn_in: [ + :title, + :text + ] + } + ] + } + end + + def pretext + "I opened an issue on behalf on #{author_profile_link}: *#{@resource.to_reference}* from #{project.name_with_namespace}" + end + + def author_profile_link + "[#{author.to_reference}](#{url_for(author)})" + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/show_issue.rb b/lib/gitlab/chat_commands/presenters/show_issue.rb index 2a89c30b972..e5644a4ad7e 100644 --- a/lib/gitlab/chat_commands/presenters/show_issue.rb +++ b/lib/gitlab/chat_commands/presenters/show_issue.rb @@ -1,38 +1,54 @@ -module Gitlab::ChatCommands::Presenters - class ShowIssue < Gitlab::ChatCommands::Presenters::Issuable - def present - in_channel_response(show_issue) - end +module Gitlab + module ChatCommands + module Presenters + class ShowIssue < Presenters::Issuable + def present + in_channel_response(show_issue) + end - private + private - def show_issue - { - attachments: [ + def show_issue { - 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 + attachments: [ + { + title: "#{@resource.title} · #{@resource.to_reference}", + title_link: resource_url, + author_name: author.name, + author_icon: author.avatar_url, + fallback: "New issue #{@resource.to_reference}: #{@resource.title}", + pretext: pretext, + text: text, + color: color(@resource), + fields: fields, + mrkdwn_in: [ + :pretext, + :text + ] + } ] } - ] - } - end + end + + def text + message = "**#{status_text(@resource)}**" + + if @resource.upvotes.zero? && @resource.downvotes.zero? && @resource.user_notes_count.zero? + return message + end + + 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? - 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 - message + def pretext + "Issue *#{@resource.to_reference} from #{project.name_with_namespace}" + end + end end end end diff --git a/spec/lib/gitlab/chat_commands/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/issue_search_spec.rb index 04d10ad52a1..551ccb79a58 100644 --- a/spec/lib/gitlab/chat_commands/issue_search_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_search_spec.rb @@ -26,7 +26,7 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do it 'returns all results' do expect(subject).to have_key(:attachments) - expect(subject[:text]).to match("Here are the issues I found:") + expect(subject[:text]).to eq("Here are the 2 issues I found:") end end diff --git a/spec/lib/gitlab/chat_commands/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/issue_show_spec.rb index 89932c395c6..1f20d0a44ce 100644 --- a/spec/lib/gitlab/chat_commands/issue_show_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_show_spec.rb @@ -20,7 +20,7 @@ describe Gitlab::ChatCommands::IssueShow, service: true do it 'returns the issue' do expect(subject[:response_type]).to be(:in_channel) - expect(title).to eq(issue.title) + expect(title).to start_with(issue.title) end context 'when its reference is given' do @@ -28,7 +28,7 @@ describe Gitlab::ChatCommands::IssueShow, service: true do it 'shows the issue' do expect(subject[:response_type]).to be(:in_channel) - expect(title).to eq(issue.title) + expect(title).to start_with(issue.title) end end end diff --git a/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb index 1c48c727e30..dc2dd300072 100644 --- a/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb +++ b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb @@ -32,7 +32,7 @@ describe Gitlab::ChatCommands::Presenters::Deploy do end describe '#too_many_actions' do - subject { described_class.new(nil).too_many_actions } + subject { described_class.new([]).too_many_actions } it { is_expected.to have_key(:text) } it { is_expected.to have_key(:response_type) } diff --git a/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb b/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb index 1852395fc97..13a1f70fe78 100644 --- a/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb +++ b/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb @@ -3,13 +3,12 @@ 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 + it 'formats the message correct' do is_expected.to have_key(:text) is_expected.to have_key(:status) is_expected.to have_key(:response_type) @@ -19,6 +18,6 @@ describe Gitlab::ChatCommands::Presenters::ListIssues do 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") + expect(message).to start_with("Here are the 2 issues I found") end end diff --git a/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb b/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb index 13a318fe680..ca4062e692a 100644 --- a/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb +++ b/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb @@ -12,7 +12,7 @@ describe Gitlab::ChatCommands::Presenters::ShowIssue do 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) + expect(attachment[:title]).to start_with(issue.title) end context 'with upvotes' do @@ -21,7 +21,7 @@ describe Gitlab::ChatCommands::Presenters::ShowIssue do end it 'shows the upvote count' do - expect(attachment[:text]).to start_with(":+1: 1") + expect(attachment[:text]).to start_with("**Open** · :+1: 1") end end end diff --git a/spec/lib/mattermost/client_spec.rb b/spec/lib/mattermost/client_spec.rb new file mode 100644 index 00000000000..dc11a414717 --- /dev/null +++ b/spec/lib/mattermost/client_spec.rb @@ -0,0 +1,24 @@ +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 diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb new file mode 100644 index 00000000000..5ccf1100898 --- /dev/null +++ b/spec/lib/mattermost/command_spec.rb @@ -0,0 +1,61 @@ +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 diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb new file mode 100644 index 00000000000..74d12e37181 --- /dev/null +++ b/spec/lib/mattermost/session_spec.rb @@ -0,0 +1,123 @@ +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 diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb new file mode 100644 index 00000000000..2d14be6bcc2 --- /dev/null +++ b/spec/lib/mattermost/team_spec.rb @@ -0,0 +1,66 @@ +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 -- cgit v1.2.1 From 5ec214b0a5d9d3f0f0418a0e14ebf30b60a14a12 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Thu, 26 Jan 2017 15:30:34 +0100 Subject: Rename presenters for consitency --- lib/gitlab/chat_commands/command.rb | 2 +- lib/gitlab/chat_commands/issue_create.rb | 42 ------------ lib/gitlab/chat_commands/issue_new.rb | 42 ++++++++++++ lib/gitlab/chat_commands/issue_search.rb | 8 +-- lib/gitlab/chat_commands/issue_show.rb | 2 +- lib/gitlab/chat_commands/presenters/deploy.rb | 8 --- lib/gitlab/chat_commands/presenters/help.rb | 6 +- lib/gitlab/chat_commands/presenters/issuable.rb | 4 +- lib/gitlab/chat_commands/presenters/issue_new.rb | 48 +++++++++++++ .../chat_commands/presenters/issue_search.rb | 45 +++++++++++++ lib/gitlab/chat_commands/presenters/issue_show.rb | 56 ++++++++++++++++ lib/gitlab/chat_commands/presenters/list_issues.rb | 43 ------------ lib/gitlab/chat_commands/presenters/new_issue.rb | 42 ------------ lib/gitlab/chat_commands/presenters/show_issue.rb | 54 --------------- lib/mattermost/command.rb | 4 -- spec/lib/gitlab/chat_commands/command_spec.rb | 2 +- spec/lib/gitlab/chat_commands/issue_create_spec.rb | 78 ---------------------- spec/lib/gitlab/chat_commands/issue_new_spec.rb | 78 ++++++++++++++++++++++ .../chat_commands/presenters/issue_new_spec.rb | 17 +++++ .../chat_commands/presenters/issue_search_spec.rb | 23 +++++++ .../chat_commands/presenters/issue_show_spec.rb | 27 ++++++++ .../chat_commands/presenters/list_issues_spec.rb | 23 ------- .../chat_commands/presenters/show_issue_spec.rb | 27 -------- 23 files changed, 346 insertions(+), 335 deletions(-) delete mode 100644 lib/gitlab/chat_commands/issue_create.rb create mode 100644 lib/gitlab/chat_commands/issue_new.rb create mode 100644 lib/gitlab/chat_commands/presenters/issue_new.rb create mode 100644 lib/gitlab/chat_commands/presenters/issue_search.rb create mode 100644 lib/gitlab/chat_commands/presenters/issue_show.rb delete mode 100644 lib/gitlab/chat_commands/presenters/list_issues.rb delete mode 100644 lib/gitlab/chat_commands/presenters/new_issue.rb delete mode 100644 lib/gitlab/chat_commands/presenters/show_issue.rb delete mode 100644 spec/lib/gitlab/chat_commands/issue_create_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/issue_new_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb delete mode 100644 spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb delete mode 100644 spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index 4e5031a8a26..e7baa20356c 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -3,7 +3,7 @@ module Gitlab class Command < BaseCommand COMMANDS = [ Gitlab::ChatCommands::IssueShow, - Gitlab::ChatCommands::IssueCreate, + Gitlab::ChatCommands::IssueNew, Gitlab::ChatCommands::IssueSearch, Gitlab::ChatCommands::Deploy, ].freeze diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb deleted file mode 100644 index 3f3d7de8b2e..00000000000 --- a/lib/gitlab/chat_commands/issue_create.rb +++ /dev/null @@ -1,42 +0,0 @@ -module Gitlab - module ChatCommands - class IssueCreate < IssueCommand - def self.match(text) - # we can not match \n with the dot by passing the m modifier as than - # the title and description are not seperated - /\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text) - end - - def self.help_message - 'issue new <title> *`⇧ Shift`*+*`↵ Enter`* <description>' - end - - def self.allowed?(project, user) - can?(user, :create_issue, project) - end - - def execute(match) - title = match[:title] - 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 - end - - def presenter(issue) - Gitlab::ChatCommands::Presenters::NewIssue.new(issue) - end - end - end -end diff --git a/lib/gitlab/chat_commands/issue_new.rb b/lib/gitlab/chat_commands/issue_new.rb new file mode 100644 index 00000000000..016054ecd46 --- /dev/null +++ b/lib/gitlab/chat_commands/issue_new.rb @@ -0,0 +1,42 @@ +module Gitlab + module ChatCommands + class IssueNew < IssueCommand + def self.match(text) + # we can not match \n with the dot by passing the m modifier as than + # the title and description are not seperated + /\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text) + end + + def self.help_message + 'issue new <title> *`⇧ Shift`*+*`↵ Enter`* <description>' + end + + def self.allowed?(project, user) + can?(user, :create_issue, project) + end + + def execute(match) + title = match[:title] + description = match[:description].to_s.rstrip + + issue = create_issue(title: title, description: description) + + if issue.persisted? + presenter(issue).present + else + presenter(issue).display_errors + end + end + + private + + def create_issue(title:, description:) + Issues::CreateService.new(project, current_user, title: title, description: description).execute + end + + def presenter(issue) + Gitlab::ChatCommands::Presenters::IssueNew.new(issue) + end + end + end +end diff --git a/lib/gitlab/chat_commands/issue_search.rb b/lib/gitlab/chat_commands/issue_search.rb index e2d3a0f466a..3491b53093e 100644 --- a/lib/gitlab/chat_commands/issue_search.rb +++ b/lib/gitlab/chat_commands/issue_search.rb @@ -12,12 +12,10 @@ module Gitlab def execute(match) 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 + if issues.present? + Presenters::IssueSearch.new(issues).present else - Presenters::ListIssues.new(issues).present + Presenters::Access.new(issues).not_found end end end diff --git a/lib/gitlab/chat_commands/issue_show.rb b/lib/gitlab/chat_commands/issue_show.rb index 9f3e1b9a64b..d6013f4d10c 100644 --- a/lib/gitlab/chat_commands/issue_show.rb +++ b/lib/gitlab/chat_commands/issue_show.rb @@ -13,7 +13,7 @@ module Gitlab issue = find_by_iid(match[:iid]) if issue - Gitlab::ChatCommands::Presenters::ShowIssue.new(issue).present + Gitlab::ChatCommands::Presenters::IssueShow.new(issue).present else Gitlab::ChatCommands::Presenters::Access.new.not_found end diff --git a/lib/gitlab/chat_commands/presenters/deploy.rb b/lib/gitlab/chat_commands/presenters/deploy.rb index b1cfaac15af..863d0bf99ca 100644 --- a/lib/gitlab/chat_commands/presenters/deploy.rb +++ b/lib/gitlab/chat_commands/presenters/deploy.rb @@ -15,14 +15,6 @@ module Gitlab 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 end diff --git a/lib/gitlab/chat_commands/presenters/help.rb b/lib/gitlab/chat_commands/presenters/help.rb index c7a67467b7e..39ad3249f5b 100644 --- a/lib/gitlab/chat_commands/presenters/help.rb +++ b/lib/gitlab/chat_commands/presenters/help.rb @@ -9,10 +9,10 @@ module Gitlab private def help_message(trigger) - if @resource.none? - "No commands available :thinking_face:" - else + if @resource.present? header_with_list("Available commands", full_commands(trigger)) + else + "No commands available :thinking_face:" end end diff --git a/lib/gitlab/chat_commands/presenters/issuable.rb b/lib/gitlab/chat_commands/presenters/issuable.rb index 2cb6b1525fc..dfb1c8f6616 100644 --- a/lib/gitlab/chat_commands/presenters/issuable.rb +++ b/lib/gitlab/chat_commands/presenters/issuable.rb @@ -1,9 +1,7 @@ module Gitlab module ChatCommands module Presenters - class Issuable < Presenters::Base - private - + module Issuable def color(issuable) issuable.open? ? '#38ae67' : '#d22852' end diff --git a/lib/gitlab/chat_commands/presenters/issue_new.rb b/lib/gitlab/chat_commands/presenters/issue_new.rb new file mode 100644 index 00000000000..d26dd22b2a0 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/issue_new.rb @@ -0,0 +1,48 @@ +module Gitlab + module ChatCommands + module Presenters + class IssueNew < Presenters::Base + include Presenters::Issuable + + def present + in_channel_response(new_issue) + end + + private + + def new_issue + { + attachments: [ + { + title: "#{@resource.title} · #{@resource.to_reference}", + title_link: resource_url, + author_name: author.name, + author_icon: author.avatar_url, + fallback: "New issue #{@resource.to_reference}: #{@resource.title}", + pretext: pretext, + color: color(@resource), + fields: fields, + mrkdwn_in: [ + :title, + :text + ] + } + ] + } + end + + def pretext + "I opened an issue on behalf on #{author_profile_link}: *#{@resource.to_reference}* from #{project.name_with_namespace}" + end + + def project_link + "[#{project.name_with_namespace}](#{url_for(project)})" + end + + def author_profile_link + "[#{author.to_reference}](#{url_for(author)})" + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/issue_search.rb b/lib/gitlab/chat_commands/presenters/issue_search.rb new file mode 100644 index 00000000000..d58a6d6114a --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/issue_search.rb @@ -0,0 +1,45 @@ +module Gitlab + module ChatCommands + module Presenters + class IssueSearch < Presenters::Base + include Presenters::Issuable + + def present + text = if @resource.count >= 5 + "Here are the first 5 issues I found:" + else + "Here are the #{@resource.count} issues I found:" + end + + ephemeral_response(text: text, attachments: attachments) + end + + private + + def attachments + @resource.map do |issue| + url = "[#{issue.to_reference}](#{url_for([namespace, project, issue])})" + + { + color: color(issue), + fallback: "#{issue.to_reference} #{issue.title}", + text: "#{url} · #{issue.title} (#{status_text(issue)})", + + mrkdwn_in: [ + "text" + ] + } + end + end + + def project + @project ||= @resource.first.project + end + + def namespace + @namespace ||= project.namespace.becomes(Namespace) + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/issue_show.rb b/lib/gitlab/chat_commands/presenters/issue_show.rb new file mode 100644 index 00000000000..2fc671f13a6 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/issue_show.rb @@ -0,0 +1,56 @@ +module Gitlab + module ChatCommands + module Presenters + class IssueShow < Presenters::Base + include Presenters::Issuable + + def present + in_channel_response(show_issue) + end + + private + + def show_issue + { + attachments: [ + { + title: "#{@resource.title} · #{@resource.to_reference}", + title_link: resource_url, + author_name: author.name, + author_icon: author.avatar_url, + fallback: "Issue #{@resource.to_reference}: #{@resource.title}", + pretext: pretext, + text: text, + color: color(@resource), + fields: fields, + mrkdwn_in: [ + :pretext, + :text + ] + } + ] + } + end + + def text + message = "**#{status_text(@resource)}**" + + if @resource.upvotes.zero? && @resource.downvotes.zero? && @resource.user_notes_count.zero? + return message + end + + 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 + + def pretext + "Issue *#{@resource.to_reference} from #{project.name_with_namespace}" + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/list_issues.rb b/lib/gitlab/chat_commands/presenters/list_issues.rb deleted file mode 100644 index 2458b9356b7..00000000000 --- a/lib/gitlab/chat_commands/presenters/list_issues.rb +++ /dev/null @@ -1,43 +0,0 @@ -module Gitlab - module ChatCommands - module Presenters - class ListIssues < Presenters::Issuable - def present - text = if @resource.count >= 5 - "Here are the first 5 issues I found:" - else - "Here are the #{@resource.count} issues I found:" - end - - ephemeral_response(text: text, attachments: attachments) - end - - private - - def attachments - @resource.map do |issue| - url = "[#{issue.to_reference}](#{url_for([namespace, project, issue])})" - - { - color: color(issue), - fallback: "#{issue.to_reference} #{issue.title}", - text: "#{url} · #{issue.title} (#{status_text(issue)})", - - mrkdwn_in: [ - "text" - ] - } - end - end - - def project - @project ||= @resource.first.project - end - - def namespace - @namespace ||= project.namespace.becomes(Namespace) - end - end - end - end -end diff --git a/lib/gitlab/chat_commands/presenters/new_issue.rb b/lib/gitlab/chat_commands/presenters/new_issue.rb deleted file mode 100644 index c7c6febb56e..00000000000 --- a/lib/gitlab/chat_commands/presenters/new_issue.rb +++ /dev/null @@ -1,42 +0,0 @@ -module Gitlab - module ChatCommands - module Presenters - class NewIssue < Presenters::Issuable - def present - in_channel_response(show_issue) - end - - private - - def show_issue - { - attachments: [ - { - title: "#{@resource.title} · #{@resource.to_reference}", - title_link: resource_url, - author_name: author.name, - author_icon: author.avatar_url, - fallback: "New issue #{@resource.to_reference}: #{@resource.title}", - pretext: pretext, - color: color(@resource), - fields: fields, - mrkdwn_in: [ - :title, - :text - ] - } - ] - } - end - - def pretext - "I opened an issue on behalf on #{author_profile_link}: *#{@resource.to_reference}* from #{project.name_with_namespace}" - end - - def author_profile_link - "[#{author.to_reference}](#{url_for(author)})" - end - end - end - end -end diff --git a/lib/gitlab/chat_commands/presenters/show_issue.rb b/lib/gitlab/chat_commands/presenters/show_issue.rb deleted file mode 100644 index e5644a4ad7e..00000000000 --- a/lib/gitlab/chat_commands/presenters/show_issue.rb +++ /dev/null @@ -1,54 +0,0 @@ -module Gitlab - module ChatCommands - module Presenters - class ShowIssue < Presenters::Issuable - def present - in_channel_response(show_issue) - end - - private - - def show_issue - { - attachments: [ - { - title: "#{@resource.title} · #{@resource.to_reference}", - title_link: resource_url, - author_name: author.name, - author_icon: author.avatar_url, - fallback: "New issue #{@resource.to_reference}: #{@resource.title}", - pretext: pretext, - text: text, - color: color(@resource), - fields: fields, - mrkdwn_in: [ - :pretext, - :text - ] - } - ] - } - end - - def text - message = "**#{status_text(@resource)}**" - - if @resource.upvotes.zero? && @resource.downvotes.zero? && @resource.user_notes_count.zero? - return message - end - - 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 - - def pretext - "Issue *#{@resource.to_reference} from #{project.name_with_namespace}" - end - end - end - end -end diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb index 2e4f7705f86..33e450d7f0a 100644 --- a/lib/mattermost/command.rb +++ b/lib/mattermost/command.rb @@ -1,11 +1,7 @@ module Mattermost class Command < Client def create(params) -<<<<<<< HEAD response = session_post("/api/v3/teams/#{params[:team_id]}/commands/create", -======= - response = json_post("/api/v3/teams/#{params[:team_id]}/commands/create", ->>>>>>> Revert removing of some files body: params.to_json) response['token'] diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index d8b2303555c..0acf40de1d3 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -94,7 +94,7 @@ describe Gitlab::ChatCommands::Command, service: true do context 'IssueCreate is triggered' do let(:params) { { text: 'issue create my title' } } - it { is_expected.to eq(Gitlab::ChatCommands::IssueCreate) } + it { is_expected.to eq(Gitlab::ChatCommands::IssueNew) } end context 'IssueSearch is triggered' do diff --git a/spec/lib/gitlab/chat_commands/issue_create_spec.rb b/spec/lib/gitlab/chat_commands/issue_create_spec.rb deleted file mode 100644 index 0f84b19a5a4..00000000000 --- a/spec/lib/gitlab/chat_commands/issue_create_spec.rb +++ /dev/null @@ -1,78 +0,0 @@ -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[:response_type]).to be(:in_channel) - 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 - - context "with more newlines between the title and the description" do - let(:description) { "Surfin bird" } - let(:regex_match) { described_class.match("issue create bird is the word\n\n#{description}\n") } - - it 'creates the issue' do - expect { subject }.to change { project.issues.count }.by(1) - 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 - - 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 - - it 'matches the alias new' do - match = described_class.match("issue new my title") - - expect(match).not_to be_nil - expect(match[:title]).to eq('my title') - end - end -end diff --git a/spec/lib/gitlab/chat_commands/issue_new_spec.rb b/spec/lib/gitlab/chat_commands/issue_new_spec.rb new file mode 100644 index 00000000000..84c22328064 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/issue_new_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::IssueNew, 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[:response_type]).to be(:in_channel) + 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 + + context "with more newlines between the title and the description" do + let(:description) { "Surfin bird" } + let(:regex_match) { described_class.match("issue create bird is the word\n\n#{description}\n") } + + it 'creates the issue' do + expect { subject }.to change { project.issues.count }.by(1) + 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 + + 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 + + it 'matches the alias new' do + match = described_class.match("issue new my title") + + expect(match).not_to be_nil + expect(match[:title]).to eq('my title') + end + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb new file mode 100644 index 00000000000..17fcdbc2452 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::IssueNew 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 start_with(issue.title) + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb new file mode 100644 index 00000000000..ec6d3e34a96 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::IssueSearch do + let(:project) { create(:empty_project) } + let(:message) { subject[:text] } + + before { create_list(:issue, 2, project: project) } + + subject { described_class.new(project.issues).present } + + it 'formats the message correct' 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 2 issues I found") + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb new file mode 100644 index 00000000000..89d154e26e4 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::IssueShow 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 start_with(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("**Open** · :+1: 1") + end + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb b/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb deleted file mode 100644 index 13a1f70fe78..00000000000 --- a/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'spec_helper' - -describe Gitlab::ChatCommands::Presenters::ListIssues do - let(:project) { create(:empty_project) } - let(:message) { subject[:text] } - - before { create_list(:issue, 2, project: project) } - - subject { described_class.new(project.issues).present } - - it 'formats the message correct' 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 2 issues I found") - end -end diff --git a/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb b/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb deleted file mode 100644 index ca4062e692a..00000000000 --- a/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb +++ /dev/null @@ -1,27 +0,0 @@ -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 start_with(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("**Open** · :+1: 1") - end - end -end -- cgit v1.2.1 From 500e1a56e0a2225a61ec4bea40a474e7e3e3d1cc Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Mon, 30 Jan 2017 16:58:30 +0600 Subject: unifies mr diff file button style --- app/helpers/commits_helper.rb | 2 +- app/views/projects/diffs/_file.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index e9461b9f859..6dcb624c4da 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -198,7 +198,7 @@ module CommitsHelper link_to( namespace_project_blob_path(project.namespace, project, tree_join(commit_sha, diff_new_path)), - class: 'btn view-file js-view-file btn-file-option' + class: 'btn view-file js-view-file' ) do raw('View file @') + content_tag(:span, commit_sha[0..6], class: 'commit-short-id') diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index c37a33bbcd5..fc478ccc995 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -5,7 +5,7 @@ - unless diff_file.submodule? .file-actions.hidden-xs - if blob_text_viewable?(blob) - = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file", disabled: @diff_notes_disabled do + = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: "Toggle comments for this file", disabled: @diff_notes_disabled do = icon('comment') \ - if editable_diff?(diff_file) -- cgit v1.2.1 From d4dd1fcf93d10b65b9e1b5ca392daacaf7c5138c Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Mon, 30 Jan 2017 17:19:32 +0600 Subject: adds changelog --- changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml diff --git a/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml b/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml new file mode 100644 index 00000000000..293aab67d39 --- /dev/null +++ b/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml @@ -0,0 +1,4 @@ +--- +title: Unify MR diff file button style +merge_request: 8874 +author: -- cgit v1.2.1 From d8b8168623ae780b72723546524cce0aae979556 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Mon, 30 Jan 2017 12:18:52 -0600 Subject: Remove underline style for icon hover --- app/assets/stylesheets/framework/icons.scss | 6 ++++++ app/views/projects/merge_requests/widget/_heading.html.haml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index dccf5177e35..33de9022da7 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -57,3 +57,9 @@ fill: $gl-text-color; } } + +.icon-link { + &:hover { + text-decoration: none; + } +} diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index cc939ab9441..ecd626872a5 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -2,7 +2,7 @@ .mr-widget-heading - %w[success success_with_warnings skipped canceled failed running pending].each do |status| .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: ("display:none" unless @pipeline.status == status) } - = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id) do + = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do = ci_icon_for_status(status) %span Pipeline -- cgit v1.2.1 From 29414ab0438583c7401e94a74a613497874b5e4e Mon Sep 17 00:00:00 2001 From: Drew Blessing <drew@gitlab.com> Date: Tue, 24 Jan 2017 11:12:49 -0600 Subject: Reduce hits to LDAP on Git HTTP auth by reordering auth mechanisms We accept half a dozen different authentication mechanisms for Git over HTTP. Fairly high in the list we were checking user password, which would also query LDAP. In the case of LFS, OAuth tokens or personal access tokens, we were unnecessarily hitting LDAP when the authentication will not succeed. This was causing some LDAP/AD systems to lock the account. Now, user password authentication is the last mechanism tried since it's the most expensive. --- .../24462-reduce_ldap_queries_for_lfs.yml | 4 + lib/gitlab/auth.rb | 11 ++- spec/lib/gitlab/auth_spec.rb | 96 ++++++++++++++++------ 3 files changed, 82 insertions(+), 29 deletions(-) create mode 100644 changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml diff --git a/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml b/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml new file mode 100644 index 00000000000..05fbd8f0bf2 --- /dev/null +++ b/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml @@ -0,0 +1,4 @@ +--- +title: Reduce hits to LDAP on Git HTTP auth by reordering auth mechanisms +merge_request: 8752 +author: diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 8dda65c71ef..f638905a1e0 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -10,13 +10,16 @@ module Gitlab def find_for_git_client(login, password, project:, ip:) raise "Must provide an IP for rate limiting" if ip.nil? + # `user_with_password_for_git` should be the last check + # because it's the most expensive, especially when LDAP + # is enabled. result = service_request_check(login, password, project) || build_access_token_check(login, password) || - user_with_password_for_git(login, password) || - oauth_access_token_check(login, password) || lfs_token_check(login, password) || + oauth_access_token_check(login, password) || personal_access_token_check(login, password) || + user_with_password_for_git(login, password) || Gitlab::Auth::Result.new rate_limit!(ip, success: result.success?, login: login) @@ -143,7 +146,9 @@ module Gitlab read_authentication_abilities end - Result.new(actor, nil, token_handler.type, authentication_abilities) if Devise.secure_compare(token_handler.token, password) + if Devise.secure_compare(token_handler.token, password) + Gitlab::Auth::Result.new(actor, nil, token_handler.type, authentication_abilities) + end end def build_access_token_check(login, password) diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index f251c0dd25a..b234de4c772 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -58,58 +58,102 @@ describe Gitlab::Auth, lib: true do expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) end - it 'recognizes user lfs tokens' do - user = create(:user) - token = Gitlab::LfsToken.new(user).token + context 'while using LFS authenticate' do + it 'recognizes user lfs tokens' do + user = create(:user) + token = Gitlab::LfsToken.new(user).token - expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username) - expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities)) - end + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username) + expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities)) + end - it 'recognizes deploy key lfs tokens' do - key = create(:deploy_key) - token = Gitlab::LfsToken.new(key).token + it 'recognizes deploy key lfs tokens' do + key = create(:deploy_key) + token = Gitlab::LfsToken.new(key).token - expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}") - expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities)) - end + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}") + expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities)) + end - context "while using OAuth tokens as passwords" do - it 'succeeds for OAuth tokens with the `api` scope' do + it 'does not try password auth before oauth' do user = create(:user) - application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) - token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api") + token = Gitlab::LfsToken.new(user).token + + expect(gl_auth).not_to receive(:find_with_user_password) + gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip') + end + end + + context 'while using OAuth tokens as passwords' do + let(:user) { create(:user) } + let(:token_w_api_scope) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api') } + let(:application) { Doorkeeper::Application.create!(name: 'MyApp', redirect_uri: 'https://app.com', owner: user) } + + it 'succeeds for OAuth tokens with the `api` scope' do expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'oauth2') - expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)) + expect(gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)) end it 'fails for OAuth tokens with other scopes' do - user = create(:user) - application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) - token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "read_user") + token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'read_user') expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'oauth2') expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil)) end + + it 'does not try password auth before oauth' do + expect(gl_auth).not_to receive(:find_with_user_password) + + gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip') + end end - context "while using personal access tokens as passwords" do - it 'succeeds for personal access tokens with the `api` scope' do - user = create(:user) - personal_access_token = create(:personal_access_token, user: user, scopes: ['api']) + context 'while using personal access tokens as passwords' do + let(:user) { create(:user) } + let(:token_w_api_scope) { create(:personal_access_token, user: user, scopes: ['api']) } + it 'succeeds for personal access tokens with the `api` scope' do expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.email) - expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities)) + expect(gl_auth.find_for_git_client(user.email, token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities)) end it 'fails for personal access tokens with other scopes' do - user = create(:user) personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user']) expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: user.email) expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil)) end + + it 'does not try password auth before personal access tokens' do + expect(gl_auth).not_to receive(:find_with_user_password) + + gl_auth.find_for_git_client(user.email, token_w_api_scope.token, project: nil, ip: 'ip') + end + end + + context 'while using regular user and password' do + it 'falls through lfs authentication' do + user = create( + :user, + username: 'normal_user', + password: 'my-secret', + ) + + expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip')) + .to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) + end + + it 'falls through oauth authentication when the username is oauth2' do + user = create( + :user, + username: 'oauth2', + password: 'my-secret', + ) + + expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip')) + .to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) + end end it 'returns double nil for invalid credentials' do -- cgit v1.2.1 From d7f298c177555a09ac06acc9ad037611f664cc9e Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Mon, 30 Jan 2017 12:12:57 +0100 Subject: Incorporate feedback --- lib/gitlab/chat_commands/command.rb | 2 +- lib/gitlab/chat_commands/help.rb | 4 ++-- lib/gitlab/chat_commands/presenters/access.rb | 14 ++++++++++++++ lib/gitlab/chat_commands/presenters/help.rb | 12 +++++++----- lib/gitlab/chat_commands/presenters/issue_new.rb | 4 +++- lib/gitlab/chat_commands/presenters/issue_search.rb | 4 +++- lib/gitlab/chat_commands/presenters/issue_show.rb | 11 ++++++++--- spec/lib/gitlab/chat_commands/command_spec.rb | 6 +----- .../lib/gitlab/chat_commands/presenters/issue_show_spec.rb | 10 ++++++++++ 9 files changed, 49 insertions(+), 18 deletions(-) diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index e7baa20356c..f34ed0f4cf2 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -18,7 +18,7 @@ module Gitlab Gitlab::ChatCommands::Presenters::Access.new.access_denied end else - Gitlab::ChatCommands::Help.new(project, current_user, params).execute(available_commands) + Gitlab::ChatCommands::Help.new(project, current_user, params).execute(available_commands, params[:text]) end end diff --git a/lib/gitlab/chat_commands/help.rb b/lib/gitlab/chat_commands/help.rb index e76733f5445..6c0e4d304a4 100644 --- a/lib/gitlab/chat_commands/help.rb +++ b/lib/gitlab/chat_commands/help.rb @@ -16,8 +16,8 @@ module Gitlab true end - def execute(commands) - Gitlab::ChatCommands::Presenters::Help.new(commands).present(trigger) + def execute(commands, text) + Gitlab::ChatCommands::Presenters::Help.new(commands).present(trigger, text) end def trigger diff --git a/lib/gitlab/chat_commands/presenters/access.rb b/lib/gitlab/chat_commands/presenters/access.rb index b66ef48d6a8..92f4fa17f78 100644 --- a/lib/gitlab/chat_commands/presenters/access.rb +++ b/lib/gitlab/chat_commands/presenters/access.rb @@ -20,6 +20,20 @@ module Gitlab ephemeral_response(text: message) end + + def unknown_command(commands) + ephemeral_response(text: help_message(trigger)) + end + + private + + def help_message(trigger) + header_with_list("Command not found, these are the commands you can use", full_commands(trigger)) + end + + def full_commands(trigger) + @resource.map { |command| "#{trigger} #{command.help_message}" } + end end end end diff --git a/lib/gitlab/chat_commands/presenters/help.rb b/lib/gitlab/chat_commands/presenters/help.rb index 39ad3249f5b..cd47b7f4c6a 100644 --- a/lib/gitlab/chat_commands/presenters/help.rb +++ b/lib/gitlab/chat_commands/presenters/help.rb @@ -2,17 +2,19 @@ module Gitlab module ChatCommands module Presenters class Help < Presenters::Base - def present(trigger) - ephemeral_response(text: help_message(trigger)) + def present(trigger, text) + ephemeral_response(text: help_message(trigger, text)) end private - def help_message(trigger) - if @resource.present? + def help_message(trigger, text) + return "No commands available :thinking_face:" unless @resource.present? + + if text.start_with?('help') header_with_list("Available commands", full_commands(trigger)) else - "No commands available :thinking_face:" + header_with_list("Unknown command, these commands are available", full_commands(trigger)) end end diff --git a/lib/gitlab/chat_commands/presenters/issue_new.rb b/lib/gitlab/chat_commands/presenters/issue_new.rb index d26dd22b2a0..6e88e0574a3 100644 --- a/lib/gitlab/chat_commands/presenters/issue_new.rb +++ b/lib/gitlab/chat_commands/presenters/issue_new.rb @@ -24,7 +24,9 @@ module Gitlab fields: fields, mrkdwn_in: [ :title, - :text + :pretext, + :text, + :fields ] } ] diff --git a/lib/gitlab/chat_commands/presenters/issue_search.rb b/lib/gitlab/chat_commands/presenters/issue_search.rb index d58a6d6114a..3478359b91d 100644 --- a/lib/gitlab/chat_commands/presenters/issue_search.rb +++ b/lib/gitlab/chat_commands/presenters/issue_search.rb @@ -7,6 +7,8 @@ module Gitlab def present text = if @resource.count >= 5 "Here are the first 5 issues I found:" + elsif @resource.one? + "Here is the only issue I found:" else "Here are the #{@resource.count} issues I found:" end @@ -26,7 +28,7 @@ module Gitlab text: "#{url} · #{issue.title} (#{status_text(issue)})", mrkdwn_in: [ - "text" + :text ] } end diff --git a/lib/gitlab/chat_commands/presenters/issue_show.rb b/lib/gitlab/chat_commands/presenters/issue_show.rb index 2fc671f13a6..fe5847ccd15 100644 --- a/lib/gitlab/chat_commands/presenters/issue_show.rb +++ b/lib/gitlab/chat_commands/presenters/issue_show.rb @@ -5,7 +5,11 @@ module Gitlab include Presenters::Issuable def present - in_channel_response(show_issue) + if @resource.confidential? + ephemeral_response(show_issue) + else + in_channel_response(show_issue) + end end private @@ -25,7 +29,8 @@ module Gitlab fields: fields, mrkdwn_in: [ :pretext, - :text + :text, + :fields ] } ] @@ -48,7 +53,7 @@ module Gitlab end def pretext - "Issue *#{@resource.to_reference} from #{project.name_with_namespace}" + "Issue *#{@resource.to_reference}* from #{project.name_with_namespace}" end end end diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index 0acf40de1d3..b6e924d67be 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -5,7 +5,6 @@ describe Gitlab::ChatCommands::Command, service: true do let(:user) { create(:user) } describe '#execute' do -<<<<<<< HEAD subject do described_class.new(project, user, params).execute end @@ -19,16 +18,13 @@ describe Gitlab::ChatCommands::Command, service: true do expect(subject[:text]).to start_with('404 not found') end end -======= - subject { described_class.new(project, user, params).execute } ->>>>>>> Chat Commands have presenters 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 start_with('Unknown command') expect(subject[:text]).to match('/gitlab issue show') end end diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb index 89d154e26e4..5b678d31fce 100644 --- a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb +++ b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb @@ -21,7 +21,17 @@ describe Gitlab::ChatCommands::Presenters::IssueShow do end it 'shows the upvote count' do + expect(subject[:response_type]).to be(:in_channel) expect(attachment[:text]).to start_with("**Open** · :+1: 1") end end + + context 'confidential issue' do + let(:issue) { create(:issue, project: project) } + + it 'shows an ephemeral response' do + expect(subject[:response_type]).to be(:in_channel) + expect(attachment[:text]).to start_with("**Open**") + end + end end -- cgit v1.2.1 From b61b45a7596b3f20aa1a0d264e6ba3c7af844bac Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Mon, 30 Jan 2017 14:01:02 -0600 Subject: Link to pipeline page from commit widget --- app/views/projects/commit/_commit_box.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 2b1c4e28ce2..56f2c1529fe 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -64,10 +64,10 @@ - if @commit.status .well-segment.pipeline-info %div{ class: "icon-container ci-status-icon-#{@commit.status}" } - = link_to pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) do + = link_to namespace_project_pipeline_path(@project.namespace, @project, @commit.pipelines.last.id) do = ci_icon_for_status(@commit.status) Pipeline - = link_to "##{@commit.pipelines.last.id}", pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "monospace" + = link_to "##{@commit.pipelines.last.id}", namespace_project_pipeline_path(@project.namespace, @project, @commit.pipelines.last.id), class: "monospace" for = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" %span.ci-status-label -- cgit v1.2.1 From 538d1bffec52ecdaae44eaf9fdbcb6102c1cbefd Mon Sep 17 00:00:00 2001 From: Adam Pahlevi <adam.pahlevi@gmail.com> Date: Sat, 28 Jan 2017 09:50:25 +0700 Subject: resolve deprecation warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit don’t pass AR object, use the ID to avoid depr warning pass in the id instead of AR object to specs for `ProjectDestroyWorker` --- app/controllers/groups_controller.rb | 2 +- spec/models/members/project_member_spec.rb | 2 +- spec/workers/project_destroy_worker_spec.rb | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index f81237db991..264b14713fb 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -84,7 +84,7 @@ class GroupsController < Groups::ApplicationController if Groups::UpdateService.new(@group, current_user, group_params).execute redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated." else - @group.reset_path! + @group.restore_path! render action: "edit" end diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index 90d14c2c0b9..e4be0aba7a6 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -117,7 +117,7 @@ describe ProjectMember, models: true do users = create_list(:user, 2) described_class.add_users_to_projects( - [projects.first.id, projects.second], + [projects.first.id, projects.second.id], [users.first.id, users.second], described_class::MASTER) diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb index 1b910d9b91e..1f4c39eb64a 100644 --- a/spec/workers/project_destroy_worker_spec.rb +++ b/spec/workers/project_destroy_worker_spec.rb @@ -8,14 +8,14 @@ describe ProjectDestroyWorker do describe "#perform" do it "deletes the project" do - subject.perform(project.id, project.owner, {}) + subject.perform(project.id, project.owner.id, {}) expect(Project.all).not_to include(project) expect(Dir.exist?(path)).to be_falsey end it "deletes the project but skips repo deletion" do - subject.perform(project.id, project.owner, { "skip_repo" => true }) + subject.perform(project.id, project.owner.id, { "skip_repo" => true }) expect(Project.all).not_to include(project) expect(Dir.exist?(path)).to be_truthy -- cgit v1.2.1 From 124c99032292c11a2f69197233683db9bee4d463 Mon Sep 17 00:00:00 2001 From: Adam Pahlevi <adam.pahlevi@gmail.com> Date: Sat, 28 Jan 2017 10:18:39 +0700 Subject: add complete changelog --- changelogs/unreleased/fix-depr-warn.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-depr-warn.yml diff --git a/changelogs/unreleased/fix-depr-warn.yml b/changelogs/unreleased/fix-depr-warn.yml new file mode 100644 index 00000000000..61817027720 --- /dev/null +++ b/changelogs/unreleased/fix-depr-warn.yml @@ -0,0 +1,4 @@ +--- +title: resolve deprecation warnings +merge_request: 8855 +author: Adam Pahlevi -- cgit v1.2.1 From ad69a89f044a8493d14f9ef4ce2c7e09b830003f Mon Sep 17 00:00:00 2001 From: tauriedavis <taurie@gitlab.com> Date: Mon, 30 Jan 2017 19:29:35 -0800 Subject: 19164 Add settings dropdown to mobile screens --- app/assets/stylesheets/framework/nav.scss | 12 ++++++++---- changelogs/unreleased/19164-mobile-settings.yml | 4 ++++ 2 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/19164-mobile-settings.yml diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 401c2d0f6ee..adbc141262e 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -294,16 +294,19 @@ .container-fluid { position: relative; + + .nav-control { + @media (max-width: $screen-sm-max) { + text-align: left; + margin-right: 75px; + } + } } .controls { float: right; padding: 7px 0 0; - @media (max-width: $screen-sm-max) { - display: none; - } - i { color: $layout-link-gray; } @@ -361,6 +364,7 @@ .fade-left { @include fade(right, $gray-light); left: -5px; + text-align: center; .fa { left: -7px; diff --git a/changelogs/unreleased/19164-mobile-settings.yml b/changelogs/unreleased/19164-mobile-settings.yml new file mode 100644 index 00000000000..c26a20f87e2 --- /dev/null +++ b/changelogs/unreleased/19164-mobile-settings.yml @@ -0,0 +1,4 @@ +--- +title: 19164 Add settings dropdown to mobile screens +merge_request: +author: -- cgit v1.2.1 From 8615138706337e6aca75d635e8fe3a867f9e69bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Tue, 31 Jan 2017 11:05:50 +0100 Subject: Move Dashboard shortcuts specs from Spinah to RSpec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- features/dashboard/shortcuts.feature | 21 --------------------- features/steps/dashboard/shortcuts.rb | 7 ------- spec/features/dashboard/shortcuts_spec.rb | 29 +++++++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 28 deletions(-) delete mode 100644 features/dashboard/shortcuts.feature delete mode 100644 features/steps/dashboard/shortcuts.rb create mode 100644 spec/features/dashboard/shortcuts_spec.rb diff --git a/features/dashboard/shortcuts.feature b/features/dashboard/shortcuts.feature deleted file mode 100644 index 41d79aa6ec8..00000000000 --- a/features/dashboard/shortcuts.feature +++ /dev/null @@ -1,21 +0,0 @@ -@dashboard -Feature: Dashboard Shortcuts - Background: - Given I sign in as a user - And I visit dashboard page - - @javascript - Scenario: Navigate to projects tab - Given I press "g" and "p" - Then the active main tab should be Projects - - @javascript - Scenario: Navigate to issue tab - Given I press "g" and "i" - Then the active main tab should be Issues - - @javascript - Scenario: Navigate to merge requests tab - Given I press "g" and "m" - Then the active main tab should be Merge Requests - diff --git a/features/steps/dashboard/shortcuts.rb b/features/steps/dashboard/shortcuts.rb deleted file mode 100644 index 118d27888df..00000000000 --- a/features/steps/dashboard/shortcuts.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Spinach::Features::DashboardShortcuts < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedProject - include SharedSidebarActiveTab - include SharedShortcuts -end diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb new file mode 100644 index 00000000000..d9be4e5dbdd --- /dev/null +++ b/spec/features/dashboard/shortcuts_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +feature 'Dashboard shortcuts', feature: true, js: true do + before do + login_as :user + visit dashboard_projects_path + end + + scenario 'Navigate to tabs' do + find('body').native.send_key('g') + find('body').native.send_key('p') + + ensure_active_main_tab('Projects') + + find('body').native.send_key('g') + find('body').native.send_key('i') + + ensure_active_main_tab('Issues') + + find('body').native.send_key('g') + find('body').native.send_key('m') + + ensure_active_main_tab('Merge Requests') + end + + def ensure_active_main_tab(content) + expect(find('.nav-sidebar li.active')).to have_content(content) + end +end -- cgit v1.2.1 From ef20bb2edd932a8e144aded11c83046e77ea79d9 Mon Sep 17 00:00:00 2001 From: dimitrieh <dimitriehoekstra@gmail.com> Date: Tue, 31 Jan 2017 12:09:37 +0100 Subject: Edited the column header for the environments list from created to updated and added created to environments detail page colum header titles --- app/assets/javascripts/environments/components/environment.js.es6 | 2 +- app/views/projects/environments/show.html.haml | 2 +- changelogs/unreleased/27494-environment-list-column-headers.yml | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/27494-environment-list-column-headers.yml diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index fea642467fa..971be04e2d2 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -182,7 +182,7 @@ <th class="environments-deploy">Last deployment</th> <th class="environments-build">Build</th> <th class="environments-commit">Commit</th> - <th class="environments-date">Created</th> + <th class="environments-date">Updated</th> <th class="hidden-xs environments-actions"></th> </tr> </thead> diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 6e0d9456900..51c31a09378 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -33,7 +33,7 @@ %th ID %th Commit %th Build - %th + %th Created %th.hidden-xs = render @deployments diff --git a/changelogs/unreleased/27494-environment-list-column-headers.yml b/changelogs/unreleased/27494-environment-list-column-headers.yml new file mode 100644 index 00000000000..798c01f3238 --- /dev/null +++ b/changelogs/unreleased/27494-environment-list-column-headers.yml @@ -0,0 +1,4 @@ +--- +title: Edited the column header for the environments list from created to updated and added created to environments detail page colum header titles +merge_request: +author: -- cgit v1.2.1 From 4840c682414aa2e7d57fa6f792405a49709b7dd8 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Tue, 31 Jan 2017 10:40:24 +0000 Subject: Don't capitalize environment name in show page Upate test to match the new behavior --- app/views/projects/environments/show.html.haml | 2 +- changelogs/unreleased/27484-environment-show-name.yml | 4 ++++ spec/features/environment_spec.rb | 4 ++++ spec/features/environments_spec.rb | 2 +- 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/27484-environment-show-name.yml diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 6e0d9456900..b23ca109746 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -5,7 +5,7 @@ %div{ class: container_class } .top-area.adjust .col-md-9 - %h3.page-title= @environment.name.capitalize + %h3.page-title= @environment.name .col-md-3 .nav-controls = render 'projects/environments/terminal_button', environment: @environment diff --git a/changelogs/unreleased/27484-environment-show-name.yml b/changelogs/unreleased/27484-environment-show-name.yml new file mode 100644 index 00000000000..dc400d65006 --- /dev/null +++ b/changelogs/unreleased/27484-environment-show-name.yml @@ -0,0 +1,4 @@ +--- +title: Don't capitalize environment name in show page +merge_request: +author: diff --git a/spec/features/environment_spec.rb b/spec/features/environment_spec.rb index 56f6cd2e095..511c95b758f 100644 --- a/spec/features/environment_spec.rb +++ b/spec/features/environment_spec.rb @@ -19,6 +19,10 @@ feature 'Environment', :feature do visit_environment(environment) end + scenario 'shows environment name' do + expect(page).to have_content(environment.name) + end + context 'without deployments' do scenario 'does show no deployments' do expect(page).to have_content('You don\'t have any deployments right now.') diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index 72b984cfab8..c033b693213 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -194,7 +194,7 @@ feature 'Environments page', :feature, :js do end scenario 'does create a new pipeline' do - expect(page).to have_content('Production') + expect(page).to have_content('production') end end -- cgit v1.2.1 From 164eb3aa37cbcff2c5fbf582c3acdbaa3e6fee77 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Tue, 31 Jan 2017 12:00:43 +0100 Subject: Improve styling of the new issue message --- lib/gitlab/chat_commands/presenters/issue_new.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/chat_commands/presenters/issue_new.rb b/lib/gitlab/chat_commands/presenters/issue_new.rb index 6e88e0574a3..a1a3add56c9 100644 --- a/lib/gitlab/chat_commands/presenters/issue_new.rb +++ b/lib/gitlab/chat_commands/presenters/issue_new.rb @@ -34,11 +34,11 @@ module Gitlab end def pretext - "I opened an issue on behalf on #{author_profile_link}: *#{@resource.to_reference}* from #{project.name_with_namespace}" + "I created an issue on #{author_profile_link}'s behalf: **#{@resource.to_reference}** in #{project_link}" end def project_link - "[#{project.name_with_namespace}](#{url_for(project)})" + "[#{project.name_with_namespace}](#{projects_url(project)})" end def author_profile_link -- cgit v1.2.1 From a1a5dd4b5c9abac53bb0af6f2bd74dc1159cef8d Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Tue, 31 Jan 2017 16:10:16 +0100 Subject: add spec replicating validation error --- .../gitlab/import_export/members_mapper_spec.rb | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb index 0b7984d6ca9..495ca4b4955 100644 --- a/spec/lib/gitlab/import_export/members_mapper_spec.rb +++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb @@ -92,5 +92,33 @@ describe Gitlab::ImportExport::MembersMapper, services: true do expect(members_mapper.map[exported_user_id]).to eq(user2.id) end end + + context 'importer same as group member' do + let(:user2) { create(:admin, authorized_projects_populated: true) } + let(:group) { create(:group) } + let(:project) { create(:empty_project, :public, name: 'searchable_project', namespace: group) } + let(:members_mapper) do + described_class.new( + exported_members: exported_members, user: user2, project: project) + end + + before do + GroupMember.add_users_to_group( + group, + [user, user2], + GroupMember::DEVELOPER + ) + end + + it 'maps the project member' do + expect(members_mapper.map[exported_user_id]).to eq(user2.id) + end + + it 'maps the project member if it already exists' do + ProjectMember.create!(user: user2, access_level: ProjectMember::MASTER, source_id: project.id) + + expect(members_mapper.map[exported_user_id]).to eq(user2.id) + end + end end end -- cgit v1.2.1 From 918eaba0c3898b5b81d2db1cd598fd3fecac9ef8 Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Tue, 31 Jan 2017 16:32:26 +0100 Subject: remove old project members from project --- lib/gitlab/import_export/members_mapper.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb index 2405b94db50..6189867dc9c 100644 --- a/lib/gitlab/import_export/members_mapper.rb +++ b/lib/gitlab/import_export/members_mapper.rb @@ -41,6 +41,10 @@ module Gitlab end def ensure_default_member! + @project.project_members.each do |member| + member.destroy + end + ProjectMember.create!(user: @user, access_level: ProjectMember::MASTER, source_id: @project.id, importing: true) end -- cgit v1.2.1 From dc58df4160726b989aff0d4d80443b9f62c6ed56 Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Tue, 31 Jan 2017 16:33:02 +0100 Subject: add changelog --- changelogs/unreleased/fix-import-user-validation-error.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-import-user-validation-error.yml diff --git a/changelogs/unreleased/fix-import-user-validation-error.yml b/changelogs/unreleased/fix-import-user-validation-error.yml new file mode 100644 index 00000000000..985a3b0b26f --- /dev/null +++ b/changelogs/unreleased/fix-import-user-validation-error.yml @@ -0,0 +1,4 @@ +--- +title: Remove old project members when retrying an export +merge_request: +author: -- cgit v1.2.1 From 87346bfe6de362b832e27c75f3e9b1def2ef1f11 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Tue, 31 Jan 2017 10:04:29 -0600 Subject: Remove settings cog from within admin scroll tabs; keep links centered --- app/assets/stylesheets/framework/nav.scss | 1 - app/views/layouts/nav/_admin.html.haml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index adbc141262e..fd081c2d7e1 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -297,7 +297,6 @@ .nav-control { @media (max-width: $screen-sm-max) { - text-align: left; margin-right: 75px; } } diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index ac04f57e217..19a947af4ca 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -1,5 +1,5 @@ += render 'layouts/nav/admin_settings' .scrolling-tabs-container{ class: nav_control_class } - = render 'layouts/nav/admin_settings' .fade-left = icon('angle-left') .fade-right -- cgit v1.2.1 From 8ea1dafe83da0b018dcc413242194749afd0e05a Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Tue, 31 Jan 2017 19:20:35 +0100 Subject: use destroy_all --- lib/gitlab/import_export/members_mapper.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb index 6189867dc9c..a09577ae48d 100644 --- a/lib/gitlab/import_export/members_mapper.rb +++ b/lib/gitlab/import_export/members_mapper.rb @@ -41,9 +41,7 @@ module Gitlab end def ensure_default_member! - @project.project_members.each do |member| - member.destroy - end + @project.project_members.destroy_all ProjectMember.create!(user: @user, access_level: ProjectMember::MASTER, source_id: @project.id, importing: true) end -- cgit v1.2.1 From 7bf6df8463c4f8871682f385e9368d169b4ffecf Mon Sep 17 00:00:00 2001 From: Brian Hall <brian@hack.design> Date: Mon, 30 Jan 2017 22:12:31 -0600 Subject: Change the reply shortcut to focus the field even without a selection. --- app/assets/javascripts/lib/utils/common_utils.js.es6 | 1 + app/assets/javascripts/shortcuts_issuable.js | 9 ++++++--- changelogs/unreleased/empty-selection-reply-shortcut.yml | 4 ++++ spec/javascripts/shortcuts_issuable_spec.js | 12 ++++++++++-- 4 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 changelogs/unreleased/empty-selection-reply-shortcut.yml diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index 51993bb3420..e3bff2559fd 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -162,6 +162,7 @@ w.gl.utils.getSelectedFragment = () => { const selection = window.getSelection(); + if (selection.rangeCount === 0) return null; const documentFragment = selection.getRangeAt(0).cloneContents(); if (documentFragment.textContent.length === 0) return null; diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 4ef516af8c8..4dcc5ebe28f 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -39,17 +39,20 @@ } ShortcutsIssuable.prototype.replyWithSelectedText = function() { - var quote, replyField, documentFragment, selected, separator; + var quote, documentFragment, selected, separator; + var replyField = $('.js-main-target-form #note_note'); documentFragment = window.gl.utils.getSelectedFragment(); - if (!documentFragment) return; + if (!documentFragment) { + replyField.focus(); + return; + } // If the documentFragment contains more than just Markdown, don't copy as GFM. if (documentFragment.querySelector('.md, .wiki')) return; selected = window.gl.CopyAsGFM.nodeToGFM(documentFragment); - replyField = $('.js-main-target-form #note_note'); if (selected.trim() === "") { return; } diff --git a/changelogs/unreleased/empty-selection-reply-shortcut.yml b/changelogs/unreleased/empty-selection-reply-shortcut.yml new file mode 100644 index 00000000000..5a42c98a800 --- /dev/null +++ b/changelogs/unreleased/empty-selection-reply-shortcut.yml @@ -0,0 +1,4 @@ +--- +title: Change the reply shortcut to focus the field even without a selection. +merge_request: 8873 +author: Brian Hall diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index 386fc8f514e..db2302c4fb0 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -27,11 +27,19 @@ return this.selector = 'form.js-main-target-form textarea#note_note'; }); describe('with empty selection', function() { - return it('does nothing', function() { - stubSelection(''); + it('does not return an error', function() { this.shortcut.replyWithSelectedText(); return expect($(this.selector).val()).toBe(''); }); + return it('triggers `input`', function() { + var focused; + focused = false; + $(this.selector).on('focus', function() { + return focused = true; + }); + this.shortcut.replyWithSelectedText(); + return expect(focused).toBe(true); + }); }); describe('with any selection', function() { beforeEach(function() { -- cgit v1.2.1 From c6aed2dfc8f879a9edf611235cae3760f3af3d4e Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Tue, 31 Jan 2017 20:06:05 +0100 Subject: update spec --- spec/lib/gitlab/import_export/members_mapper_spec.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb index 495ca4b4955..f2cb028206f 100644 --- a/spec/lib/gitlab/import_export/members_mapper_spec.rb +++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb @@ -103,11 +103,7 @@ describe Gitlab::ImportExport::MembersMapper, services: true do end before do - GroupMember.add_users_to_group( - group, - [user, user2], - GroupMember::DEVELOPER - ) + group.add_users([user, user2], GroupMember::DEVELOPER) end it 'maps the project member' do @@ -115,7 +111,7 @@ describe Gitlab::ImportExport::MembersMapper, services: true do end it 'maps the project member if it already exists' do - ProjectMember.create!(user: user2, access_level: ProjectMember::MASTER, source_id: project.id) + project.add_master(user2) expect(members_mapper.map[exported_user_id]).to eq(user2.id) end -- cgit v1.2.1 From cd582d3c19843e776cf4aaf151753ec61a9e56ef Mon Sep 17 00:00:00 2001 From: Brian Hall <brian@hack.design> Date: Tue, 31 Jan 2017 13:29:34 -0600 Subject: Remove unnecessary returns / unset variables from the CoffeeScript -> JS conversion. --- spec/javascripts/shortcuts_issuable_spec.js | 47 ++++++++++++++--------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index db2302c4fb0..db11c2516a6 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -11,9 +11,9 @@ beforeEach(function() { loadFixtures(fixtureName); document.querySelector('.js-new-note-form').classList.add('js-main-target-form'); - return this.shortcut = new ShortcutsIssuable(); + this.shortcut = new ShortcutsIssuable(); }); - return describe('#replyWithSelectedText', function() { + describe('#replyWithSelectedText', function() { var stubSelection; // Stub window.gl.utils.getSelectedFragment to return a node with the provided HTML. stubSelection = function(html) { @@ -24,64 +24,61 @@ }; }; beforeEach(function() { - return this.selector = 'form.js-main-target-form textarea#note_note'; + this.selector = 'form.js-main-target-form textarea#note_note'; }); describe('with empty selection', function() { it('does not return an error', function() { this.shortcut.replyWithSelectedText(); - return expect($(this.selector).val()).toBe(''); + expect($(this.selector).val()).toBe(''); }); - return it('triggers `input`', function() { - var focused; - focused = false; + it('triggers `input`', function() { + var focused = false; $(this.selector).on('focus', function() { - return focused = true; + focused = true; }); this.shortcut.replyWithSelectedText(); - return expect(focused).toBe(true); + expect(focused).toBe(true); }); }); describe('with any selection', function() { beforeEach(function() { - return stubSelection('<p>Selected text.</p>'); + stubSelection('<p>Selected text.</p>'); }); it('leaves existing input intact', function() { $(this.selector).val('This text was already here.'); expect($(this.selector).val()).toBe('This text was already here.'); this.shortcut.replyWithSelectedText(); - return expect($(this.selector).val()).toBe("This text was already here.\n\n> Selected text.\n\n"); + expect($(this.selector).val()).toBe("This text was already here.\n\n> Selected text.\n\n"); }); it('triggers `input`', function() { - var triggered; - triggered = false; + var triggered = false; $(this.selector).on('input', function() { - return triggered = true; + triggered = true; }); this.shortcut.replyWithSelectedText(); - return expect(triggered).toBe(true); + expect(triggered).toBe(true); }); - return it('triggers `focus`', function() { - var focused; - focused = false; + it('triggers `focus`', function() { + var focused = false; $(this.selector).on('focus', function() { - return focused = true; + focused = true; }); this.shortcut.replyWithSelectedText(); - return expect(focused).toBe(true); + expect(focused).toBe(true); }); }); describe('with a one-line selection', function() { - return it('quotes the selection', function() { + it('quotes the selection', function() { stubSelection('<p>This text has been selected.</p>'); this.shortcut.replyWithSelectedText(); - return expect($(this.selector).val()).toBe("> This text has been selected.\n\n"); + expect($(this.selector).val()).toBe("> This text has been selected.\n\n"); }); }); - return describe('with a multi-line selection', function() { - return it('quotes the selected lines as a group', function() { + describe('with a multi-line selection', function() { + it('quotes the selected lines as a group', function() { stubSelection("<p>Selected line one.</p>\n\n<p>Selected line two.</p>\n\n<p>Selected line three.</p>"); this.shortcut.replyWithSelectedText(); - return expect($(this.selector).val()).toBe("> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n"); + expect($(this.selector).val()).toBe("> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n"); }); }); }); -- cgit v1.2.1 From dd1c410ea4478400e2650f4102726f2ab1a906bd Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Sun, 29 Jan 2017 17:53:48 -0500 Subject: Reduce the number of loops that Cycle Analytics specs use See https://gitlab.com/gitlab-org/gitlab-ce/issues/27402 --- spec/models/cycle_analytics/code_spec.rb | 22 ++++------ spec/models/cycle_analytics/issue_spec.rb | 12 +++--- spec/models/cycle_analytics/production_spec.rb | 18 +++----- spec/models/cycle_analytics/review_spec.rb | 4 +- spec/models/cycle_analytics/staging_spec.rb | 18 +++----- spec/models/cycle_analytics/test_spec.rb | 50 +++++++++------------- .../cycle_analytics_helpers/test_generation.rb | 50 ++++++++++------------ 7 files changed, 72 insertions(+), 102 deletions(-) diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb index 3b7cc7d9e2e..9053485939e 100644 --- a/spec/models/cycle_analytics/code_spec.rb +++ b/spec/models/cycle_analytics/code_spec.rb @@ -27,15 +27,13 @@ describe 'CycleAnalytics#code', feature: true do context "when a regular merge request (that doesn't close the issue) is created" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) + issue = create(:issue, project: project) - create_commit_referencing_issue(issue) - create_merge_request_closing_issue(issue, message: "Closes nothing") + create_commit_referencing_issue(issue) + create_merge_request_closing_issue(issue, message: "Closes nothing") - merge_merge_requests_closing_issue(issue) - deploy_master - end + merge_merge_requests_closing_issue(issue) + deploy_master expect(subject[:code].median).to be_nil end @@ -60,14 +58,12 @@ describe 'CycleAnalytics#code', feature: true do context "when a regular merge request (that doesn't close the issue) is created" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) + issue = create(:issue, project: project) - create_commit_referencing_issue(issue) - create_merge_request_closing_issue(issue, message: "Closes nothing") + create_commit_referencing_issue(issue) + create_merge_request_closing_issue(issue, message: "Closes nothing") - merge_merge_requests_closing_issue(issue) - end + merge_merge_requests_closing_issue(issue) expect(subject[:code].median).to be_nil end diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb index 5c73edbbc53..fc7d18bd40e 100644 --- a/spec/models/cycle_analytics/issue_spec.rb +++ b/spec/models/cycle_analytics/issue_spec.rb @@ -33,14 +33,12 @@ describe 'CycleAnalytics#issue', models: true do context "when a regular label (instead of a list label) is added to the issue" do it "returns nil" do - 5.times do - regular_label = create(:label) - issue = create(:issue, project: project) - issue.update(label_ids: [regular_label.id]) + regular_label = create(:label) + issue = create(:issue, project: project) + issue.update(label_ids: [regular_label.id]) - create_merge_request_closing_issue(issue) - merge_merge_requests_closing_issue(issue) - end + create_merge_request_closing_issue(issue) + merge_merge_requests_closing_issue(issue) expect(subject[:issue].median).to be_nil end diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb index 591bbdddf55..2cbee741fb0 100644 --- a/spec/models/cycle_analytics/production_spec.rb +++ b/spec/models/cycle_analytics/production_spec.rb @@ -29,11 +29,9 @@ describe 'CycleAnalytics#production', feature: true do context "when a regular merge request (that doesn't close the issue) is merged and deployed" do it "returns nil" do - 5.times do - merge_request = create(:merge_request) - MergeRequests::MergeService.new(project, user).execute(merge_request) - deploy_master - end + merge_request = create(:merge_request) + MergeRequests::MergeService.new(project, user).execute(merge_request) + deploy_master expect(subject[:production].median).to be_nil end @@ -41,12 +39,10 @@ describe 'CycleAnalytics#production', feature: true do context "when the deployment happens to a non-production environment" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) - MergeRequests::MergeService.new(project, user).execute(merge_request) - deploy_master(environment: 'staging') - end + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + MergeRequests::MergeService.new(project, user).execute(merge_request) + deploy_master(environment: 'staging') expect(subject[:production].median).to be_nil end diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb index 33d2c0a7416..febb18c9884 100644 --- a/spec/models/cycle_analytics/review_spec.rb +++ b/spec/models/cycle_analytics/review_spec.rb @@ -23,9 +23,7 @@ describe 'CycleAnalytics#review', feature: true do context "when a regular merge request (that doesn't close the issue) is created and merged" do it "returns nil" do - 5.times do - MergeRequests::MergeService.new(project, user).execute(create(:merge_request)) - end + MergeRequests::MergeService.new(project, user).execute(create(:merge_request)) expect(subject[:review].median).to be_nil end diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb index 00693d67475..104e65335dd 100644 --- a/spec/models/cycle_analytics/staging_spec.rb +++ b/spec/models/cycle_analytics/staging_spec.rb @@ -40,11 +40,9 @@ describe 'CycleAnalytics#staging', feature: true do context "when a regular merge request (that doesn't close the issue) is merged and deployed" do it "returns nil" do - 5.times do - merge_request = create(:merge_request) - MergeRequests::MergeService.new(project, user).execute(merge_request) - deploy_master - end + merge_request = create(:merge_request) + MergeRequests::MergeService.new(project, user).execute(merge_request) + deploy_master expect(subject[:staging].median).to be_nil end @@ -52,12 +50,10 @@ describe 'CycleAnalytics#staging', feature: true do context "when the deployment happens to a non-production environment" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) - MergeRequests::MergeService.new(project, user).execute(merge_request) - deploy_master(environment: 'staging') - end + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + MergeRequests::MergeService.new(project, user).execute(merge_request) + deploy_master(environment: 'staging') expect(subject[:staging].median).to be_nil end diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb index f857ea6cbec..c2ba012a0e6 100644 --- a/spec/models/cycle_analytics/test_spec.rb +++ b/spec/models/cycle_analytics/test_spec.rb @@ -24,16 +24,14 @@ describe 'CycleAnalytics#test', feature: true do context "when the pipeline is for a regular merge request (that doesn't close an issue)" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) - pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) - pipeline.run! - pipeline.succeed! + pipeline.run! + pipeline.succeed! - merge_merge_requests_closing_issue(issue) - end + merge_merge_requests_closing_issue(issue) expect(subject[:test].median).to be_nil end @@ -41,12 +39,10 @@ describe 'CycleAnalytics#test', feature: true do context "when the pipeline is not for a merge request" do it "returns nil" do - 5.times do - pipeline = create(:ci_pipeline, ref: "refs/heads/master", sha: project.repository.commit('master').sha) + pipeline = create(:ci_pipeline, ref: "refs/heads/master", sha: project.repository.commit('master').sha) - pipeline.run! - pipeline.succeed! - end + pipeline.run! + pipeline.succeed! expect(subject[:test].median).to be_nil end @@ -54,16 +50,14 @@ describe 'CycleAnalytics#test', feature: true do context "when the pipeline is dropped (failed)" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) - pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) - pipeline.run! - pipeline.drop! + pipeline.run! + pipeline.drop! - merge_merge_requests_closing_issue(issue) - end + merge_merge_requests_closing_issue(issue) expect(subject[:test].median).to be_nil end @@ -71,16 +65,14 @@ describe 'CycleAnalytics#test', feature: true do context "when the pipeline is cancelled" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) - pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) - pipeline.run! - pipeline.cancel! + pipeline.run! + pipeline.cancel! - merge_merge_requests_closing_issue(issue) - end + merge_merge_requests_closing_issue(issue) expect(subject[:test].median).to be_nil end diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb index 10b90b40ba7..19b32c84d81 100644 --- a/spec/support/cycle_analytics_helpers/test_generation.rb +++ b/spec/support/cycle_analytics_helpers/test_generation.rb @@ -63,22 +63,20 @@ module CycleAnalyticsHelpers # test case. allow(self).to receive(:project) { other_project } - 5.times do - data = data_fn[self] - start_time = Time.now - end_time = rand(1..10).days.from_now - - start_time_conditions.each do |condition_name, condition_fn| - Timecop.freeze(start_time) { condition_fn[self, data] } - end + data = data_fn[self] + start_time = Time.now + end_time = rand(1..10).days.from_now - end_time_conditions.each do |condition_name, condition_fn| - Timecop.freeze(end_time) { condition_fn[self, data] } - end + start_time_conditions.each do |condition_name, condition_fn| + Timecop.freeze(start_time) { condition_fn[self, data] } + end - Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn + end_time_conditions.each do |condition_name, condition_fn| + Timecop.freeze(end_time) { condition_fn[self, data] } end + Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn + # Turn off the stub before checking assertions allow(self).to receive(:project).and_call_original @@ -114,17 +112,15 @@ module CycleAnalyticsHelpers context "start condition NOT PRESENT: #{start_time_conditions.map(&:first).to_sentence}" do context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do it "returns nil" do - 5.times do - data = data_fn[self] - end_time = rand(1..10).days.from_now - - end_time_conditions.each_with_index do |(condition_name, condition_fn), index| - Timecop.freeze(end_time + index.days) { condition_fn[self, data] } - end + data = data_fn[self] + end_time = rand(1..10).days.from_now - Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn + end_time_conditions.each_with_index do |(condition_name, condition_fn), index| + Timecop.freeze(end_time + index.days) { condition_fn[self, data] } end + Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn + expect(subject[phase].median).to be_nil end end @@ -133,17 +129,15 @@ module CycleAnalyticsHelpers context "start condition: #{start_time_conditions.map(&:first).to_sentence}" do context "end condition NOT PRESENT: #{end_time_conditions.map(&:first).to_sentence}" do it "returns nil" do - 5.times do - data = data_fn[self] - start_time = Time.now - - start_time_conditions.each do |condition_name, condition_fn| - Timecop.freeze(start_time) { condition_fn[self, data] } - end + data = data_fn[self] + start_time = Time.now - post_fn[self, data] if post_fn + start_time_conditions.each do |condition_name, condition_fn| + Timecop.freeze(start_time) { condition_fn[self, data] } end + post_fn[self, data] if post_fn + expect(subject[phase].median).to be_nil end end -- cgit v1.2.1 From 21cf64b43140a6e5e9addd514da4304c70bd0815 Mon Sep 17 00:00:00 2001 From: Clement Ho <ClemMakesApps@gmail.com> Date: Wed, 1 Feb 2017 15:40:07 -0600 Subject: Fix filtered search manager spec teaspoon error --- spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 index c8b5c2b36ad..a508dacf7f0 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 @@ -23,6 +23,7 @@ `); spyOn(gl.FilteredSearchManager.prototype, 'bindEvents').and.callFake(() => {}); + spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {}); spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {}); spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {}); spyOn(gl.utils, 'getParameterByName').and.returnValue(null); -- cgit v1.2.1 From ceb1ebd9590aaddc96cc059735bcf571464a8460 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Thu, 11 Aug 2016 18:30:18 +0300 Subject: Active tense test coverage Ports changes from https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/642 back into CE --- spec/features/notes_on_merge_requests_spec.rb | 6 +++--- spec/helpers/diff_helper_spec.rb | 10 +++++----- spec/javascripts/awards_handler_spec.js | 2 +- spec/lib/gitlab/diff/highlight_spec.rb | 8 ++++---- spec/lib/gitlab/diff/parallel_diff_spec.rb | 2 +- spec/lib/gitlab/highlight_spec.rb | 2 +- spec/lib/gitlab/ldap/access_spec.rb | 2 +- spec/requests/api/builds_spec.rb | 2 +- spec/requests/api/groups_spec.rb | 8 ++++---- spec/requests/api/projects_spec.rb | 2 +- spec/requests/ci/api/builds_spec.rb | 4 ++-- spec/services/event_create_service_spec.rb | 6 +++--- spec/services/merge_requests/close_service_spec.rb | 2 +- spec/tasks/gitlab/mail_google_schema_whitelisting.rb | 2 +- 14 files changed, 29 insertions(+), 29 deletions(-) diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index b785b2f7704..fab2d532e06 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -89,7 +89,7 @@ describe 'Comments', feature: true do end end - it 'should reset the edit note form textarea with the original content of the note if cancelled' do + it 'resets the edit note form textarea with the original content of the note if cancelled' do within('.current-note-edit-form') do fill_in 'note[note]', with: 'Some new content' find('.btn-cancel').click @@ -198,7 +198,7 @@ describe 'Comments', feature: true do end describe 'the note form' do - it "shouldn't add a second form for same row" do + it "does not add a second form for same row" do click_diff_line is_expected. @@ -206,7 +206,7 @@ describe 'Comments', feature: true do count: 1) end - it 'should be removed when canceled' do + it 'is removed when canceled' do is_expected.to have_css('.js-temp-notes-holder') page.within("form[data-line-code='#{line_code}']") do diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 468bcc7badc..eae097126ce 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -134,7 +134,7 @@ describe DiffHelper do let(:new_pos) { 50 } let(:text) { 'some_text' } - it "should generate foldable top match line for inline view with empty text by default" do + it "generates foldable top match line for inline view with empty text by default" do output = diff_match_line old_pos, new_pos expect(output).to be_html_safe @@ -143,7 +143,7 @@ describe DiffHelper do expect(output).to have_css 'td:nth-child(3):not(.parallel).line_content.match', text: '' end - it "should allow to define text and bottom option" do + it "allows to define text and bottom option" do output = diff_match_line old_pos, new_pos, text: text, bottom: true expect(output).to be_html_safe @@ -152,7 +152,7 @@ describe DiffHelper do expect(output).to have_css 'td:nth-child(3):not(.parallel).line_content.match', text: text end - it "should generate match line for parallel view" do + it "generates match line for parallel view" do output = diff_match_line old_pos, new_pos, text: text, view: :parallel expect(output).to be_html_safe @@ -162,7 +162,7 @@ describe DiffHelper do expect(output).to have_css 'td:nth-child(4).line_content.match.parallel', text: text end - it "should allow to generate only left match line for parallel view" do + it "allows to generate only left match line for parallel view" do output = diff_match_line old_pos, nil, text: text, view: :parallel expect(output).to be_html_safe @@ -171,7 +171,7 @@ describe DiffHelper do expect(output).not_to have_css 'td:nth-child(3)' end - it "should allow to generate only right match line for parallel view" do + it "allows to generate only right match line for parallel view" do output = diff_match_line nil, new_pos, text: text, view: :parallel expect(output).to be_html_safe diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 71446b9df61..f1bfd529983 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -113,7 +113,7 @@ }); }); describe('::getAwardUrl', function() { - return it('should return the url for request', function() { + return it('returns the url for request', function() { return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/issues-project/issues/1/toggle_award_emoji'); }); }); diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index 1e21270d928..5893485634d 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -12,11 +12,11 @@ describe Gitlab::Diff::Highlight, lib: true do context "with a diff file" do let(:subject) { Gitlab::Diff::Highlight.new(diff_file, repository: project.repository).highlight } - it 'should return Gitlab::Diff::Line elements' do + it 'returns Gitlab::Diff::Line elements' do expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line) end - it 'should not modify "match" lines' do + it 'does not modify "match" lines' do expect(subject[0].text).to eq('@@ -6,12 +6,18 @@ module Popen') expect(subject[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') end @@ -43,11 +43,11 @@ describe Gitlab::Diff::Highlight, lib: true do context "with diff lines" do let(:subject) { Gitlab::Diff::Highlight.new(diff_file.diff_lines, repository: project.repository).highlight } - it 'should return Gitlab::Diff::Line elements' do + it 'returns Gitlab::Diff::Line elements' do expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line) end - it 'should not modify "match" lines' do + it 'does not modify "match" lines' do expect(subject[0].text).to eq('@@ -6,12 +6,18 @@ module Popen') expect(subject[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') end diff --git a/spec/lib/gitlab/diff/parallel_diff_spec.rb b/spec/lib/gitlab/diff/parallel_diff_spec.rb index fe5fa048413..0f779339c54 100644 --- a/spec/lib/gitlab/diff/parallel_diff_spec.rb +++ b/spec/lib/gitlab/diff/parallel_diff_spec.rb @@ -12,7 +12,7 @@ describe Gitlab::Diff::ParallelDiff, lib: true do subject { described_class.new(diff_file) } describe '#parallelize' do - it 'should return an array of arrays containing the parsed diff' do + it 'returns an array of arrays containing the parsed diff' do diff_lines = diff_file.highlighted_diff_lines expected = [ # Unchanged lines diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index fadfe4d378e..e177d883158 100644 --- a/spec/lib/gitlab/highlight_spec.rb +++ b/spec/lib/gitlab/highlight_spec.rb @@ -12,7 +12,7 @@ describe Gitlab::Highlight, lib: true do Gitlab::Highlight.highlight_lines(project.repository, commit.id, 'files/ruby/popen.rb') end - it 'should properly highlight all the lines' do + it 'highlights all the lines properly' do expect(lines[4]).to eq(%Q{<span id="LC5" class="line"> <span class="kp">extend</span> <span class="nb">self</span></span>\n}) expect(lines[21]).to eq(%Q{<span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n}) expect(lines[26]).to eq(%Q{<span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n}) diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb index b9d12c3c24c..9dd997aa7dc 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/ldap/access_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::LDAP::Access, lib: true do it { is_expected.to be_falsey } - it 'should block user in GitLab' do + it 'blocks user in GitLab' do expect(access).to receive(:block_user).with(user, 'does not exist anymore') access.allowed? diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 645e36683bc..bd6e23ee769 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -67,7 +67,7 @@ describe API::Builds, api: true do context 'unauthorized user' do let(:api_user) { nil } - it 'should not return project builds' do + it 'does not return project builds' do expect(response).to have_http_status(401) end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 1187d2e609d..a027c23bb88 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -326,7 +326,7 @@ describe API::Groups, api: true do expect(response).to have_http_status(404) end - it "should only return projects to which user has access" do + it "only returns projects to which user has access" do project3.team << [user3, :developer] get api("/groups/#{group1.id}/projects", user3) @@ -338,7 +338,7 @@ describe API::Groups, api: true do end context "when authenticated as admin" do - it "should return any existing group" do + it "returns any existing group" do get api("/groups/#{group2.id}/projects", admin) expect(response).to have_http_status(200) @@ -346,7 +346,7 @@ describe API::Groups, api: true do expect(json_response.first['name']).to eq(project2.name) end - it "should not return a non existing group" do + it "does not return a non existing group" do get api("/groups/1328/projects", admin) expect(response).to have_http_status(404) @@ -354,7 +354,7 @@ describe API::Groups, api: true do end context 'when using group path in URL' do - it 'should return any existing group' do + it 'returns any existing group' do get api("/groups/#{group1.path}/projects", admin) expect(response).to have_http_status(200) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index a1db81ce18c..753dde0ca3a 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -459,7 +459,7 @@ describe API::Projects, api: true do before { project } before { admin } - it 'should create new project without path and return 201' do + it 'creates new project without path and return 201' do expect { post api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1) expect(response).to have_http_status(201) end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 8dbe5f0b025..1cedaa4ba63 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -458,7 +458,7 @@ describe Ci::API::Builds do before { build.run! } describe "POST /builds/:id/artifacts/authorize" do - context "should authorize posting artifact to running build" do + context "authorizes posting artifact to running build" do it "using token as parameter" do post authorize_url, { token: build.token }, headers @@ -492,7 +492,7 @@ describe Ci::API::Builds do end end - context "should fail to post too large artifact" do + context "fails to post too large artifact" do it "using token as parameter" do stub_application_setting(max_artifacts_size: 0) diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index b7dc99ed887..f2c2009bcbf 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -9,7 +9,7 @@ describe EventCreateService, services: true do it { expect(service.open_issue(issue, issue.author)).to be_truthy } - it "should create new event" do + it "creates new event" do expect { service.open_issue(issue, issue.author) }.to change { Event.count } end end @@ -19,7 +19,7 @@ describe EventCreateService, services: true do it { expect(service.close_issue(issue, issue.author)).to be_truthy } - it "should create new event" do + it "creates new event" do expect { service.close_issue(issue, issue.author) }.to change { Event.count } end end @@ -29,7 +29,7 @@ describe EventCreateService, services: true do it { expect(service.reopen_issue(issue, issue.author)).to be_truthy } - it "should create new event" do + it "creates new event" do expect { service.reopen_issue(issue, issue.author) }.to change { Event.count } end end diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index 5f6a7716beb..d55a7657c0e 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -29,7 +29,7 @@ describe MergeRequests::CloseService, services: true do it { expect(@merge_request).to be_valid } it { expect(@merge_request).to be_closed } - it 'should execute hooks with close action' do + it 'executes hooks with close action' do expect(service).to have_received(:execute_hooks). with(@merge_request, 'close') end diff --git a/spec/tasks/gitlab/mail_google_schema_whitelisting.rb b/spec/tasks/gitlab/mail_google_schema_whitelisting.rb index 80fc8c48fed..8d1cff7a261 100644 --- a/spec/tasks/gitlab/mail_google_schema_whitelisting.rb +++ b/spec/tasks/gitlab/mail_google_schema_whitelisting.rb @@ -20,7 +20,7 @@ describe 'gitlab:mail_google_schema_whitelisting rake task' do Rake.application.invoke_task "gitlab:mail_google_schema_whitelisting" end - it 'should run the task without errors' do + it 'runs the task without errors' do expect { run_rake_task }.not_to raise_error end end -- cgit v1.2.1 From 80ad1c62991fd5e3fcdc0e9d803d4df19bef9bf2 Mon Sep 17 00:00:00 2001 From: samrose3 <sam@gitlab.com> Date: Mon, 23 Jan 2017 18:30:03 -0500 Subject: Support non-ASCII characters in GFM autocomplete --- app/assets/javascripts/gfm_auto_complete.js.es6 | 4 ++-- ...n-does-not-suggest-by-non-ascii-characters-in-name.yml | 4 ++++ spec/features/issues/gfm_autocomplete_spec.rb | 15 ++++++++++++++- 3 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 3f23095dad9..7f1f2a5d278 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -83,12 +83,12 @@ _a = decodeURI("%C3%80"); _y = decodeURI("%C3%BF"); - regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?![" + atSymbolsWithBar + "])([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)$", 'gi'); + regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?![" + atSymbolsWithBar + "])(([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi'); match = regexp.exec(subtext); if (match) { - return match[2] || match[1]; + return (match[1] || match[1] === "") ? match[1] : match[2]; } else { return null; } diff --git a/changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml b/changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml new file mode 100644 index 00000000000..1758ed9e9ea --- /dev/null +++ b/changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml @@ -0,0 +1,4 @@ +--- +title: Support non-ASCII characters in GFM autocomplete +merge_request: 8729 +author: diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 31156fcf994..93139dc9e94 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' feature 'GFM autocomplete', feature: true, js: true do include WaitForAjax - let(:user) { create(:user, username: 'someone.special') } + let(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') } let(:project) { create(:project) } let(:label) { create(:label, project: project, title: 'special+') } let(:issue) { create(:issue, project: project) } @@ -59,6 +59,19 @@ feature 'GFM autocomplete', feature: true, js: true do expect(find('#at-view-64')).to have_selector('.cur:first-of-type') end + it 'includes items for assignee dropdowns with non-ASCII characters in name' do + page.within '.timeline-content-form' do + find('#note_note').native.send_keys('') + find('#note_note').native.send_keys("@#{user.name[0...8]}") + end + + expect(page).to have_selector('.atwho-container') + + wait_for_ajax + + expect(find('#at-view-64')).to have_content(user.name) + end + it 'selects the first item for non-assignee dropdowns if a query is entered' do page.within '.timeline-content-form' do find('#note_note').native.send_keys('') -- cgit v1.2.1 From 2916ea82d9e3bdb69e6eeb9544f494d7269214bd Mon Sep 17 00:00:00 2001 From: Sam Rose <sam@gitlab.com> Date: Wed, 28 Dec 2016 11:53:05 -0500 Subject: Update pipeline and commit URL and text on CI status change --- app/assets/javascripts/merge_request_widget.js.es6 | 22 +++++++++++++++++- .../projects/merge_requests_controller.rb | 3 ++- .../merge_requests/widget/_heading.html.haml | 2 +- .../projects/merge_requests/widget/_show.html.haml | 4 ++++ ...o-not-update-when-new-pipeline-is-triggered.yml | 4 ++++ spec/javascripts/merge_request_widget_spec.js | 26 ++++++++++++++++++++-- 6 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index 7cc319e2f4e..fa782ebbedf 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -154,12 +154,22 @@ return; } if (data.environments && data.environments.length) _this.renderEnvironments(data.environments); - if (data.status !== _this.opts.ci_status && (data.status != null)) { + if (data.status !== _this.opts.ci_status || + data.sha !== _this.opts.ci_sha || + data.pipeline !== _this.opts.ci_pipeline) { _this.opts.ci_status = data.status; _this.showCIStatus(data.status); if (data.coverage) { _this.showCICoverage(data.coverage); } + if (data.pipeline) { + _this.opts.ci_pipeline = data.pipeline; + _this.updatePipelineUrls(data.pipeline); + } + if (data.sha) { + _this.opts.ci_sha = data.sha; + _this.updateCommitUrls(data.sha); + } if (showNotification) { status = _this.ciLabelForStatus(data.status); if (status === "preparing") { @@ -248,6 +258,16 @@ return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-info btn-create').addClass(css_class); }; + MergeRequestWidget.prototype.updatePipelineUrls = function(id) { + const pipelineUrl = this.opts.pipeline_path; + $('.pipeline').text(`#${id}`).attr('href', [pipelineUrl, id].join('/')); + }; + + MergeRequestWidget.prototype.updateCommitUrls = function(id) { + const commitsUrl = this.opts.commits_path; + $('.js-commit-link').text(`#${id}`).attr('href', [commitsUrl, id].join('/')); + }; + return MergeRequestWidget; })(); })(window.gl || (window.gl = {})); diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 3492502e296..6eb542e4bd8 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -434,7 +434,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController title: merge_request.title, sha: (merge_request.diff_head_commit.short_id if merge_request.diff_head_sha), status: status, - coverage: coverage + coverage: coverage, + pipeline: pipeline.try(:id) } render json: response diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 804a4a2473b..0e3af62ebc2 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -10,7 +10,7 @@ = ci_label_for_status(status) for = succeed "." do - = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace" + = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace js-commit-link" %span.ci-coverage - elsif @merge_request.has_ci? diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index 38328501ffd..f07e6b3ad54 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -24,6 +24,10 @@ preparing: "{{status}} build", normal: "Build {{status}}" }, + ci_sha: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.short_sha : ''}", + ci_pipeline: #{@merge_request.head_pipeline.try(:id).to_json}, + commits_path: "#{project_commits_path(@project)}", + pipeline_path: "#{project_pipelines_path(@project)}", pipelines_path: "#{pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}" }; diff --git a/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml b/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml new file mode 100644 index 00000000000..f74e9fa8b6d --- /dev/null +++ b/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml @@ -0,0 +1,4 @@ +--- +title: Update pipeline and commit links when CI status is updated +merge_request: 8351 +author: diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js index bf45100af03..6f1d6406897 100644 --- a/spec/javascripts/merge_request_widget_spec.js +++ b/spec/javascripts/merge_request_widget_spec.js @@ -1,6 +1,7 @@ /* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, quote-props, no-var, max-len */ /*= require merge_request_widget */ +/*= require smart_interval */ /*= require lib/utils/datetime_utility */ (function() { @@ -21,7 +22,11 @@ normal: "Build {{status}}" }, gitlab_icon: "gitlab_logo.png", - builds_path: "http://sampledomain.local/sampleBuildsPath" + ci_pipeline: 80, + ci_sha: "12a34bc5", + builds_path: "http://sampledomain.local/sampleBuildsPath", + commits_path: "http://sampledomain.local/commits", + pipeline_path: "http://sampledomain.local/pipelines" }; this["class"] = new window.gl.MergeRequestWidget(this.opts); }); @@ -118,10 +123,11 @@ }); }); - return describe('getCIStatus', function() { + describe('getCIStatus', function() { beforeEach(function() { this.ciStatusData = { "title": "Sample MR title", + "pipeline": 80, "sha": "12a34bc5", "status": "success", "coverage": 98 @@ -165,6 +171,22 @@ this["class"].getCIStatus(true); return expect(spy).not.toHaveBeenCalled(); }); + it('should update the pipeline URL when the pipeline changes', function() { + var spy; + spy = spyOn(this["class"], 'updatePipelineUrls').and.stub(); + this["class"].getCIStatus(false); + this.ciStatusData.pipeline += 1; + this["class"].getCIStatus(false); + return expect(spy).toHaveBeenCalled(); + }); + it('should update the commit URL when the sha changes', function() { + var spy; + spy = spyOn(this["class"], 'updateCommitUrls').and.stub(); + this["class"].getCIStatus(false); + this.ciStatusData.sha = "9b50b99a"; + this["class"].getCIStatus(false); + return expect(spy).toHaveBeenCalled(); + }); }); }); }).call(this); -- cgit v1.2.1 From f799585c41d801bc657f992adf3d4b201af927d2 Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Wed, 1 Feb 2017 16:38:52 +0000 Subject: Keep snippet visibility on error When a snippet is submitted, but there's an error, we didn't keep the visibility level. As the default is private, this means that submitting a public snippet that failed would then fall back to being a private snippet. --- app/helpers/visibility_level_helper.rb | 4 ---- app/models/snippet.rb | 2 +- app/views/projects/snippets/edit.html.haml | 2 +- app/views/projects/snippets/new.html.haml | 2 +- app/views/shared/snippets/_form.html.haml | 3 +-- app/views/snippets/edit.html.haml | 2 +- app/views/snippets/new.html.haml | 2 +- 7 files changed, 6 insertions(+), 11 deletions(-) diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index 3a83ae15dd8..fc93acfe63e 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -93,10 +93,6 @@ module VisibilityLevelHelper current_application_settings.default_project_visibility end - def default_snippet_visibility - current_application_settings.default_snippet_visibility - end - def default_group_visibility current_application_settings.default_group_visibility end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 771a7350556..960f1521be9 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -17,7 +17,7 @@ class Snippet < ActiveRecord::Base default_content_html_invalidator || file_name_changed? end - default_value_for :visibility_level, Snippet::PRIVATE + default_value_for(:visibility_level) { current_application_settings.default_snippet_visibility } belongs_to :author, class_name: 'User' belongs_to :project diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml index 216f70f5605..fb39028529d 100644 --- a/app/views/projects/snippets/edit.html.haml +++ b/app/views/projects/snippets/edit.html.haml @@ -3,4 +3,4 @@ %h3.page-title Edit Snippet %hr -= render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet), visibility_level: @snippet.visibility_level += render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet) diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml index 772a594269c..cfed3a79bc5 100644 --- a/app/views/projects/snippets/new.html.haml +++ b/app/views/projects/snippets/new.html.haml @@ -3,4 +3,4 @@ %h3.page-title New Snippet %hr -= render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet), visibility_level: default_snippet_visibility += render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet) diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index 0c788032020..2d22782eb36 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -11,7 +11,7 @@ .col-sm-10 = f.text_field :title, class: 'form-control', required: true, autofocus: true - = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: true, form_model: @snippet + = render 'shared/visibility_level', f: f, visibility_level: @snippet.visibility_level, can_change_visibility_level: true, form_model: @snippet .file-editor .form-group @@ -34,4 +34,3 @@ = link_to "Cancel", namespace_project_snippets_path(@project.namespace, @project), class: "btn btn-cancel" - else = link_to "Cancel", snippets_path(@project), class: "btn btn-cancel" - diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml index 82f44a9a5c3..915bf98eb3e 100644 --- a/app/views/snippets/edit.html.haml +++ b/app/views/snippets/edit.html.haml @@ -2,4 +2,4 @@ %h3.page-title Edit Snippet %hr -= render 'shared/snippets/form', url: snippet_path(@snippet), visibility_level: @snippet.visibility_level += render 'shared/snippets/form', url: snippet_path(@snippet) diff --git a/app/views/snippets/new.html.haml b/app/views/snippets/new.html.haml index 79e2392490d..ca8afb4bb6a 100644 --- a/app/views/snippets/new.html.haml +++ b/app/views/snippets/new.html.haml @@ -2,4 +2,4 @@ %h3.page-title New Snippet %hr -= render "shared/snippets/form", url: snippets_path(@snippet), visibility_level: default_snippet_visibility += render "shared/snippets/form", url: snippets_path(@snippet) -- cgit v1.2.1 From c63194ce6f952173649d7de4038aa96348e90565 Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Wed, 1 Feb 2017 18:15:59 +0000 Subject: Check public snippets for spam Apply the same spam checks to public snippets (either personal snippets that are public, or public snippets on public projects) as to issues on public projects. --- app/controllers/concerns/spammable_actions.rb | 2 +- app/controllers/projects/snippets_controller.rb | 8 ++- app/controllers/snippets_controller.rb | 6 +- app/models/concerns/spammable.rb | 8 ++- app/models/project_snippet.rb | 4 ++ app/models/snippet.rb | 12 ++++ app/services/create_snippet_service.rb | 9 ++- app/views/projects/snippets/_actions.html.haml | 5 ++ app/views/snippets/_actions.html.haml | 5 ++ changelogs/unreleased/snippet-spam.yml | 4 ++ config/routes/project.rb | 1 + config/routes/snippets.rb | 1 + lib/api/project_snippets.rb | 2 +- lib/api/snippets.rb | 2 +- .../projects/snippets_controller_spec.rb | 80 ++++++++++++++++++++++ spec/controllers/snippets_controller_spec.rb | 59 ++++++++++++++++ spec/lib/gitlab/import_export/all_models.yml | 1 + spec/requests/api/project_snippets_spec.rb | 48 ++++++++++++- spec/requests/api/snippets_spec.rb | 32 ++++++++- 19 files changed, 277 insertions(+), 12 deletions(-) create mode 100644 changelogs/unreleased/snippet-spam.yml diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb index 99acd98ae13..562f92bd83c 100644 --- a/app/controllers/concerns/spammable_actions.rb +++ b/app/controllers/concerns/spammable_actions.rb @@ -7,7 +7,7 @@ module SpammableActions def mark_as_spam if SpamService.new(spammable).mark_as_spam! - redirect_to spammable, notice: "#{spammable.class} was submitted to Akismet successfully." + redirect_to spammable, notice: "#{spammable.spammable_entity_type.titlecase} was submitted to Akismet successfully." else redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.' end diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 02a97c1c574..5d193f26a8e 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -1,8 +1,9 @@ class Projects::SnippetsController < Projects::ApplicationController include ToggleAwardEmoji + include SpammableActions before_action :module_enabled - before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji] + before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam] # Allow read any snippet before_action :authorize_read_project_snippet!, except: [:new, :create, :index] @@ -36,8 +37,8 @@ class Projects::SnippetsController < Projects::ApplicationController end def create - @snippet = CreateSnippetService.new(@project, current_user, - snippet_params).execute + create_params = snippet_params.merge(request: request) + @snippet = CreateSnippetService.new(@project, current_user, create_params).execute if @snippet.valid? respond_with(@snippet, @@ -88,6 +89,7 @@ class Projects::SnippetsController < Projects::ApplicationController @snippet ||= @project.snippets.find(params[:id]) end alias_method :awardable, :snippet + alias_method :spammable, :snippet def authorize_read_project_snippet! return render_404 unless can?(current_user, :read_project_snippet, @snippet) diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index dee57e4a388..b169d993688 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -1,5 +1,6 @@ class SnippetsController < ApplicationController include ToggleAwardEmoji + include SpammableActions before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download] @@ -40,8 +41,8 @@ class SnippetsController < ApplicationController end def create - @snippet = CreateSnippetService.new(nil, current_user, - snippet_params).execute + create_params = snippet_params.merge(request: request) + @snippet = CreateSnippetService.new(nil, current_user, create_params).execute respond_with @snippet.becomes(Snippet) end @@ -96,6 +97,7 @@ class SnippetsController < ApplicationController end end alias_method :awardable, :snippet + alias_method :spammable, :snippet def authorize_read_snippet! authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet) diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb index 1aa97debe42..1acff093aa1 100644 --- a/app/models/concerns/spammable.rb +++ b/app/models/concerns/spammable.rb @@ -34,7 +34,13 @@ module Spammable end def check_for_spam - self.errors.add(:base, "Your #{self.class.name.underscore} has been recognized as spam and has been discarded.") if spam? + if spam? + self.errors.add(:base, "Your #{spammable_entity_type} has been recognized as spam and has been discarded.") + end + end + + def spammable_entity_type + self.class.name.underscore end def spam_title diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb index 25b5d777641..9bb456eee24 100644 --- a/app/models/project_snippet.rb +++ b/app/models/project_snippet.rb @@ -9,4 +9,8 @@ class ProjectSnippet < Snippet participant :author participant :notes_with_associations + + def check_for_spam? + super && project.public? + end end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 960f1521be9..2665a7249a3 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -7,6 +7,7 @@ class Snippet < ActiveRecord::Base include Sortable include Awardable include Mentionable + include Spammable cache_markdown_field :title, pipeline: :single_line cache_markdown_field :content @@ -46,6 +47,9 @@ class Snippet < ActiveRecord::Base participant :author participant :notes_with_associations + attr_spammable :title, spam_title: true + attr_spammable :content, spam_description: true + def self.reference_prefix '$' end @@ -127,6 +131,14 @@ class Snippet < ActiveRecord::Base notes.includes(:author) end + def check_for_spam? + public? + end + + def spammable_entity_type + 'snippet' + end + class << self # Searches for snippets with a matching title or file name. # diff --git a/app/services/create_snippet_service.rb b/app/services/create_snippet_service.rb index 95cc9baf406..14f5ba064ff 100644 --- a/app/services/create_snippet_service.rb +++ b/app/services/create_snippet_service.rb @@ -1,5 +1,8 @@ class CreateSnippetService < BaseService def execute + request = params.delete(:request) + api = params.delete(:api) + snippet = if project project.snippets.build(params) else @@ -12,8 +15,12 @@ class CreateSnippetService < BaseService end snippet.author = current_user + snippet.spam = SpamService.new(snippet, request).check(api) + + if snippet.save + UserAgentDetailService.new(snippet, request).create + end - snippet.save snippet end end diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index 068a6610350..e2a5107a883 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -8,6 +8,8 @@ - if can?(current_user, :create_project_snippet, @project) = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do New snippet + - if @snippet.submittable_as_spam? && current_user.admin? + = link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam' - if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet) .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } @@ -27,3 +29,6 @@ %li = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do Edit + - if @snippet.submittable_as_spam? && current_user.admin? + %li + = link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index 95fc7198104..9a9a3ff9220 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -8,6 +8,8 @@ - if current_user = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do New snippet + - if @snippet.submittable_as_spam? && current_user.admin? + = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam' - if current_user .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } @@ -26,3 +28,6 @@ %li = link_to edit_snippet_path(@snippet) do Edit + - if @snippet.submittable_as_spam? && current_user.admin? + %li + = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post diff --git a/changelogs/unreleased/snippet-spam.yml b/changelogs/unreleased/snippet-spam.yml new file mode 100644 index 00000000000..4867f088953 --- /dev/null +++ b/changelogs/unreleased/snippet-spam.yml @@ -0,0 +1,4 @@ +--- +title: Check public snippets for spam +merge_request: +author: diff --git a/config/routes/project.rb b/config/routes/project.rb index f36febc6e04..efe2fbc521d 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -64,6 +64,7 @@ constraints(ProjectUrlConstrainer.new) do resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do member do get 'raw' + post :mark_as_spam end end diff --git a/config/routes/snippets.rb b/config/routes/snippets.rb index 3ca096f31ba..ce0d1314292 100644 --- a/config/routes/snippets.rb +++ b/config/routes/snippets.rb @@ -2,6 +2,7 @@ resources :snippets, concerns: :awardable do member do get 'raw' get 'download' + post :mark_as_spam end end diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index 9d8c5b63685..dcc0c82ee27 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -58,7 +58,7 @@ module API end post ":id/snippets" do authorize! :create_project_snippet, user_project - snippet_params = declared_params + snippet_params = declared_params.merge(request: request, api: true) snippet_params[:content] = snippet_params.delete(:code) snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb index e096e636806..eb9ece49e7f 100644 --- a/lib/api/snippets.rb +++ b/lib/api/snippets.rb @@ -64,7 +64,7 @@ module API desc: 'The visibility level of the snippet' end post do - attrs = declared_params(include_missing: false) + attrs = declared_params(include_missing: false).merge(request: request, api: true) snippet = CreateSnippetService.new(nil, current_user, attrs).execute if snippet.persisted? diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb index 32b0e42c3cd..88e4f81f232 100644 --- a/spec/controllers/projects/snippets_controller_spec.rb +++ b/spec/controllers/projects/snippets_controller_spec.rb @@ -69,6 +69,86 @@ describe Projects::SnippetsController do end end + describe 'POST #create' do + def create_snippet(project, snippet_params = {}) + sign_in(user) + + project.team << [user, :developer] + + post :create, { + namespace_id: project.namespace.to_param, + project_id: project.to_param, + project_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params) + } + end + + context 'when the snippet is spam' do + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end + + context 'when the project is private' do + let(:private_project) { create(:project_empty_repo, :private) } + + context 'when the snippet is public' do + it 'creates the snippet' do + expect { create_snippet(private_project, visibility_level: Snippet::PUBLIC) }. + to change { Snippet.count }.by(1) + end + end + end + + context 'when the project is public' do + context 'when the snippet is private' do + it 'creates the snippet' do + expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }. + to change { Snippet.count }.by(1) + end + end + + context 'when the snippet is public' do + it 'rejects the shippet' do + expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. + not_to change { Snippet.count } + expect(response).to render_template(:new) + end + + it 'creates a spam log' do + expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. + to change { SpamLog.count }.by(1) + end + end + end + end + end + + describe 'POST #mark_as_spam' do + let(:snippet) { create(:project_snippet, :private, project: project, author: user) } + + before do + allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true) + stub_application_setting(akismet_enabled: true) + end + + def mark_as_spam + admin = create(:admin) + create(:user_agent_detail, subject: snippet) + project.team << [admin, :master] + sign_in(admin) + + post :mark_as_spam, + namespace_id: project.namespace.path, + project_id: project.path, + id: snippet.id + end + + it 'updates the snippet' do + mark_as_spam + + expect(snippet.reload).not_to be_submittable_as_spam + end + end + %w[show raw].each do |action| describe "GET ##{action}" do context 'when the project snippet is private' do diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index d76fe9f580f..dadcb90cfc2 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -138,6 +138,65 @@ describe SnippetsController do end end + describe 'POST #create' do + def create_snippet(snippet_params = {}) + sign_in(user) + + post :create, { + personal_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params) + } + end + + context 'when the snippet is spam' do + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end + + context 'when the snippet is private' do + it 'creates the snippet' do + expect { create_snippet(visibility_level: Snippet::PRIVATE) }. + to change { Snippet.count }.by(1) + end + end + + context 'when the snippet is public' do + it 'rejects the shippet' do + expect { create_snippet(visibility_level: Snippet::PUBLIC) }. + not_to change { Snippet.count } + expect(response).to render_template(:new) + end + + it 'creates a spam log' do + expect { create_snippet(visibility_level: Snippet::PUBLIC) }. + to change { SpamLog.count }.by(1) + end + end + end + end + + describe 'POST #mark_as_spam' do + let(:snippet) { create(:personal_snippet, :public, author: user) } + + before do + allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true) + stub_application_setting(akismet_enabled: true) + end + + def mark_as_spam + admin = create(:admin) + create(:user_agent_detail, subject: snippet) + sign_in(admin) + + post :mark_as_spam, id: snippet.id + end + + it 'updates the snippet' do + mark_as_spam + + expect(snippet.reload).not_to be_submittable_as_spam + end + end + %w(raw download).each do |action| describe "GET #{action}" do context 'when the personal snippet is private' do diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 7fb6829f582..20241d4d63e 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -52,6 +52,7 @@ snippets: - project - notes - award_emoji +- user_agent_detail releases: - project project_members: diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 01032c0929b..9e25e30bc86 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -4,6 +4,7 @@ describe API::ProjectSnippets, api: true do include ApiHelpers let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } let(:admin) { create(:admin) } describe 'GET /projects/:project_id/snippets/:id' do @@ -50,7 +51,7 @@ describe API::ProjectSnippets, api: true do title: 'Test Title', file_name: 'test.rb', code: 'puts "hello world"', - visibility_level: Gitlab::VisibilityLevel::PUBLIC + visibility_level: Snippet::PUBLIC } end @@ -72,6 +73,51 @@ describe API::ProjectSnippets, api: true do expect(response).to have_http_status(400) end + + context 'when the snippet is spam' do + def create_snippet(project, snippet_params = {}) + project.team << [user, :developer] + + post api("/projects/#{project.id}/snippets", user), params.merge(snippet_params) + end + + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end + + context 'when the project is private' do + let(:private_project) { create(:project_empty_repo, :private) } + + context 'when the snippet is public' do + it 'creates the snippet' do + expect { create_snippet(private_project, visibility_level: Snippet::PUBLIC) }. + to change { Snippet.count }.by(1) + end + end + end + + context 'when the project is public' do + context 'when the snippet is private' do + it 'creates the snippet' do + expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }. + to change { Snippet.count }.by(1) + end + end + + context 'when the snippet is public' do + it 'rejects the shippet' do + expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. + not_to change { Snippet.count } + expect(response).to have_http_status(400) + end + + it 'creates a spam log' do + expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. + to change { SpamLog.count }.by(1) + end + end + end + end end describe 'PUT /projects/:project_id/snippets/:id/' do diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index f6fb6ea5506..6b9a739b439 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -80,7 +80,7 @@ describe API::Snippets, api: true do title: 'Test Title', file_name: 'test.rb', content: 'puts "hello world"', - visibility_level: Gitlab::VisibilityLevel::PUBLIC + visibility_level: Snippet::PUBLIC } end @@ -101,6 +101,36 @@ describe API::Snippets, api: true do expect(response).to have_http_status(400) end + + context 'when the snippet is spam' do + def create_snippet(snippet_params = {}) + post api('/snippets', user), params.merge(snippet_params) + end + + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end + + context 'when the snippet is private' do + it 'creates the snippet' do + expect { create_snippet(visibility_level: Snippet::PRIVATE) }. + to change { Snippet.count }.by(1) + end + end + + context 'when the snippet is public' do + it 'rejects the shippet' do + expect { create_snippet(visibility_level: Snippet::PUBLIC) }. + not_to change { Snippet.count } + expect(response).to have_http_status(400) + end + + it 'creates a spam log' do + expect { create_snippet(visibility_level: Snippet::PUBLIC) }. + to change { SpamLog.count }.by(1) + end + end + end end describe 'PUT /snippets/:id' do -- cgit v1.2.1 From 21ea4863ea42a83566193862579329d865176ff9 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 2 Feb 2017 11:09:24 +0000 Subject: Fixes broken build: Use jquery to get the element position in the page --- spec/features/merge_requests/toggler_behavior_spec.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/features/merge_requests/toggler_behavior_spec.rb b/spec/features/merge_requests/toggler_behavior_spec.rb index 6958f6a2c9f..44a9b545ff8 100644 --- a/spec/features/merge_requests/toggler_behavior_spec.rb +++ b/spec/features/merge_requests/toggler_behavior_spec.rb @@ -18,11 +18,10 @@ feature 'toggler_behavior', js: true, feature: true do it 'should be scrolled down to fragment' do page_height = page.current_window.size[1] page_scroll_y = page.evaluate_script("window.scrollY") - fragment_position_top = page.evaluate_script("document.querySelector('#{fragment_id}').getBoundingClientRect().top") - + fragment_position_top = page.evaluate_script("$('#{fragment_id}').offset().top") expect(find('.js-toggle-content').visible?).to eq true expect(find(fragment_id).visible?).to eq true - expect(fragment_position_top).to be > page_scroll_y + expect(fragment_position_top).to be >= page_scroll_y expect(fragment_position_top).to be < (page_scroll_y + page_height) end end -- cgit v1.2.1 From b329a4675ab3641e5b0526da40ed4f47d61b53d4 Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Thu, 2 Feb 2017 18:22:32 +0600 Subject: removes old css class from everywhere --- app/assets/stylesheets/framework/buttons.scss | 4 ---- app/helpers/blob_helper.rb | 4 ++-- app/views/projects/merge_requests/conflicts/_file_actions.html.haml | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index bb6129158d9..cda46223492 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -330,10 +330,6 @@ } } -.btn-file-option { - background: linear-gradient(180deg, $white-light 25%, $gray-light 100%); -} - .btn-build { margin-left: 10px; diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index c3508443d8a..311a70725ab 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -21,7 +21,7 @@ module BlobHelper options[:link_opts]) if !on_top_of_branch?(project, ref) - button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", title: "You can only edit files when you are on a branch", data: { container: 'body' } + button_tag "Edit", class: "btn disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' } elsif can_edit_blob?(blob, project, ref) link_to "Edit", edit_path, class: 'btn btn-sm' elsif can?(current_user, :fork_project, project) @@ -32,7 +32,7 @@ module BlobHelper } fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params) - link_to "Edit", fork_path, class: 'btn btn-file-option', method: :post + link_to "Edit", fork_path, class: 'btn', method: :post end end diff --git a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml index 2595ce74ac0..0839880713f 100644 --- a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml +++ b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml @@ -8,5 +8,5 @@ '@click' => "onClickResolveModeButton(file, 'edit')", type: 'button' } Edit inline - %a.btn.view-file.btn-file-option{ ":href" => "file.blobPath" } + %a.btn.view-file{ ":href" => "file.blobPath" } View file @{{conflictsData.shortCommitSha}} -- cgit v1.2.1 From 34918d94c011e8f81bd962d43d67fe8bd9f21e3e Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Thu, 2 Feb 2017 13:10:42 +0000 Subject: Use `add_$role` helper in snippets specs --- spec/controllers/projects/snippets_controller_spec.rb | 8 ++++---- spec/requests/api/project_snippets_spec.rb | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb index 88e4f81f232..19e948d8fb8 100644 --- a/spec/controllers/projects/snippets_controller_spec.rb +++ b/spec/controllers/projects/snippets_controller_spec.rb @@ -6,8 +6,8 @@ describe Projects::SnippetsController do let(:user2) { create(:user) } before do - project.team << [user, :master] - project.team << [user2, :master] + project.add_master(user) + project.add_master(user2) end describe 'GET #index' do @@ -73,7 +73,7 @@ describe Projects::SnippetsController do def create_snippet(project, snippet_params = {}) sign_in(user) - project.team << [user, :developer] + project.add_developer(user) post :create, { namespace_id: project.namespace.to_param, @@ -133,7 +133,7 @@ describe Projects::SnippetsController do def mark_as_spam admin = create(:admin) create(:user_agent_detail, subject: snippet) - project.team << [admin, :master] + project.add_master(admin) sign_in(admin) post :mark_as_spam, diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 9e25e30bc86..45d5ae267c5 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -23,7 +23,7 @@ describe API::ProjectSnippets, api: true do let(:user) { create(:user) } it 'returns all snippets available to team member' do - project.team << [user, :developer] + project.add_developer(user) public_snippet = create(:project_snippet, :public, project: project) internal_snippet = create(:project_snippet, :internal, project: project) private_snippet = create(:project_snippet, :private, project: project) @@ -76,7 +76,7 @@ describe API::ProjectSnippets, api: true do context 'when the snippet is spam' do def create_snippet(project, snippet_params = {}) - project.team << [user, :developer] + project.add_developer(user) post api("/projects/#{project.id}/snippets", user), params.merge(snippet_params) end -- cgit v1.2.1