diff options
75 files changed, 1242 insertions, 181 deletions
@@ -32,6 +32,7 @@ gem 'omniauth-saml', '~> 1.7.0' gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth_crowd', '~> 2.2.0' +gem 'omniauth-authentiq', '~> 0.2.0' gem 'rack-oauth2', '~> 1.2.1' gem 'jwt' diff --git a/Gemfile.lock b/Gemfile.lock index 8bc22479346..9f8367b420a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -428,6 +428,8 @@ GEM rack (>= 1.0, < 3) omniauth-auth0 (1.4.1) omniauth-oauth2 (~> 1.1) + omniauth-authentiq (0.2.2) + omniauth-oauth2 (~> 1.3, >= 1.3.1) omniauth-azure-oauth2 (0.0.6) jwt (~> 1.0) omniauth (~> 1.0) @@ -897,6 +899,7 @@ DEPENDENCIES oj (~> 2.17.4) omniauth (~> 1.3.1) omniauth-auth0 (~> 1.4.1) + omniauth-authentiq (~> 0.2.0) omniauth-azure-oauth2 (~> 0.0.6) omniauth-cas3 (~> 1.1.2) omniauth-facebook (~> 4.0.0) diff --git a/app/assets/images/auth_buttons/authentiq_64.png b/app/assets/images/auth_buttons/authentiq_64.png Binary files differnew file mode 100644 index 00000000000..81767bbcc54 --- /dev/null +++ b/app/assets/images/auth_buttons/authentiq_64.png diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 17d03c87bf5..64e6258c154 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -379,14 +379,7 @@ togglePreventSelection(isPrevented = !!this.setting.tabSelectsMatch) { this.setting.tabSelectsMatch = !isPrevented; this.setting.spaceSelectsMatch = !isPrevented; - const eventListenerAction = `${isPrevented ? 'add' : 'remove'}EventListener`; - this.$inputor[0][eventListenerAction]('keydown', gl.GfmAutoComplete.preventSpaceTabEnter); }, - preventSpaceTabEnter(e) { - const key = e.which || e.keyCode; - const preventables = [9, 13, 32]; - if (preventables.indexOf(key) > -1) e.preventDefault(); - } }; }).call(this); diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 index 1c10a7445bb..9c3c96c20ed 100644 --- a/app/assets/javascripts/issuable.js.es6 +++ b/app/assets/javascripts/issuable.js.es6 @@ -1,13 +1,13 @@ -/* eslint-disable func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, prefer-const, padded-blocks, wrap-iife, max-len */ +/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, prefer-const, padded-blocks, wrap-iife, max-len */ /* global Issuable */ /* global Turbolinks */ -(function() { +((global) => { var issuable_created; issuable_created = false; - this.Issuable = { + global.Issuable = { init: function() { Issuable.initTemplates(); Issuable.initSearch(); @@ -111,7 +111,11 @@ filterResults: (function(_this) { return function(form) { var formAction, formData, issuesUrl; - formData = form.serialize(); + formData = form.serializeArray(); + formData = formData.filter(function(data) { + return data.value !== ''; + }); + formData = $.param(formData); formAction = form.attr('action'); issuesUrl = formAction; issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&'); @@ -184,4 +188,4 @@ } }; -}).call(this); +})(window); diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 40bc0579393..3cf49f4ff1b 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -9,6 +9,7 @@ @import "framework/asciidoctor.scss"; @import "framework/blocks.scss"; @import "framework/buttons.scss"; +@import "framework/badges.scss"; @import "framework/calendar.scss"; @import "framework/callout.scss"; @import "framework/common.scss"; diff --git a/app/assets/stylesheets/framework/badges.scss b/app/assets/stylesheets/framework/badges.scss new file mode 100644 index 00000000000..e9d7cda0647 --- /dev/null +++ b/app/assets/stylesheets/framework/badges.scss @@ -0,0 +1,11 @@ +.badge { + font-weight: normal; + background-color: $badge-bg; + color: $badge-color; + vertical-align: baseline; +} + +.badge-dark { + background-color: $badge-bg-dark; + color: $badge-color-dark; +} diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index e4affbb1be1..bbf9de06630 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -76,13 +76,6 @@ color: $black; } } - - .badge { - font-weight: normal; - background-color: $nav-badge-bg; - color: $gl-gray-light; - vertical-align: baseline; - } } &.sub-nav { @@ -434,4 +427,4 @@ border-bottom: none; } } -} +}
\ No newline at end of file diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 460c5d995be..a5fb097a47f 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -280,6 +280,14 @@ $btn-active-gray: #ececec; $btn-active-gray-light: e4e7ed; /* +* Badges +*/ +$badge-bg: #f3f3f3; +$badge-bg-dark: #eee; +$badge-color: #929292; +$badge-color-dark: #8f8f8f; + +/* * Award emoji */ $award-emoji-menu-shadow: rgba(0,0,0,.175); diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 3d60426de01..5517dc5dcbd 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -121,13 +121,6 @@ .folder-name { cursor: pointer; - - .badge { - font-weight: normal; - background-color: $gray-darker; - color: $gl-gray-light; - vertical-align: baseline; - } } } @@ -142,4 +135,4 @@ margin-right: 0; } } -} +}
\ No newline at end of file diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index d834bc29e8f..f6164c8907e 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -909,4 +909,4 @@ min-height: 450px; } } -} +}
\ No newline at end of file diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index e16a553bcda..d6aa4c4c032 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -880,3 +880,23 @@ pre.light-well { width: 30%; } } + +.services-installation-info .row { + margin-bottom: 10px; +} + +.service-installation { + padding: 32px; + margin: 32px; + border-radius: 3px; + background-color: $white-light; + + h3 { + margin-top: 0; + } + + hr { + margin: 32px 0; + border-color: $border-color; + } +} diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index b83c3a872cf..efe9c001bcf 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -82,6 +82,8 @@ 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! + render action: "edit" end end diff --git a/app/controllers/projects/mattermosts_controller.rb b/app/controllers/projects/mattermosts_controller.rb new file mode 100644 index 00000000000..d87dff2a80e --- /dev/null +++ b/app/controllers/projects/mattermosts_controller.rb @@ -0,0 +1,43 @@ +class Projects::MattermostsController < Projects::ApplicationController + include TriggersHelper + include ActionView::Helpers::AssetUrlHelper + + layout 'project_settings' + + before_action :authorize_admin_project! + before_action :service + before_action :teams, only: [:new] + + def new + end + + def create + result, message = @service.configure(current_user, configure_params) + + if result + flash[:notice] = 'This service is now configured' + redirect_to edit_namespace_project_service_path( + @project.namespace, @project, service) + else + flash[:alert] = message || 'Failed to configure service' + redirect_to new_namespace_project_mattermost_path( + @project.namespace, @project) + end + end + + private + + def configure_params + params.require(:mattermost).permit(:trigger, :team_id).merge( + url: service_trigger_url(@service), + icon_url: asset_url('gitlab_logo.png')) + end + + def teams + @teams ||= @service.list_teams(current_user) + end + + def service + @service ||= @project.find_or_initialize_service('mattermost_slash_commands') + end +end diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 92bac149313..1ee6c1d3afa 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -1,5 +1,5 @@ module AuthHelper - PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2).freeze + PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze def ldap_enabled? diff --git a/app/helpers/mattermost_helper.rb b/app/helpers/mattermost_helper.rb new file mode 100644 index 00000000000..49ac12db832 --- /dev/null +++ b/app/helpers/mattermost_helper.rb @@ -0,0 +1,9 @@ +module MattermostHelper + def mattermost_teams_options(teams) + teams_options = teams.map do |id, options| + [options['display_name'] || options['name'], id] + end + + teams_options.compact.unshift(['Select team...', '0']) + end +end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index fd42f2328d8..b52f08c7081 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -98,7 +98,7 @@ class Namespace < ActiveRecord::Base def move_dir if any_project_has_container_registry_tags? - raise Exception.new('Namespace cannot be moved, because at least one project has tags in container registry') + raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry') end # Move the namespace directory in all storages paths used by member projects @@ -111,7 +111,7 @@ class Namespace < ActiveRecord::Base # if we cannot move namespace directory we should rollback # db changes in order to prevent out of sync between db and fs - raise Exception.new('namespace directory cannot be moved') + raise Gitlab::UpdatePathError.new('namespace directory cannot be moved') end end diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 10740275669..6c78c0af71c 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -18,4 +18,34 @@ class MattermostSlashCommandsService < ChatSlashCommandsService def to_param 'mattermost_slash_commands' end + + def configure(user, params) + token = Mattermost::Command.new(user). + create(command(params)) + + update(active: true, token: token) if token + rescue Mattermost::Error => e + [false, e.message] + end + + def list_teams(user) + Mattermost::Team.new(user).all + rescue Mattermost::Error => e + [[], e.message] + end + + private + + def command(params) + pretty_project_name = project.name_with_namespace + + params.merge( + auto_complete: true, + auto_complete_desc: "Perform common operations on: #{pretty_project_name}", + auto_complete_hint: '[help]', + description: "Perform common operations on: #{pretty_project_name}", + display_name: "GitLab / #{pretty_project_name}", + method: 'P', + user_name: 'GitLab') + end end diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb index fff2273f402..4e878ec556a 100644 --- a/app/services/groups/update_service.rb +++ b/app/services/groups/update_service.rb @@ -14,7 +14,13 @@ module Groups group.assign_attributes(params) - group.save + begin + group.save + rescue Gitlab::UpdatePathError => e + group.errors.add(:base, e.message) + + false + end end end end diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb index 927c67b65b0..d9ab8f167d8 100644 --- a/app/validators/project_path_validator.rb +++ b/app/validators/project_path_validator.rb @@ -14,7 +14,8 @@ class ProjectPathValidator < ActiveModel::EachValidator # without tree as reserved name routing can match 'group/project' as group name, # 'tree' as project name and 'deploy_keys' as route. # - RESERVED = (NamespaceValidator::RESERVED + + RESERVED = (NamespaceValidator::RESERVED - + %w[dashboard] + %w[tree commits wikis new edit create update logs_tree preview blob blame raw files create_dir find_file]).freeze diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index ac04f57e217..b69114c96cc 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -31,7 +31,7 @@ = link_to admin_abuse_reports_path, title: "Abuse Reports" do %span Abuse Reports - %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) + %span.badge.badge-dark.count= number_with_delimiter(AbuseReport.count(:all)) - if askimet_enabled? = nav_link(controller: :spam_logs) do diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index f3539fd372d..221f3ec1ffe 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -26,13 +26,13 @@ %span Issues - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute - %span.badge.count= number_with_delimiter(issues.count) + %span.badge.badge-dark.count= number_with_delimiter(issues.count) = nav_link(path: 'groups#merge_requests') do = link_to merge_requests_group_path(@group), title: 'Merge Requests' do %span Merge Requests - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute - %span.badge.count= number_with_delimiter(merge_requests.count) + %span.badge.badge-dark.count= number_with_delimiter(merge_requests.count) = nav_link(controller: [:group_members]) do = link_to group_group_members_path(@group), title: 'Members' do %span diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 904d11c2cf4..cc1571cbb4f 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -61,14 +61,14 @@ %span Issues - if @project.default_issues_tracker? - %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) + %span.badge.badge-dark.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) - if project_nav_tab? :merge_requests = nav_link(controller: :merge_requests) do = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do %span Merge Requests - %span.badge.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) + %span.badge.badge-dark.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) - if project_nav_tab? :wiki = nav_link(controller: :wikis) do diff --git a/app/views/projects/mattermosts/_no_teams.html.haml b/app/views/projects/mattermosts/_no_teams.html.haml new file mode 100644 index 00000000000..605c7f61dee --- /dev/null +++ b/app/views/projects/mattermosts/_no_teams.html.haml @@ -0,0 +1,12 @@ +%p + You aren’t a member of any team on the Mattermost instance at + %strong= Gitlab.config.mattermost.host +%p + To install this service, + = link_to "#{Gitlab.config.mattermost.host}/select_team", target: '__blank' do + join a team + = icon('external-link') + and try again. +%hr +.clearfix + = link_to 'Go back', edit_namespace_project_service_path(@project.namespace, @project, @service), class: 'btn btn-lg pull-right' diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml new file mode 100644 index 00000000000..7980f7c9a72 --- /dev/null +++ b/app/views/projects/mattermosts/_team_selection.html.haml @@ -0,0 +1,44 @@ +%p + This service will be installed on the Mattermost instance at + %strong= link_to Gitlab.config.mattermost.host, Gitlab.config.mattermost.host +%hr += form_for(:mattermost, method: :post, url: namespace_project_mattermost_path(@project.namespace, @project)) do |f| + %h4 Team + %p + = @teams.one? ? 'The team' : 'Select the team' + where the slash commands will be used in + - selected_id = @teams.keys.first if @teams.one? + = f.select(:team_id, mattermost_teams_options(@teams), {}, { class: 'form-control', selected: "#{selected_id}", disabled: @teams.one? }) + .help-block + - if @teams.one? + This is the only team where you are an administrator. + - else + The list shows teams where you are administrator + To create a team, ask your Mattermost system administrator. + To create a team, + = link_to "#{Gitlab.config.mattermost.host}/create_team" do + use Mattermost's interface + = icon('external-link') + %hr + %h4 Command trigger word + %p Choose the word that will trigger commands + = f.text_field(:trigger, value: @project.path, class: 'form-control') + .help-block + %p + Trigger word must be unique, and can't begin with a slash or contain any spaces. + Use the word that works best for your team. + %p + Suggestions: + %code= 'gitlab' + %code= @project.path # Path contains no spaces, but dashes + %code= @project.path_with_namespace + %p + Reserved: + = link_to 'https://docs.mattermost.com/help/messaging/executing-commands.html#built-in-commands', target: '__blank' do + see list of built-in slash commands + = icon('external-link') + %hr + .clearfix + .pull-right + = link_to 'Cancel', edit_namespace_project_service_path(@project.namespace, @project, @service), class: 'btn btn-lg' + = f.submit 'Install', class: 'btn btn-save btn-lg' diff --git a/app/views/projects/mattermosts/new.html.haml b/app/views/projects/mattermosts/new.html.haml new file mode 100644 index 00000000000..96b1d2aee61 --- /dev/null +++ b/app/views/projects/mattermosts/new.html.haml @@ -0,0 +1,8 @@ +.service-installation + .inline.pull-right + = custom_icon('mattermost_logo', size: 48) + %h3 Install Mattermost Command + - if @teams.empty? + = render 'no_teams' + - else + = render 'team_selection' diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 9ab7971b56c..5bc417d1760 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -17,7 +17,7 @@ - # TODO, remove in later versions when services like Jenkins will set CI status via Commit status API .mr-widget-heading - %w[success skipped canceled failed running pending].each do |status| - .ci_widget{class: "ci-#{status}", style: "display:none"} + .ci_widget{class: "ci-#{status} ci-status-icon-#{status}", style: "display:none"} = ci_icon_for_status(status) %span CI build diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index db51c4f8a4e..fc338dcf887 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -8,7 +8,6 @@ .col-lg-9 = form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form| = render 'shared/service_settings', form: form, subject: @service - .footer-block.row-content-block = form.submit 'Save changes', class: 'btn btn-save' diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml new file mode 100644 index 00000000000..8ca4c51a064 --- /dev/null +++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml @@ -0,0 +1,91 @@ +- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}" + +To setup this service: +%ul.list-unstyled + %li + 1. + = link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands' + on your Mattermost installation + %li + 2. + = link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command' + in Mattermost with these options: + +%hr + +.help-form + .form-group + = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :display_name, "GitLab / #{@project.name_with_namespace}", class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#display_name') + + .form-group + = label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#description') + + .form-group + = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block + %p Fill in the word that works best for your team. + %p + Suggestions: + %code= 'gitlab' + %code= @project.path # Path contains no spaces, but dashes + %code= @project.path_with_namespace + + .form-group + = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#request_url') + + .form-group + = label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block POST + + .form-group + = label_tag :response_username, 'Response username', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#response_username') + + .form-group + = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#response_icon') + + .form-group + = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block Yes + + .form-group + = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#autocomplete_hint') + + .form-group + = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#autocomplete_description') + +%hr + +%ul.list-unstyled + %li + 3. After adding the slash command, paste the + + %strong token + into the field below diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml index 01a77a952d1..63b797cd391 100644 --- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml @@ -1,4 +1,4 @@ -- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}" +- enabled = Gitlab.config.mattermost.enabled .well This service allows GitLab users to perform common operations on this @@ -7,93 +7,9 @@ See list of available commands in Mattermost after setting up this service, by entering %code /<command_trigger_word> help - %br - %br - To setup this service: - %ul.list-unstyled - %li - 1. - = link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands' - on your Mattermost installation - %li - 2. - = link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command' - in Mattermost with these options: - - %hr - - .help-form - .form-group - = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :display_name, "GitLab / #{@project.name_with_namespace}", class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#display_name') - - .form-group - = label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#description') - - .form-group - = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block - %p Fill in the word that works best for your team. - %p - Suggestions: - %code= 'gitlab' - %code= @project.path # Path contains no spaces, but dashes - %code= @project.path_with_namespace - - .form-group - = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#request_url') - - .form-group - = label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block POST - - .form-group - = label_tag :response_username, 'Response username', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#response_username') - - .form-group - = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#response_icon') - - .form-group - = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block Yes - - .form-group - = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#autocomplete_hint') - - .form-group - = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#autocomplete_description') - %hr + - unless enabled + = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service - %ul.list-unstyled - %li - 3. After adding the slash command, paste the - %strong token - into the field below +- if enabled + = render 'projects/services/mattermost_slash_commands/installation_info', subject: @service diff --git a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml new file mode 100644 index 00000000000..c929eee3bb9 --- /dev/null +++ b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml @@ -0,0 +1,7 @@ +.services-installation-info + - unless @service.activated? + .row + .col-sm-9.col-sm-offset-3 + = link_to new_namespace_project_mattermost_path(@project.namespace, @project), class: 'btn btn-lg' do + = custom_icon('mattermost_logo', size: 15) + = 'Add to Mattermost' diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml index 07d4927b6c9..e2033654018 100644 --- a/app/views/shared/empty_states/_issues.html.haml +++ b/app/views/shared/empty_states/_issues.html.haml @@ -10,10 +10,10 @@ .text-content - if has_button && current_user %h4 - The Issue Tracker is a good place to add things that need to be improved or solved in a project! + The Issue Tracker is the place to add things that need to be improved or solved in a project %p - An issue can be a bug, a todo or a feature request that needs to be discussed in a project. - Besides, issues are searchable and filterable. + Issues can be bugs, tasks or ideas to be discussed. + Also, issues are searchable and filterable. - if project_select_button = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue' - else diff --git a/app/views/shared/icons/_mattermost_logo.svg.erb b/app/views/shared/icons/_mattermost_logo.svg.erb new file mode 100644 index 00000000000..83fbd1a407d --- /dev/null +++ b/app/views/shared/icons/_mattermost_logo.svg.erb @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="<%= size %>" height="<%= size %>" version="1" viewBox="0 0 501 501"><path d="M236 .7C137.7 7.5 54 68.2 18.2 158.5c-32 81-19.6 172.8 33 242.5 39.8 53 97.2 87 164.3 97 16.5 2.7 48 3.2 63.5 1.2 48.7-6.3 92.2-24.6 129-54.2 13-10.5 33-31.2 42.2-43.7 26.4-35.5 42.8-75.8 49-120.3 1.6-12.3 1.6-48.7 0-61-4-28.3-12-54.8-24.2-79.5-12.8-26-26.5-45.3-46.8-65.8C417.8 64 400.2 49 398.4 49c-.6 0-.4 10.5.3 26l1.3 26 7 8.7c19 23.7 32.8 53.5 38.2 83 2.5 14 3 43 1 55.8-4.5 27.8-15.2 54-31 76.5-8.6 12.2-28 31.6-40.2 40.2-24 17-50 27.6-80 33-10 1.8-49 1.8-59 0-43-7.7-78.8-26-107.2-54.8-29.3-29.7-46.5-64-52.4-104.4-2-14-1.5-42 1-55C90 121.4 132 72 192 49.7c8-3 18.4-5.8 29.5-8.2 1.7-.4 34.4-38 35.3-40.6.3-1-10.2-1-20.8-.4z"/><path d="M322.2 24.6c-1.3.8-8.4 9.3-16 18.7-7.4 9.5-22.4 28-33.2 41.2-51 62.2-66 81.6-70.6 91-6 12-8.4 21-9 33-1.2 19.8 5 36 19 50C222 268 230 273 243 277.2c9 3 10.4 3.2 24 3.2 13.8 0 15 0 22.6-3 23.2-9 39-28.4 45-55.7 2-8.2 2-28.7.4-79.7l-2-72c-1-36.8-1.4-41.8-3-44-2-3-4.8-3.6-7.8-1.4z"/></svg> diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index 40fe53e6a8d..415361f8fbf 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -3,7 +3,7 @@ - show_menu_above = show_menu_above || false - selected_text = selected.try(:title) || params[:milestone_title] - dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone") -- if selected.present? +- if selected.present? || params[:milestone_title].present? = hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id) = dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do diff --git a/changelogs/unreleased/25895-fix-headers-in-ci-api-helpers.yml b/changelogs/unreleased/25895-fix-headers-in-ci-api-helpers.yml new file mode 100644 index 00000000000..b9a8e17c64a --- /dev/null +++ b/changelogs/unreleased/25895-fix-headers-in-ci-api-helpers.yml @@ -0,0 +1,4 @@ +--- +title: Ensure nil User-Agent doesn't break the CI API +merge_request: +author: diff --git a/changelogs/unreleased/25898-ci-icon-color-mr.yml b/changelogs/unreleased/25898-ci-icon-color-mr.yml new file mode 100644 index 00000000000..dd0f93e176f --- /dev/null +++ b/changelogs/unreleased/25898-ci-icon-color-mr.yml @@ -0,0 +1,4 @@ +--- +title: Adds CSS class to status icon on MR widget to prevent non-colored icon +merge_request: 8219 +author: diff --git a/changelogs/unreleased/8038-authentiq-id-oauth-support.yml b/changelogs/unreleased/8038-authentiq-id-oauth-support.yml new file mode 100644 index 00000000000..36f8ac9c840 --- /dev/null +++ b/changelogs/unreleased/8038-authentiq-id-oauth-support.yml @@ -0,0 +1,4 @@ +--- +title: Add Authentiq as Oauth provider +merge_request: 8038 +author: Alexandros Keramidas diff --git a/changelogs/unreleased/badge-color-on-white-bg.yml b/changelogs/unreleased/badge-color-on-white-bg.yml new file mode 100644 index 00000000000..680d7ff11f0 --- /dev/null +++ b/changelogs/unreleased/badge-color-on-white-bg.yml @@ -0,0 +1,4 @@ +--- +title: Added lighter count badge background-color for on white backgrounds +merge_request: 7873 +author: diff --git a/changelogs/unreleased/dz-rename-invalid-groups.yml b/changelogs/unreleased/dz-rename-invalid-groups.yml new file mode 100644 index 00000000000..90af42da01c --- /dev/null +++ b/changelogs/unreleased/dz-rename-invalid-groups.yml @@ -0,0 +1,4 @@ +--- +title: Rename groups with .git in the end of the path +merge_request: 8199 +author: diff --git a/changelogs/unreleased/dz-whitelist-dashboard-project-path.yml b/changelogs/unreleased/dz-whitelist-dashboard-project-path.yml new file mode 100644 index 00000000000..2787a5c57df --- /dev/null +++ b/changelogs/unreleased/dz-whitelist-dashboard-project-path.yml @@ -0,0 +1,4 @@ +--- +title: Allow projects with 'dashboard' as path +merge_request: +author: diff --git a/changelogs/unreleased/fix-copy-issues-empty-state.yml b/changelogs/unreleased/fix-copy-issues-empty-state.yml new file mode 100644 index 00000000000..a87b7612217 --- /dev/null +++ b/changelogs/unreleased/fix-copy-issues-empty-state.yml @@ -0,0 +1,4 @@ +--- +title: Improve copy in Issue Tracker empty state +merge_request: 8202 +author: diff --git a/changelogs/unreleased/fix-group-path-rename-error.yml b/changelogs/unreleased/fix-group-path-rename-error.yml new file mode 100644 index 00000000000..e3d97ae3987 --- /dev/null +++ b/changelogs/unreleased/fix-group-path-rename-error.yml @@ -0,0 +1,4 @@ +--- +title: Fix 500 error renaming group +merge_request: +author: diff --git a/changelogs/unreleased/fix-import-labels-error.yml b/changelogs/unreleased/fix-import-labels-error.yml new file mode 100644 index 00000000000..86cae3a49ff --- /dev/null +++ b/changelogs/unreleased/fix-import-labels-error.yml @@ -0,0 +1,4 @@ +--- +title: Fix project import label priorities error +merge_request: +author: diff --git a/changelogs/unreleased/mattermost-slash-auto-config.yml b/changelogs/unreleased/mattermost-slash-auto-config.yml new file mode 100644 index 00000000000..43014d38769 --- /dev/null +++ b/changelogs/unreleased/mattermost-slash-auto-config.yml @@ -0,0 +1,4 @@ +--- +title: Allow to auto-configure Mattermost +merge_request: 8070 +author: diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index b8b41a0d86c..42e5f105d46 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -368,6 +368,16 @@ production: &base # login_url: '/cas/login', # service_validate_url: '/cas/p3/serviceValidate', # logout_url: '/cas/logout'} } + # - { name: 'authentiq', + # # for client credentials (client ID and secret), go to https://www.authentiq.com/ + # app_id: 'YOUR_CLIENT_ID', + # app_secret: 'YOUR_CLIENT_SECRET', + # args: { + # scope: 'aq:name email~rs address aq:push' + # # redirect_uri parameter is optional except when 'gitlab.host' in this file is set to 'localhost' + # # redirect_uri: 'YOUR_REDIRECT_URI' + # } + # } # - { name: 'github', # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET', @@ -576,4 +586,4 @@ test: admin_group: '' staging: - <<: *base + <<: *base
\ No newline at end of file diff --git a/config/routes/project.rb b/config/routes/project.rb index 335fccb617b..baabd22b840 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -76,6 +76,8 @@ constraints(ProjectUrlConstrainer.new) do end end + resource :mattermost, only: [:new, :create] + resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do member do put :enable diff --git a/db/migrate/20161220141214_remove_dot_git_from_group_names.rb b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb new file mode 100644 index 00000000000..bd0e4b2cc07 --- /dev/null +++ b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb @@ -0,0 +1,82 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveDotGitFromGroupNames < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + include Gitlab::ShellAdapter + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def up + invalid_groups.each do |group| + path_was = group['path'] + path_was_wildcard = quote_string("#{path_was}/%") + path = quote_string(rename_path(path_was)) + + move_namespace(group['id'], path_was, path) + + execute "UPDATE routes SET path = '#{path}' WHERE source_type = 'Namespace' AND source_id = #{group['id']}" + execute "UPDATE namespaces SET path = '#{path}' WHERE id = #{group['id']}" + + select_all("SELECT id, path FROM routes WHERE path LIKE '#{path_was_wildcard}'").each do |route| + new_path = "#{path}/#{route['path'].split('/').last}" + execute "UPDATE routes SET path = '#{new_path}' WHERE id = #{route['id']}" + end + end + end + + def down + # nothing to do here + end + + private + + def invalid_groups + select_all("SELECT id, path FROM namespaces WHERE type = 'Group' AND path LIKE '%.git'") + end + + def route_exists?(path) + select_all("SELECT id, path FROM routes WHERE path = '#{quote_string(path)}'").present? + end + + # Accepts invalid path like test.git and returns test_git or + # test_git1 if test_git already taken + def rename_path(path) + # To stay closer with original name and reduce risk of duplicates + # we rename suffix instead of removing it + path = path.sub(/\.git\z/, '_git') + + counter = 0 + base = path + + while route_exists?(path) + counter += 1 + path = "#{base}#{counter}" + end + + path + end + + def move_namespace(group_id, path_was, path) + repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{group_id}").map do |row| + Gitlab.config.repositories.storages[row['repository_storage']] + end + + # Move the namespace directory in all storages paths used by member projects + repository_storage_paths.each do |repository_storage_path| + # Ensure old directory exists before moving it + gitlab_shell.add_namespace(repository_storage_path, path_was) + + unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path) + Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}" + + # if we cannot move namespace directory we should rollback + # db changes in order to prevent out of sync between db and fs + raise Exception.new('namespace directory cannot be moved') + end + end + + Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) + end +end diff --git a/db/schema.rb b/db/schema.rb index 14801b581e6..13a847827cc 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161213172958) do +ActiveRecord::Schema.define(version: 20161220141214) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -854,7 +854,7 @@ ActiveRecord::Schema.define(version: 20161213172958) do t.datetime "expires_at" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.string "scopes", default: "--- []\n", null: false + t.string "scopes", default: "--- []\n", null: false end add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md index 2fc5d0355b5..13bd501e397 100644 --- a/doc/administration/auth/README.md +++ b/doc/administration/auth/README.md @@ -6,7 +6,7 @@ providers. - [LDAP](ldap.md) Includes Active Directory, Apple Open Directory, Open LDAP, and 389 Server - [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, - Bitbucket, Facebook, Shibboleth, Crowd and Azure + Bitbucket, Facebook, Shibboleth, Crowd, Azure and Authentiq ID - [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS - [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider - [Okta](okta.md) Configure GitLab to sign in using Okta diff --git a/doc/administration/auth/authentiq.md b/doc/administration/auth/authentiq.md new file mode 100644 index 00000000000..3f39539da95 --- /dev/null +++ b/doc/administration/auth/authentiq.md @@ -0,0 +1,69 @@ +# Authentiq OmniAuth Provider + +To enable the Authentiq OmniAuth provider for passwordless authentication you must register an application with Authentiq. + +Authentiq will generate a Client ID and the accompanying Client Secret for you to use. + +1. Get your Client credentials (Client ID and Client Secret) at [Authentiq](https://www.authentiq.com/register). + +2. On your GitLab server, open the configuration file: + + For omnibus installation + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For installations from source: + + ```sh + sudo -u git -H editor /home/git/gitlab/config/gitlab.yml + ``` + +3. See [Initial OmniAuth Configuration](../../integration/omniauth.md#initial-omniauth-configuration) for initial settings to enable single sign-on and add Authentiq as an OAuth provider. + +4. Add the provider configuration for Authentiq: + + For Omnibus packages: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + "name" => "authentiq", + "app_id" => "YOUR_CLIENT_ID", + "app_secret" => "YOUR_CLIENT_SECRET", + "args" => { + scope: 'aq:name email~rs aq:push' + } + } + ] + ``` + + For installations from source: + + ```yaml + - { name: 'authentiq', + app_id: 'YOUR_CLIENT_ID', + app_secret: 'YOUR_CLIENT_SECRET', + args: { + scope: 'aq:name email~rs aq:push' + } + } + ``` + + +5. The `scope` is set to request the user's name, email (required and signed), and permission to send push notifications to sign in on subsequent visits. +See [OmniAuth Authentiq strategy](https://github.com/AuthentiqID/omniauth-authentiq#scopes-and-redirect-uri-configuration) for more information on scopes and modifiers. + +6. Change 'YOUR_CLIENT_ID' and 'YOUR_CLIENT_SECRET' to the Client credentials you received in step 1. + +7. Save the configuration file. + +8. [Reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure) or [restart GitLab](../restart_gitlab.md#installations-from-source) + for the changes to take effect if you installed GitLab via Omnibus or from source respectively. + +On the sign in page there should now be an Authentiq icon below the regular sign in form. Click the icon to begin the authentication process. + +- If the user has the Authentiq ID app installed in their iOS or Android device, they can scan the QR code, decide what personal details to share and sign in to your GitLab installation. +- If not they will be prompted to download the app and then follow the procedure above. + +If everything goes right, the user will be returned to GitLab and will be signed in.
\ No newline at end of file diff --git a/doc/integration/README.md b/doc/integration/README.md index f8ffa6dcb7f..ed843c0bfa9 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -8,7 +8,7 @@ See the documentation below for details on how to configure these services. - [JIRA](../project_services/jira.md) Integrate with the JIRA issue tracker - [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc. - [LDAP](ldap.md) Set up sign in via LDAP -- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd and Azure +- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd, Azure and Authentiq ID - [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider - [CAS](cas.md) Configure GitLab to sign in using CAS - [OAuth2 provider](oauth_provider.md) OAuth2 application creation diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 8a55fce96fe..4c933cef9b7 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -30,6 +30,7 @@ contains some settings that are common for all providers. - [Crowd](crowd.md) - [Azure](azure.md) - [Auth0](auth0.md) +- [Authentiq](../administration/auth/authentiq.md) ## Initial OmniAuth Configuration diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb index 31fbd1da108..5ff25a3a9b2 100644 --- a/lib/ci/api/helpers.rb +++ b/lib/ci/api/helpers.rb @@ -60,7 +60,7 @@ module Ci end def build_not_found! - if headers['User-Agent'].match(/gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /) + if headers['User-Agent'].to_s.match(/gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /) no_content! else not_found! diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 65b229ca8ff..7a649f28340 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -22,7 +22,7 @@ module Gitlab IMPORTED_OBJECT_MAX_RETRIES = 5.freeze - EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels project_label group_label].freeze + EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels].freeze def self.create(*args) new(*args).create @@ -189,7 +189,7 @@ module Gitlab # Otherwise always create the record, skipping the extra SELECT clause. @existing_or_new_object ||= begin if EXISTING_OBJECT_CHECK.include?(@relation_name) - attribute_hash = attribute_hash_for(['events', 'priorities']) + attribute_hash = attribute_hash_for(['events']) existing_object.assign_attributes(attribute_hash) if attribute_hash.any? @@ -210,9 +210,8 @@ module Gitlab def existing_object @existing_object ||= begin - finder_attributes = @relation_name == :group_label ? %w[title group_id] : %w[title project_id] - finder_hash = parsed_relation_hash.slice(*finder_attributes) - existing_object = relation_class.find_or_create_by(finder_hash) + existing_object = find_or_create_object! + # Done in two steps, as MySQL behaves differently than PostgreSQL using # the +find_or_create_by+ method and does not return the ID the second time. existing_object.update!(parsed_relation_hash) @@ -224,6 +223,25 @@ module Gitlab @relation_name == :services && parsed_relation_hash['type'] && !Object.const_defined?(parsed_relation_hash['type']) end + + def find_or_create_object! + finder_attributes = @relation_name == :group_label ? %w[title group_id] : %w[title project_id] + finder_hash = parsed_relation_hash.slice(*finder_attributes) + + if label? + label = relation_class.find_or_initialize_by(finder_hash) + parsed_relation_hash.delete('priorities') if label.persisted? + + label.save! + label + else + relation_class.find_or_create_by(finder_hash) + end + end + + def label? + @relation_name.to_s.include?('label') + end end end end diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb index 65713e73a59..dd99f9bb7d7 100644 --- a/lib/gitlab/middleware/multipart.rb +++ b/lib/gitlab/middleware/multipart.rb @@ -42,7 +42,7 @@ module Gitlab key, value = parsed_field.first if value.nil? - value = File.open(tmp_path) + value = open_file(tmp_path) @open_files << value else value = decorate_params_value(value, @request.params[key], tmp_path) @@ -68,7 +68,7 @@ module Gitlab case path_value when nil - value_hash[path_key] = File.open(tmp_path) + value_hash[path_key] = open_file(tmp_path) @open_files << value_hash[path_key] value_hash when Hash @@ -78,6 +78,10 @@ module Gitlab raise "unexpected path value: #{path_value.inspect}" end end + + def open_file(path) + ::UploadedFile.new(path, File.basename(path), 'application/octet-stream') + end end def initialize(app) diff --git a/lib/gitlab/update_path_error.rb b/lib/gitlab/update_path_error.rb new file mode 100644 index 00000000000..ce14cc887d0 --- /dev/null +++ b/lib/gitlab/update_path_error.rb @@ -0,0 +1,3 @@ +module Gitlab + class UpdatePathError < StandardError; end +end 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 index fb8d7d97f8a..377cb7b1021 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -1,5 +1,12 @@ module Mattermost - class NoSessionError < StandardError; end + 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. # @@ -17,6 +24,8 @@ module Mattermost include Doorkeeper::Helpers::Controller include HTTParty + LEASE_TIMEOUT = 60 + base_uri Settings.mattermost.host attr_accessor :current_resource_owner, :token @@ -26,12 +35,16 @@ module Mattermost end def with_session - raise NoSessionError unless create - - begin - yield self - ensure - destroy + with_lease do + raise Mattermost::NoSessionError unless create + + begin + yield self + rescue Errno::ECONNREFUSED + raise Mattermost::NoSessionError + ensure + destroy + end end end @@ -58,11 +71,15 @@ module Mattermost end def get(path, options = {}) - self.class.get(path, options.merge(headers: @headers)) + handle_exceptions do + self.class.get(path, options.merge(headers: @headers)) + end end def post(path, options = {}) - self.class.post(path, options.merge(headers: @headers)) + handle_exceptions do + self.class.post(path, options.merge(headers: @headers)) + end end private @@ -111,5 +128,33 @@ module Mattermost 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 diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index a763e2c5ba8..98dfb3e5216 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -105,4 +105,25 @@ describe GroupsController do end end end + + describe 'PUT update' do + before do + sign_in(user) + end + + it 'updates the path succesfully' do + post :update, id: group.to_param, group: { path: 'new_path' } + + expect(response).to have_http_status(302) + expect(controller).to set_flash[:notice] + end + + it 'does not update the path on error' do + allow_any_instance_of(Group).to receive(:move_dir).and_raise(Gitlab::UpdatePathError) + post :update, id: group.to_param, group: { path: 'new_path' } + + expect(assigns(:group).errors).not_to be_empty + expect(assigns(:group).path).not_to eq('new_path') + end + end end diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb new file mode 100644 index 00000000000..2ae635a1244 --- /dev/null +++ b/spec/controllers/projects/mattermosts_controller_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe Projects::MattermostsController do + let!(:project) { create(:empty_project) } + let!(:user) { create(:user) } + + before do + project.team << [user, :master] + sign_in(user) + end + + describe 'GET #new' do + before do + allow_any_instance_of(MattermostSlashCommandsService). + to receive(:list_teams).and_return([]) + + get(:new, + namespace_id: project.namespace.to_param, + project_id: project.to_param) + end + + it 'accepts the request' do + expect(response).to have_http_status(200) + end + end + + describe 'POST #create' do + let(:mattermost_params) { { trigger: 'http://localhost:3000/trigger', team_id: 'abc' } } + + subject do + post(:create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + mattermost: mattermost_params) + end + + context 'no request can be made to mattermost' do + it 'shows the error' do + allow_any_instance_of(MattermostSlashCommandsService).to receive(:configure).and_return([false, "error message"]) + + expect(subject).to redirect_to(new_namespace_project_mattermost_url(project.namespace, project)) + end + end + + context 'the request is succesull' do + before do + allow_any_instance_of(Mattermost::Command).to receive(:create).and_return('token') + end + + it 'redirects to the new page' do + subject + service = project.services.last + + expect(subject).to redirect_to(edit_namespace_project_service_url(project.namespace, project, service)) + end + end + end +end diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb index f474e7e891b..274d50e7ce4 100644 --- a/spec/features/projects/services/mattermost_slash_command_spec.rb +++ b/spec/features/projects/services/mattermost_slash_command_spec.rb @@ -4,29 +4,26 @@ feature 'Setup Mattermost slash commands', feature: true do include WaitForAjax let(:user) { create(:user) } - let(:project) { create(:project) } + let(:project) { create(:empty_project) } let(:service) { project.create_mattermost_slash_commands_service } + let(:mattermost_enabled) { true } before do + Settings.mattermost['enabled'] = mattermost_enabled project.team << [user, :master] login_as(user) + visit edit_namespace_project_service_path(project.namespace, project, service) end - describe 'user visites the mattermost slash command config page', js: true do + describe 'user visits the mattermost slash command config page', js: true do it 'shows a help message' do - visit edit_namespace_project_service_path(project.namespace, project, service) - wait_for_ajax expect(page).to have_content("This service allows GitLab users to perform common") end - end - - describe 'saving a token' do - let(:token) { ('a'..'z').to_a.join } it 'shows the token after saving' do - visit edit_namespace_project_service_path(project.namespace, project, service) + token = ('a'..'z').to_a.join fill_in 'service_token', with: token click_on 'Save' @@ -35,14 +32,21 @@ feature 'Setup Mattermost slash commands', feature: true do expect(value).to eq(token) end - end - describe 'the trigger url' do - it 'shows the correct url' do - visit edit_namespace_project_service_path(project.namespace, project, service) + describe 'mattermost service is enabled' do + it 'shows the add to mattermost button' do + expect(page).to have_link 'Add to Mattermost' + end + end + + describe 'mattermost service is not enabled' do + let(:mattermost_enabled) { false } + + it 'shows the correct trigger url' do + value = find_field('request_url').value - value = find_field('request_url').value - expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger") + expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger") + end end end end diff --git a/spec/javascripts/fixtures/issuable_filter.html.haml b/spec/javascripts/fixtures/issuable_filter.html.haml new file mode 100644 index 00000000000..ae745b292e6 --- /dev/null +++ b/spec/javascripts/fixtures/issuable_filter.html.haml @@ -0,0 +1,8 @@ +%form.js-filter-form{action: '/user/project/issues?scope=all&state=closed'} + %input{id: 'utf8', name: 'utf8', value: '✓'} + %input{id: 'check_all_issues', name: 'check_all_issues'} + %input{id: 'search', name: 'search'} + %input{id: 'author_id', name: 'author_id'} + %input{id: 'assignee_id', name: 'assignee_id'} + %input{id: 'milestone_title', name: 'milestone_title'} + %input{id: 'label_name', name: 'label_name'} diff --git a/spec/javascripts/issuable_spec.js.es6 b/spec/javascripts/issuable_spec.js.es6 new file mode 100644 index 00000000000..d61601ee4fb --- /dev/null +++ b/spec/javascripts/issuable_spec.js.es6 @@ -0,0 +1,81 @@ +/* global Issuable */ +/* global Turbolinks */ + +//= require issuable +//= require turbolinks + +(() => { + const BASE_URL = '/user/project/issues?scope=all&state=closed'; + const DEFAULT_PARAMS = '&utf8=%E2%9C%93'; + + function updateForm(formValues, form) { + $.each(formValues, (id, value) => { + $(`#${id}`, form).val(value); + }); + } + + function resetForm(form) { + $('input[name!="utf8"]', form).each((index, input) => { + input.setAttribute('value', ''); + }); + } + + describe('Issuable', () => { + fixture.preload('issuable_filter'); + + beforeEach(() => { + fixture.load('issuable_filter'); + Issuable.init(); + }); + + it('should be defined', () => { + expect(window.Issuable).toBeDefined(); + }); + + describe('filtering', () => { + let $filtersForm; + + beforeEach(() => { + $filtersForm = $('.js-filter-form'); + fixture.load('issuable_filter'); + resetForm($filtersForm); + }); + + it('should contain only the default parameters', () => { + spyOn(Turbolinks, 'visit'); + + Issuable.filterResults($filtersForm); + + expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS); + }); + + it('should filter for the phrase "broken"', () => { + spyOn(Turbolinks, 'visit'); + + updateForm({ search: 'broken' }, $filtersForm); + Issuable.filterResults($filtersForm); + const params = `${DEFAULT_PARAMS}&search=broken`; + + expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params); + }); + + it('should keep query parameters after modifying filter', () => { + spyOn(Turbolinks, 'visit'); + + // initial filter + updateForm({ milestone_title: 'v1.0' }, $filtersForm); + + Issuable.filterResults($filtersForm); + let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`; + expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params); + + // update filter + updateForm({ label_name: 'Frontend' }, $filtersForm); + + Issuable.filterResults($filtersForm); + params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`; + expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params); + }); + }); + }); +})(); diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 931d426c87f..2c0750c3377 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -15,6 +15,28 @@ "type": "ProjectLabel", "priorities": [ ] + }, + { + "id": 3, + "title": "test3", + "color": "#428bca", + "group_id": 8, + "created_at": "2016-07-22T08:55:44.161Z", + "updated_at": "2016-07-22T08:55:44.161Z", + "template": false, + "description": "", + "project_id": null, + "type": "GroupLabel", + "priorities": [ + { + "id": 1, + "project_id": 5, + "label_id": 1, + "priority": 1, + "created_at": "2016-10-18T09:35:43.338Z", + "updated_at": "2016-10-18T09:35:43.338Z" + } + ] } ], "issues": [ diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb index ab1ab22795c..8d925460f01 100644 --- a/spec/lib/gitlab/middleware/multipart_spec.rb +++ b/spec/lib/gitlab/middleware/multipart_spec.rb @@ -12,7 +12,7 @@ describe Gitlab::Middleware::Multipart do expect(app).to receive(:call) do |env| file = Rack::Request.new(env).params['file'] - expect(file).to be_a(File) + expect(file).to be_a(::UploadedFile) expect(file.path).to eq(tempfile.path) end @@ -39,7 +39,7 @@ describe Gitlab::Middleware::Multipart do expect(app).to receive(:call) do |env| file = Rack::Request.new(env).params['user']['avatar'] - expect(file).to be_a(File) + expect(file).to be_a(::UploadedFile) expect(file.path).to eq(tempfile.path) end @@ -54,7 +54,7 @@ describe Gitlab::Middleware::Multipart do expect(app).to receive(:call) do |env| file = Rack::Request.new(env).params['project']['milestone']['themesong'] - expect(file).to be_a(File) + expect(file).to be_a(::UploadedFile) expect(file.path).to eq(tempfile.path) 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 index 3c2eddbd221..74d12e37181 100644 --- a/spec/lib/mattermost/session_spec.rb +++ b/spec/lib/mattermost/session_spec.rb @@ -95,5 +95,29 @@ describe Mattermost::Session, type: :request do 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 diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb index 1ae1483e2a4..d6f4fbd7265 100644 --- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb +++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb @@ -2,4 +2,123 @@ require 'spec_helper' describe MattermostSlashCommandsService, :models do it_behaves_like "chat slash commands service" + + context 'Mattermost API' do + let(:project) { create(:empty_project) } + let(:service) { project.build_mattermost_slash_commands_service } + let(:user) { create(:user)} + + 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 '#configure' do + subject do + service.configure(user, team_id: 'abc', + trigger: 'gitlab', url: 'http://trigger.url', + icon_url: 'http://icon.url/icon.png') + end + + context 'the requests succeeds' do + before do + stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create'). + with(body: { + team_id: 'abc', + trigger: 'gitlab', + url: 'http://trigger.url', + icon_url: 'http://icon.url/icon.png', + auto_complete: true, + auto_complete_desc: "Perform common operations on: #{project.name_with_namespace}", + auto_complete_hint: '[help]', + description: "Perform common operations on: #{project.name_with_namespace}", + display_name: "GitLab / #{project.name_with_namespace}", + method: 'P', + user_name: 'GitLab' }.to_json). + to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: { token: 'token' }.to_json + ) + end + + it 'saves the service' do + expect { subject }.to change { project.services.count }.by(1) + end + + it 'saves the token' do + subject + + expect(service.reload.token).to eq('token') + end + end + + context 'an error is received' 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 'shows error messages' do + succeeded, message = subject + + expect(succeeded).to be(false) + expect(message).to eq('This trigger word is already in use. Please choose another word.') + end + end + end + + describe '#list_teams' do + subject do + service.list_teams(user) + end + + context 'the requests succeeds' do + before do + stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all'). + to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: ['list'].to_json + ) + end + + it 'returns a list of teams' do + expect(subject).not_to be_empty + end + end + + context 'an error is received' do + before do + stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all'). + to_return( + status: 500, + headers: { 'Content-Type' => 'application/json' }, + body: { + message: 'Failed to get team list.' + }.to_json + ) + end + + it 'shows error messages' do + teams, message = subject + + expect(teams).to be_empty + expect(message).to eq('Failed to get team list.') + end + end + end + end end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 79f12ace999..3b5dc98e4d5 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -37,6 +37,11 @@ describe Ci::API::Builds do let(:user_agent) { 'Go-http-client/1.1' } it { expect(response).to have_http_status(404) } end + + context "when runner doesn't have a User-Agent" do + let(:user_agent) { nil } + it { expect(response).to have_http_status(404) } + end end context 'when there is a pending build' do diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb index 9c2331144a0..531180e48a1 100644 --- a/spec/services/groups/update_service_spec.rb +++ b/spec/services/groups/update_service_spec.rb @@ -1,15 +1,15 @@ require 'spec_helper' describe Groups::UpdateService, services: true do - let!(:user) { create(:user) } - let!(:private_group) { create(:group, :private) } - let!(:internal_group) { create(:group, :internal) } - let!(:public_group) { create(:group, :public) } + let!(:user) { create(:user) } + let!(:private_group) { create(:group, :private) } + let!(:internal_group) { create(:group, :internal) } + let!(:public_group) { create(:group, :public) } describe "#execute" do context "project visibility_level validation" do context "public group with public projects" do - let!(:service) { described_class.new(public_group, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL ) } + let!(:service) { described_class.new(public_group, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } before do public_group.add_user(user, Gitlab::Access::MASTER) @@ -23,7 +23,7 @@ describe Groups::UpdateService, services: true do end context "internal group with internal project" do - let!(:service) { described_class.new(internal_group, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE ) } + let!(:service) { described_class.new(internal_group, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE) } before do internal_group.add_user(user, Gitlab::Access::MASTER) @@ -39,7 +39,7 @@ describe Groups::UpdateService, services: true do end context "unauthorized visibility_level validation" do - let!(:service) { described_class.new(internal_group, user, visibility_level: 99 ) } + let!(:service) { described_class.new(internal_group, user, visibility_level: 99) } before do internal_group.add_user(user, Gitlab::Access::MASTER) end @@ -49,4 +49,41 @@ describe Groups::UpdateService, services: true do expect(internal_group.errors.count).to eq(1) end end + + context 'rename group' do + let!(:service) { described_class.new(internal_group, user, path: 'new_path') } + + before do + internal_group.add_user(user, Gitlab::Access::MASTER) + create(:project, :internal, group: internal_group) + end + + it 'returns true' do + expect(service.execute).to eq(true) + end + + context 'error moving group' do + before do + allow(internal_group).to receive(:move_dir).and_raise(Gitlab::UpdatePathError) + end + + it 'does not raise an error' do + expect { service.execute }.not_to raise_error + end + + it 'returns false' do + expect(service.execute).to eq(false) + end + + it 'has the right error' do + service.execute + + expect(internal_group.errors.full_messages.first).to eq('Gitlab::UpdatePathError') + end + + it "hasn't changed the path" do + expect { service.execute}.not_to change { internal_group.reload.path} + end + end + end end |